GotW #97: Assertions (Difficulty: 4/10)

Assertions have been a foundational tool for writing understandable computer code since we could write computer code… far older than C’s assert() macro, they go back to at least John von Neumann and Herman Goldstine (1947) and Alan Turing (1949). [1,2] How well do we understand them… exactly?

[Updated Jan 2: On second thought, I’ll break the “assertions” and “postconditions” into two GotWs. This GotW has the assertion questions, slightly reordered for flow, and #98 will cover postconditions.]

JG Questions

1. What is an assertion, and what is it used for?

2. C++20 supports two main assertion facilities:

  • assert
  • static_assert

For each one, briefly summarize how it works, when it is evaluated, and whether it is possible for the programmer to specify a message to be displayed if the assertion fails.

Guru Questions

3. If an assertion fails, what does that indicate, and who is responsible for fixing the failure? Refer to the following example assertion code in your answer.

// Example 3

void f() {
    int min = /* some computation */;
    int max = /* some other computation */;

    // still yawn more yawn computation

    assert (min <= max);         // A

    // ...
}

4. Are assertions primarily about checking at compile time, at test time, or at run time? Explain.

Notes

Thanks to Wikipedia for pointing out these references.

[1] H. H. Goldstine and J. von Neumann. “Planning and Coding of problems for an Electronic Computing Instrument” (Report on the Mathematical and Logical Aspects of an Electronic Computing Instrument, Part II, Volume I, p. 12; Institute for Advanced Study, April 1947.)

[2] Alan Turing. “Checking a Large Routine” (Report of a Conference on High Speed Automatic Calculating Machines, pp. 67-9, June 1949).

14 thoughts on “GotW #97: Assertions (Difficulty: 4/10)

  1. 1. What is an assertion, and what is it used for?

    An assertion is a test making sure the program is in a valid state before continuing: If the test fails, then the program stops.

    It is used to detect and act on unrecoverable errors (unlike exceptions, which are expected to be recoverable, if catched).

    2. If an assertion fails, what does that indicate, and who is responsible for fixing the failure?

    An assertion indicates a bug in the program: The programmer is responsible of fixing it (by changing the code).

    In the example, the computation somehow breaks the program (as min > max), so it needs to be modified by the programmer.

    3. Are assertions primarily about checking at compile time, at test time, or at run time? Explain.

    All of them.

    Using C’s assert is usually at test time, by design (C asserts are usually disabled). Using static_assert is at compile time by design.

    But nothing says you can’t abort a program when an undesirable problem happens, including during execution (to avoid undefined behavior, for example).

    4. C++20 supports two main assertion facilities:

    assert is a C macro that is a no-op when the macro NDEBUG is defined at the point the macro assert is used, and that actually tests its argument otherwise. The implementation of the assertion can print the argument. We can then hijack the argument by adding a C-style string, as in the following:

    assert(test && “The test has failed”);

    5. What is a postcondition, and how is it related to an assertion?

    A post condition is the assertion happening at the end of the execution of a function body, testing that the program is still in a valid state. For example, a “square” function returning a double could have an assertion on the fact the result should be nul or positive.

    (It’s late, 03:30 am, Paris time, so my brain is already hibernating, which explains I have no better example)

    6. Should postconditions be expected to be true if an exception is thrown? Justify your answer with example(s).

    No.

    If the exception was expected (i.e. part of the contract of that function), then the assertion should indeed be true.

    But there is at least one case when this is not the case:

    Imagine a C-style assert on the following square function:

    // input: a double number
    // output: a double number whose value is input multiplied by input
    // Note: If for some reason, the output cannot be nul or positive, then this is undefined behavior

    double square(double value)
    {
    double result = value * value;
    assert(result >= 0);
    if(result < 0) throw std::exception();
    return result;
    }

    In the example above, it is documented that **anything** can happen if for some reason, the calculation results in a negative value.

    And as such, anything can happen, be it an abort if NDEBUG is not defined, or an exception throwing if NDEBUG is defined.

    Conclusion: If the code was compiled with NDEBUG, the post condition is false, but never tested (as the assert is eliminated away), and an exception is thrown. But the thrown exception should not be expected by the user, because it is not part of the contract of that function. Just some behavior that could be changed anytime.

    7. Should postconditions be able to refer to both the initial (on entry) and final (on exit) value of a parameter, if those could be different? If so, give an example.

    I don't know, but my guess would be "no".

  2. 1. Checking a condition before proceeding.

    2. Programming error in preceding code, so duty falls on the programmer.

    3. During development in general, as they are meant for detecting programming errors.

    4. static_assert issues a compiler error on false condition, and the message was actually required before C++17. assert expands to conditional abort or nothing #if defined NDEBUG. The message must be embedded in condition.

    5. A guarantee ensured by preceding code. Assertions are sometimes used to check them during development.

    6. If postconditions could be satisfied, there would be no need to throw, so no. Template instances could possibly provide exceptions (no pun intended) to this rule, as propagating exceptions thrown by user-given code is the only reasonable thing to do, even if postconditions were satisfied, though I can’t think of any reasonable examples. Anyway, code that expects postconditions satisfied when an exception is thrown shouldn’t pass a review, as throwing is considered license to NOT satisfy postconditions.

    7. When the correct final value depends on initial value, as in x=f(x).

  3. By “nothing” in 4. I mean ((void)0) that I just pasted from cppreference.com

    I hope there are no more opportunities to nitpick in my answer.
    It’s almost 7 am. but I haven’t gotten coffee yet.

  4. I agree with most, but not all of paercebal’s answers.

    On #6, though: Should postconditions be true if an exception is thrown?

    It depends on the nature of the postcondition, and the level of exception guarantee you claim to support.

    One important example comes to mind: If the postconditions test an invariant of a class, and the class offers a strong exception guarantee, then any postconditions that test the invariants of the class must be true.

    For example, consider std::vector. It offers a strong exception guarantee. A push_back() either fully succeeds, or fully fails.* If an exception occurs during push_back(), such that it cannot take on the new element, then the vector retains its previous state. This is part of why std::move_if_noexcept exists: When std::vector resizes, it can only use move semantics to relocate the existing elements if moving them will never throw.

    So, you could write a postcondition that describes the guarantee: If no exception occurs, the vector takes on the new element, otherwise the vector remains unchanged.

    (*One special case: push_back() waives the strong exception guarantee if the move constructor can throw, and the object is not copy-insertable. So, be nice and make your move-ctors and move-assignments noexcept if possible!)

    Postconditions that test whether you succeeded at “doing the thing” may fail if an exception stops you from “doing the thing.” Postconditions that test that invariants remain valid, to the extent of the exception guarantee you offer, must not fail.

  5. I just realised that my answer to 7. is phrased to too narrow meaning. So here’s another try:
    7. When the postcondition depends on both initial and final values. For example, when g(f(x))==x is guaranteed, you can use it to check the correctness of f(x) before x=f(x), and you need both the old and new x for that.

    I’m still not drinking coffee.

  6. #7 is a tough one. To satisfy #7, you would need the original and final values of the parameter alive at the same time, which implies keeping a copy. That may be either costly, or impossible.

    But, the question implies you’ve mutated the parameter. There’s a _reason_ you’ve mutated the parameter directly, as opposed to copying it.

    If the parameter is an _in_ parameter (ie. already a local copy, and not a mutable reference or pointer to a mutable object), then you could choose to make a copy locally. For something like a small scalar, that’s cheap. But, then, why did you mutate the parameter to begin with? Make a copy and mutate the copy. If copies are expensive, then you might want to reconsider the post-condition check.

    If the parameter is an _in-out_ parameter, there’s probably a good reason you’ve decided to buck of the normal advice to “prefer value semantics.” Your function is mutating an object held by the caller, and it’s an object that you’re not moving or copying. Keeping its initial value and its final value seem intractable, in the general case. For copyable objects, it seems like it would cost a full copy. But if I have a std::unique_ptr& in-out parameter, what then? Or worse, std::vector<std::unique_ptr>&?

    Chances are, you don’t need the entire original parameter value and the entire final parameter value at the same time to test the postcondition. You may instead be able to test one or more properties of the original parameter early on, and remember that simpler state to use during the postcondition check.

    For example, suppose I have this:

    void swap(std::unique_ptr& a, std::unique_ptr& b);

    I want my postcondition to check that I *actually* swapped a and b. I _can’t_ copy a or b. But, I can remember the pointer they held.

    void swap(std::unique_ptr& a, std::unique_ptr& b) {
    // Remember the pointer values for post-condition checking.
    T *const orig_a_ptr = a.get();
    T *const orig_b_ptr = b.get();

    std::unique_ptr tmp = std::move(a);
    a = std::move(b);
    b = std::move(tmp);

    // Postcondition assertions:
    assert(a.get() == orig_b_ptr);
    assert(b.get() == orig_a_ptr);
    }

    This one does end up keeping the original value, but not in the original _type_. What about more complex mutations?

    uint16_t ones_comp_fold(const uint32_t orig) {
    const uint32_t fold0 = orig + (orig >> 16);
    const uint32_t fold1 = fold0 + (fold0 >> 16);
    return fold1 ? fold1 : 0xFFFF; // Map +0 to -0
    }

    // Modify an IPv4 address in a packet’s IPv4 header, and update the IP checksum.
    void update_dst_ip(std::vector& packet, const uint32_t new_dst_ip) {
    const uint16_t old_cksum = extract_ip_cksum(packet);
    const uint32_t old_dst_ip = extract_dst_ip(packet);

    // update the header.

    // Check that the IP checksum changed only by as much as we expected.
    const uint16_t new_cksum = extract_ip_cksum(packet);
    assert(ones_comp_fold(new_cksum – old_cksum) == ones_comp_fold(new_ip – old_ip));
    }

    Note: I *think* my math is right there for the IP address example. If it’s not, assume it gets corrected in code review. ;-)

  7. Huh. Looks like WordPress ate the left-angle T right-angle from my unique_ptrs. I wonder, do code tags work?


    template
    void swap(std::unique_ptr& a, std::unique_ptr& b) {
    // Remember the pointer values for post-condition checking.
    T *const orig_a_ptr = a.get();
    T *const orig_b_ptr = b.get();

    std::unique_ptr tmp = std::move(a);
    a = std::move(b);
    b = std::move(tmp);

    // Postcondition assertions:
    assert(a.get() == orig_b_ptr);
    assert(b.get() == orig_a_ptr);
    }

  8. OK, one last try, using &lt; and &gt;. Apologies for the noise. I looked to see whether there were any guidelines on how to correctly post code here, but I didn’t find any.

    Using just <code> tags around the code block, and &lt; and &gt; entities:


    template <typename T>
    void swap(std::unique_ptr<T>& a, std::unique_ptr<T>& b) {
    // Remember the pointer values for post-condition checking.
    T *const orig_a_ptr = a.get();
    T *const orig_b_ptr = b.get();

    std::unique_ptr<T> tmp = std::move(a);
    a = std::move(b);
    b = std::move(tmp);

    // Postcondition assertions:
    assert(a.get() == orig_b_ptr);
    assert(b.get() == orig_a_ptr);
    }

    Using <pre> and <code> tags around the code block, and &lt; and &gt; entities:

    
    template <typename T>
    void swap(std::unique_ptr<T>& a, std::unique_ptr<T>& b) {
      // Remember the pointer values for post-condition checking.
      T *const orig_a_ptr = a.get();
      T *const orig_b_ptr = b.get();
      
      std::unique_ptr<T> tmp = std::move(a);
      a = std::move(b);
      b = std::move(tmp);
    
      // Postcondition assertions:
      assert(a.get() == orig_b_ptr);
      assert(b.get() == orig_a_ptr);
    }
  9. Now that I have slept a little I can imagine that an API function might take the opportunity to do some internal maintenance after satisfying postconditions. It could then propagate an exception as a request for calling code (eg. bad_array_new_length to ask it to release more store). That should be clearly stated in documentation, as normally you would expect only some exception safety guarantee then.

    intvnut suggested that a postcondition could branch at an exception, but I don’t consider it a *true* postcondition.
    Rather it’s two sets of postconditions: one for success and the other for recovery code. I read the question as expecting the same set of postconditions satisfied for recovery code as for success. Postconditions for success should be a subset of postconditions for recovery to make it count.

  10. Seeing that two assertion questions got promoted to guru questions, I think I should pay more attention to them.
    As numbering was also changed, I copy questions too to avoid confusion.

    3. If an assertion fails, what does that indicate, and who is responsible for fixing the failure? Refer to the following example assertion code in your answer.

    
    void f() {
        int min = /* some computation */;
        int max = /* some other computation */;
    
        // still yawn more yawn computation
    
        assert (min <= max);		// A
    
        // ...
    }
    

    Assertion failure implies that NDEBUG wasn’t defined at the latest inclusion of before compiling the assert. So min <= max was evaluated and found to be false. Assert calls abort(), which neither performs cleanup nor allows recovery, so it is meant for catching programming errors during development only.
    Having assert there implies that having min <= max isn’t just important to proceed, but that it’s supposed to be ensured by preceding code. So whoever is responsible for preceding code should fix it.
    In theory the error could also be in the assertion, but identifiers suggest that it is correct.

    4. Are assertions primarily about checking at compile time, at test time, or at run time? Explain.

    They are meant to assure the correctness of code, so compile time is preferable when possible. Also, all tests are assertions by definition. Testing the whole program usually requires running it, so you need run time assertions too.

    At this point I think I could improve my answer to 1 too.

    1. What is an assertion, and what is it used for?

    A safety (or sanity) check before proceeding, assuring the correctness of code during development.

    The more I read question 2 the more I feel like it could be answered by pasting the respective pages from cppreference :p
    A remark that should be made about assert is that its condition is never evaluated if NDEBUG was defined when was last included before the assertion, so you can’t rely on its side effects.

    Apparently I have recently developed a tendency to answer GotW between 3 am. and 7 am.

  11. I forgot that WP consumes &lt and &gt. &lt assert &gt is the missing word in at least 2 places in my previous comment.

  12. 1. What is an assertion, and what is it used for?

    An assertion is a statement about the effects of some code.
    Very much like a comment, with the difference that it can be
    compiled, and potentially be executed.

    It is used in the same way as often a comment is also used:
    It restates what the code already states, but in a much simpler
    and easier to grasp form. The execution of the assertion statement
    (at compile time or run time, respectively) provides us a tool to verify the statement:
    if it is true, compilation or execution respectively will pass the point of the statement
    without failure.

    2. If an assertion fails, what does that indicate, and who is responsible for fixing the failure?

    If an assertion fails, this indicates that the statement about the code which it represents
    is false. The reason for this could be the assertion statement itself or it could be any
    computation leading up to that statement. In the given example, this could be any
    of the commented-out sections or the assertion statement A itself.
    In any case, this is an error in the code which must be fixed by programmers.

    If an assertion is used to “check” the preconditions of a function, a failing assertion
    also indicates a programming error. However, this time the programming error is in the code
    calling the function.

    3. Are assertions primarily about checking at compile time, at test time, or at run time? Explain.

    Static assertions: Compile time and thus also test time. Never run time.

    Run-time assertions: Definitely at test time. Sometimes they are kept enabled after deployment
    (which is probably meant by run time here). This is to provide better diagnostics in case of programming
    errors (it does not change their purpose and meaning, see 2), which could also be seen as some kind
    of testing.

    4. C++20 supports two main assertion facilities…

    Skipping that …

    5. What is a postcondition, and how is it related to an assertion?

    A postcondition is a statement which holds true after a function returns _successfully_.
    It does not necessarily make sense to always express and execute postcondition checks as assertions.
    For example, for a function which sorts a range, the postcondition is that the range is sorted.
    An executable run-time assertion could lead to signification inefficieny. However, it still makes
    sense to verify a postcondition in tests.
    If a postcondition can be reasonably expressed as an assertion, then adding this assertion
    either in the function body right before the successful return, or at the call site right after
    the function call, will not change the behavior (i.e. the assertion does not fire) of a correct program.

    6. Should postconditions be expected to be true if an exception is thrown?

    An exception indicates unsuccessful execution of a function. Thus, postconditions, which relate
    to the successful function return (see 5) do not necessarily hold.

    For example, a postcondition for vector::resize is that size() == newSize after return. When
    an exception is thrown, this function provides the strong guarantee (under some conditions),
    so in this case the size will not have changed.
    However, some postconditions (or parts of them) will still hold in a well-designed function.
    The basic exception guarantee states that all class invariants are maintained and that
    no resources are leaked, so we should be able to rely on these partial postconditions even when an
    exception is thrown.

    The fact that not all postconditions necessarily hold in case of exceptions in contrast
    to a successful function return, is consistent with the way exceptions work:
    When an exception is thrown, the control flow can be assumed to be transferred to
    the point where the exception is caught. At that point, we usually do not have access to
    all the details which make up the postcondition or are at least not interested in them.

    7. Should postconditions be able to refer to both the initial (on entry) and final (on exit) value of a parameter,
    if those could be different?

    Generally yes, when viewing postconditions as statements about the code, not necessarily ones which are
    expressed in code or executed, see 5.
    When they are expressed in code, it would be convenient to be able to refer to the previous value. However,
    then it should also be possible to fine-tune whether the assertion is actually executed to prevent
    any severe performance hits.

    Example for a postcondtion refering to the old value:
    vector::insert(const_iterator pos, size_type count, value_type const& value).
    Postcondition (on success, see 6): size() == old_size + count.

    Note: Members, or in this case properties, of a class object are also seen as parameters
    of a member function.

  13. 1. An assertion is a compilable statement that is meant to hold true. When it’s not the case, this means there is a logic error somewhere: variables or expressions not in the expected state, or may be the expectation (i.e. the assertion) is itself buggy.
    Their presence shall not alter the flow of a correct program.

    They are used to help us quickly identify and fix logic errors.

    2. static_assert() is evaluated at compile time. This means the statement shall be a constant expression. It appeared in C++11, and since C++17 the second parameter is no longer mandatory. This second parameter permits to provide a more intelligible error message in case the statement is false.

    assert() is a macro which is inhibited when `NDEDUB` is defined, which is usually the case in _Release_ mode. C++ has no notion of compilation mode, yet several compilation chains define the Release mode as compiled for speed, and NDEBUG defined — I know this is the case for at least CMake and MSVC.

    assert() default behaviour (without NDEBUG) is to display the expression in stderr and to std::abort the execution of the program when the statement doesn’t hold true when evaluated. It’s mainly meant to be used during test phases. An aborted program execution yields a very nice context for investigation. Whether we have a core dumped or a stopped execution in the debugger, we can have access to the full state of the program (non optimized-out variables, call stacks, threads…)

    Given how C and C++ evaluate boolean expressions, we can provide more insightful error messages like for instance

    assert(x >= 0 && “cannot compute square root of a negative number”);
    // or
    assert(! “unexpected case”);

    Finally as assert() is a macro, it can be overridden (or used as an inspiration to cook our own version). On the subject I’ve seen a very nice trick lately to issue even better error messages: https://artificial-mind.net/blog/2020/09/19/destructuring-assertions.

    3. A failed assertion means there is a bug somewhere, that we detect when the assertion is executed. In the given example the error could be in many places:

    – the assertion itself may be flawed — yet here we can suppose that min means minimum and max maximum and thus expecting ‘min <= max' to be perfectly valid.
    – in the '…' part if the variables min and max are altered — as they aren't const, it's possible. If the '…' zone isn't supposed to change min and max values, then I definitively prefer to declare them const. This way, on a failed assertion, I know I don't have to investigate the code in the three dots..
    – in the post-condition of the initial computations of min and max — may be the expression used to compute them is incorrect. This is what we usually expect to detect when we write assertions in the middle of a function.
    – in the pre-conditions of the initial computations of min and max — if either is computed from `sqrt(x)` and x is negative. In that case it would have been better to have another assertion on x before the initial computations.
    – and finally in C and in C++, we can also have undefined behaviours that corrupt the state of either variable. That's the worst case scenario. It's possible another assertion defined earlier would have helped to detect the error (like on std::vector::operator[]; it would have been nice). We can use other tools to help us here: static analysis, sanitizers, valgrind…

    4. In an ideal world assert statements should be analysed by static analysers, but very few do it. So far only static_assert is evaluated at compile time. In constexpr functions, we may have assert() evaluated at compile time.

    Otherwise as I've written earlier, assert() is mainly used at execution time during test phases. We could also use it all the time (and execution time), but this is not how it's meant to be used.

  14. A1: Assertions check that a condition is true, and throw a hard error if the condition is not true. It is used to check pre-conditions or post-conditions that are expected to always be true.

    A2: `assert` is a macro (almost always a macro?) contained in the `cassert` header. If NDEBUG is defined it is a no-op, and any code within the `assert` is not executed. (Warning: do not rely on side-effects of code within an assert!) If NDEBUG is not defined (i.e. you are running in debug mode) an assert may, but is not required to, print some diagnostic information and will typically break execution if a supported debugger is attached. It is required to call `std::abort`, forcing the error to be addressed.

    `assert` does not have any standard mechanism to print a message out, although there are conventions to allow it, which make use of the fact that pretty much all implementations print the condition somewhere in their diagnostic information. This allows `assert(x != y && “x and y must be different”);` to compile.

    `static_assert` is a language feature that accepts a bool that can be evaluated at compile time. If the condition is `false`, it causes a compiler error. There are two versions (as of C++17), one taking a message to be printed to the compiler output and one without. Messages to the programmer can be placed here.

    A3: Because assertions only do anything at compile time or in debug builds, they indicate cases where the programmer has made a mistake. In particular, they are not suitable for verifying user input. Nor are they suitable for handling actual error cases that may be expected at run time. For example: in the example if `min` and `max` are input by the user or from an untrusted data source, the assert is not an appropriate way to check this. If the min and max are calculated by the code it is appropriate to assert that it is correct. I find asserts are particularly useful for checking the output from a function is sensible before returning, and it can often be used to verify that the input is correct – particularly if the contract specifies that “behaviour is undefined if…” (e.g. checking the input to a square root function is >= 0).

    Because of this, the responsibility is always on the programmer to fix it. If an error cannot be 100% fixed by the programmer an assert is not the correct tool to check for it.

    A4: static_assert is for compile time checks. assert() is for test time checks (ensure an appropriate build is used). Exceptions, error codes, or other error handling facilities (e.g. correct the incoming data where possible) should be used for runtime errors.

Comments are closed.