Assertions and static_assert

An assertion states something the code believes and checks it while the program runs in a debug build. A static assertion states something the compiler can check while it compiles, and it costs nothing at run time because there is no run time.

The two are for different mistakes. assert finds the caller that passed a null pointer; static_assert finds the template that was instantiated with the wrong type. Both turn a silent wrong answer into a message with a file and a line.

assert

assert takes a condition and does nothing when it is true. It is a macro from <cassert> that records the text of the condition, the file and the line, and it is compiled in unless NDEBUG is defined.

The program checks its arguments and its results, and prints a line when every check passed. A passing assertion is invisible, which is why a program with many of them looks the same as one with none.

The check belongs where the code assumes something it does not control: a parameter, an index, an invariant after a loop. Do not use it for input from a user, which is a case for an error, not a crash.

#include <cassert> #include <iostream> int divide(int a, int b) { assert(b != 0); return a / b; } int main() { std::cout << divide(10, 2) << '\n'; assert(divide(9, 3) == 3); std::cout << "all checks passed\n"; }

g++ -std=c++23 -Wall -Wextra assert.cpp -o assert_demo
./assert_demo

5
all checks passed

A failed assertion

When the condition is false, assert writes a message to stderr with the condition, the file and the line, and calls abort. The message is the report a developer needs, and the abort is the point: the program stops before it uses the wrong value.

The program asserts that a negative value is positive, and the run under GDB shows the message and the abort. The frames after it are the C library's, not the program's.

A failed assertion is a bug in the program, not a condition to recover from. That is why the standard makes it fatal and why release builds remove it.

#include <cassert> int main() { int value = -1; assert(value > 0); return 0; }

g++ -g -std=c++23 -Wall -Wextra fail.cpp -o assert_fail
gdb -q -batch -ex 'set disable-randomization off' -ex run -ex 'bt 3' ./assert_fail

Assertion failed: value > 0 (fail.cpp: main: 6)

Program received signal SIGABRT, Aborted.
__restore_sigs (set=set@entry=0x7fff46125020) at ./arch/x86_64/syscall_arch.h:40
warning: 40 ./arch/x86_64/syscall_arch.h: No such file or directory
#0 __restore_sigs (set=set@entry=0x7fff46125020) at ./arch/x86_64/syscall_arch.h:40
#1 0x00007749dace10cc in raise (sig=sig@entry=6) at src/signal/raise.c:11
#2 0x00007749dacc469e in abort () at src/exit/abort.c:11

NDEBUG removes the check

Defining NDEBUG before the include makes assert expand to nothing. The condition is not evaluated, so the check costs no time and the failing assertion no longer stops the program.

The program is the same failing assertion, compiled with -DNDEBUG. It prints the value and finishes, which shows both the saving and the risk: the bug is still there, and the release build no longer reports it.

Build systems define NDEBUG in the release configuration, which is why a program can behave differently in Debug and Release. Tests should run against a build where the assertions are on.

#include <cassert> #include <iostream> int main() { int value = -1; assert(value > 0); std::cout << "checked " << value << '\n'; }

g++ -DNDEBUG -std=c++23 -Wall -Wextra ndebug.cpp -o ndebug_demo
./ndebug_demo

checked -1

[[nodiscard]]

The attribute [[nodiscard]] on a function says that its result matters. Ignoring it is not an error, but the compiler warns, and the warning names the call and the declaration.

The program calls compute() and throws the result away. The message says the return value is ignored and points at the attribute, which is the whole point of the attribute.

Use it on functions that return a status, a new object or a value that the caller must not drop. It is a compile-time check with no run-time cost, and it works on types too, not only functions.

#include <iostream> [[nodiscard]] int compute() { return 42; } int main() { compute(); std::cout << "done\n"; }

g++ -std=c++23 -Wall -Wextra nodiscard.cpp -o nodiscard
./nodiscard

nodiscard.cpp: In function 'int main()':
nodiscard.cpp:10:12: warning: ignoring return value of 'int compute()', declared with attribute 'nodiscard' [-Wunused-result]
10 | compute();
| ~~~~~~~^~
nodiscard.cpp:3:19: note: declared here
3 | [[nodiscard]] int compute()
| ^~~~~~~
done

A static assertion

static_assert takes a constant expression and a message, and stops the build when the expression is false. The message is printed as written, which is why it should say what is wrong and what to do.

The program asserts that double is an integral type. The compiler reports the failed assertion with the message and the line, and no binary is produced.

The check is the right place for a property of a type or a platform: a size, an alignment, a trait. Lesson 042 used it to verify a trait, and lesson 073 to check a date at compile time.

#include <type_traits> int main() { static_assert(std::is_integral_v<double>, "double is not an integer"); return 0; }

g++ -std=c++23 -Wall -Wextra static_assert.cpp -o static_assert

static_assert.cpp: In function 'int main()':
static_assert.cpp:5:24: error: static assertion failed: double is not an integer
5 | static_assert(std::is_integral_v<double>, "double is not an integer");
| ~~~~~^~~~~~~~~~~~~~~~~~~~~

Exercises

  1. Add an assert to a function that indexes a vector and check that a passing call prints nothing.
  2. Compile assert.cpp with -DNDEBUG and call divide(1, 0). What happens instead of the assertion?
  3. Write a static_assert that checks sizeof(int) == 4 and see whether it passes on this machine.

Questions

A new set of questions in a new order every time. Choose one answer for each, then check.

  1. What does assert(condition) do when the condition is false?

    Answer

    It prints the expression and aborts

    The macro reports the failed expression and the location, then aborts the process.

  2. When are assertions removed?

    Answer

    When NDEBUG is defined

    The macro expands to nothing when NDEBUG is defined, which release builds do.

  3. What does static_assert check?

    Answer

    A condition the compiler evaluates

    The assertion is part of compilation and costs nothing at run time.

  4. What does [[nodiscard]] do?

    Answer

    It warns when the result is ignored

    The attribute turns an ignored result into a compiler diagnostic.

  5. What does this program print? #include <cassert> #include <iostream> int main() { int value = 5; assert(value > 0); std::cout << value << '\n' << "all checks passed"; }

    Answer

    5 / all checks passed

    The condition holds, so the assertion is silent and the program prints its lines.

  6. What does this program print when built with -DNDEBUG? #include <cassert> #include <iostream> int main() { int value = -1; assert(value > 0); std::cout << "checked " << value; }

    Answer

    checked -1

    The macro compiles to nothing, so the negative value is never checked.

  7. What does the compiler warn about here? #include <iostream> [[nodiscard]] int compute() { return 42; } int main() { compute(); std::cout << "done\n"; }

    Answer

    ignoring return value of 'int compute()', declared with attribute 'nodiscard'

    The attribute makes the dropped result a warning, and the program still builds.

  8. What does the compiler report for this assertion? #include <type_traits> int main() { static_assert(std::is_integral_v<double>, "double is not an integer"); }

    Answer

    static assertion failed: double is not an integer

    The trait is false, and the message written for the assertion is printed.

  9. Why must a test suite run with assertions enabled?

    Answer

    An assert checks a real condition only without NDEBUG

    A release build removes the check, so the test would not exercise it.

  10. What should an assertion never do?

    Answer

    Change the program's state

    The assertion disappears in release builds, so its side effects would disappear too.

The code on this page was built with g++ 15.2.0 and GDB 16.3 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container. The programs whose section shows a compiler error were compiled until that error and were not run.

Article author: Arthur Isaev