CS 70

Using assert in C++

The assert macro provided by the CS 70 Testing Library is a debugging tool that helps you catch programming errors early. It's particularly useful for checking internal invariants—conditions that should always be true if your code is working correctly.

When to Use assert

C++ provides several ways to handle problems, and knowing which to use in a particular situation is important:

  • throw: Use exceptions for problems caused by external circumstances beyond your control (e.g., a file doesn't exist, you're asked to create an impossibly large image, the network is down, or the user provided invalid input).

  • affirm_expected and friends: Use these in your test code to verify that your implementation behaves correctly (see the testing library documentation for details).

  • assert: Use this macro to catch your own programming mistakes—situations that should never happen if your code is correct.

  • Duck speaking

    So assert is like a sanity check for my own code?

  • LHS Cow speaking

    Exactly! If an assert fails, it means you have a bug in your implementation, not a problem with external data or user input.

What Should You Assert?

Good candidates for assert include:

  • Invariants: Conditions that should always be true at certain points in your code. For example, in an array-backed list, the size of the list should never exceed the capacity of the underlying array.

  • Preconditions: Requirements that must be true before a function can work correctly. If the assignment specification says certain inputs cause undefined behavior, you can use assert to catch when those invalid inputs are accidentally passed.

  • Postconditions: Guarantees that should be true after a function completes.

Our strong recommendation: If the assignment specification says the consequences of misuse are not specified and you can easily detect that misuse, you should add an assert.

In Homework 5, you wrote (or will write) a check_invariants() function for your IntList class using assert. That's a perfect example of this principle in action—checking that the internal state of your data structure is consistent.

How to Use assert

To use assert, first include the header:

#include <cassert>

Then call assert with a boolean condition. If the condition is false, the program terminates immediately with an error message showing the file name, line number, and the condition that failed.

Here's an example from a dynamic-array implementation:

void DynamicArray::push_back(int value) {
    // Make sure we have space
    if (size_ >= capacity_) {
        resize();
    }

    // This should NEVER be true if resize() worked correctly
    assert(size_ < capacity_);

    data_[size_] = value;
    ++size_;
}

int DynamicArray::get(size_t index) const {
    // The spec says calling get with an out-of-bounds index
    // is undefined behavior, so we can assert this precondition
    assert(index < size_);

    return data_[index];
}
  • Hedgehog speaking

    Wait, but shouldn't get throw an exception if the index is out of bounds?

  • LHS Cow speaking

    That depends on the specification! Some interfaces (like std::vector::at) guarantee an exception. Others (like std::vector::operator[]) say out-of-bounds access is undefined behavior. If it's undefined behavior, assert is perfect for catching misuse during development.

How assert Helps Find Bugs

When something goes wrong in your program, the actual crash often happens far from where the bug originated. A bad pointer might get passed around through several functions before finally causing a segfault.

Assertions catch problems early, right where they happen. Instead of getting a cryptic segfault later, you get a clear message pointing to exactly which invariant was violated and where.

For example, imagine you have a bug that causes your array's size to exceed its capacity. Without assertions, you might not notice until much later when you try to access memory that's been corrupted. With an assert(size_ <= capacity_) at key points, you'll catch the problem immediately after the bug occurs, making it much easier to track down.

  • Goat speaking

    Meh. Seems like a lot of extra work. And it'll probably slow down my code.

  • Pig speaking

    Isn't C++ about the MOST SPEED POSSIBLE?

  • LHS Cow speaking

    First of all, the few seconds it takes you to add an assert while coding is nothing compared to the minutes (or, worse, hours!) of frustration you might face tracking down a bug that does subtle damage. And on a modern machine, the overheads of a typical assert are negligible.

  • Bjarne speaking

    But if you want zero overhead, C++ gives you that option. Check out the deeper-dive section below!

Efficiency and the -DNDEBUG Flag

Assertions do have a small performance cost—they're extra checks that run every time you execute that code. For most programs this cost is negligible, but (very) occasionally you might need to worry about it.

C++ provides a way to disable assertions in release builds: compile with the -DNDEBUG flag (where “NDEBUG” stands for “No Debug”). When you compile with -DNDEBUG, all assert statements are completely removed from your code—they don't even evaluate their conditions.

# Debug build (assertions enabled)
clang++ -g -std=c++20 -Wall -Wextra myprogram.cpp

# Release build (assertions disabled)
clang++ -O3 -std=c++20 -DNDEBUG myprogram.cpp
  • Duck speaking

    So I can leave my asserts in the code and they'll just disappear in the final version?

  • LHS Cow speaking

    Yes! That's the beauty of it. You get safety during development and testing, but can opt for maximum performance in production.

Never Put Side Effects in an assert

Because assertions can be disabled, you must never put code with side effects in an assert statement. Side effects are operations that change program state, such as modifying variables, printing output, or calling functions that do these things.

// BAD - Don't do this!
assert(++count < MAX_SIZE);  // count only increments if assertions are enabled!

// BAD - Don't do this either!
assert(file.read(buffer, size));  // File only gets read in debug builds!

// GOOD - Separate the operation from the check
++count;
assert(count < MAX_SIZE);

// GOOD - Check the result of the operation
bool success = file.read(buffer, size);
assert(success);

If you violate this rule, your program will behave differently in debug builds versus release builds, which is a recipe for cooking up some truly nightmarish bugs.

  • Hedgehog speaking

    That sounds terrifying! How do I make sure I don't do that by accident?

  • LHS Cow speaking

    If your assert contains a function call, ask yourself: “Does this function do something, or just check something?” If it does something that changes state, pull that operation out before the assert.

Summary

Use assert to catch your own programming mistakes during development:

  • Check invariants, preconditions, and postconditions.
  • Add assertions when the spec says behavior is undefined and you can easily detect misuse.
  • Assertions help you find bugs early, close to where they originate.
  • In release builds, assertions can be disabled with -DNDEBUG.
  • Never put code with side effects inside an assert.

When used thoughtfully, assertions make your code more reliable and your debugging sessions much shorter.

(When logged in, completion status appears here.)