C++ Part I · Module 22 · Error Handling and Diagnostics
assert and Checking Assumptions
Chapter 22.1 sorted failures into three kinds and this is the third, which is the one that is not a failure. A violated precondition is a bug: the program asked for something impossible, and no run-time handling can be correct because the code that would do the handling is part of the program whose assumptions have just been shown false. assert is how you write such an assumption down so that a violation stops execution at the point of the mistake, with a file and a line, rather than producing a plausible wrong answer somewhere downstream. The frozen purpose asks for two things and the second is the harder one: knowing what assert must never be used for. The answer follows from a single measured fact. An assert is not present in a release build — the entire expression, including anything it calls, is removed by the preprocessor when NDEBUG is defined. The chapter measures that with a side effect inside an assert, which ran once in the debug build and zero times in the release build. That one fact is simultaneously the reason an assert is free in production and the reason it can never be the thing that validates a file, a device response or a user's input, because those conditions still exist when the check does not. The chapter also covers the compile-time sibling, which is strictly better where it applies and which found a real bug earlier in this curriculum.
Intermediate18 min readC++Part IFundamentalsError Handling
Part I · Module 22 · Chapter 22.8 · assert and Checking Assumptions
1. What It Is For
std::uint32_t fieldWidth(unsigned pos, unsigned width)
{
assert(pos < 32 && "bit position must be below 32");
assert(width >= 1 && width <= 32 - pos && "field must fit in the register");
return width;
}assert(expr) evaluates expr and, if it is false, prints a diagnostic naming the file, the line and the expression, then calls std::abort.
Three things that follow from "then calls abort", which is the design rather than a limitation.
It does not throw. There is no handler, because there is nothing to recover to — Chapter 22.1 §1's argument. An assert is a statement that this situation is impossible, so there is no code for when it happens.
It stops at the point of the mistake. That is the whole value. A wrong width that is asserted fails here, with a line number; a wrong width that is not asserted produces a mask with the wrong bits, which produces a wrong register write, which produces a wrong answer somewhere unrelated, hours later.
It names the expression. The && "message" idiom in the code above is a trick worth knowing: a string literal is always true, so expr && "text" has the same truth value as expr, and the printed expression now includes the explanation.
The two asserts above are preconditions — things the caller must satisfy. §4 covers the other kinds.
2. It Is Not There in Release
The single most important fact about assert, and it is measurable.
static int sideEffectCalls = 0;
static bool advance() { ++sideEffectCalls; return true; }
assert(advance());
std::printf("the side effect ran %d time(s)\n", sideEffectCalls);BUILD: NDEBUG not defined -- asserts are ACTIVE
after `assert(advance());` the side effect ran 1 time(s)
BUILD: NDEBUG defined -- asserts are COMPILED OUT
after `assert(advance());` the side effect ran 0 time(s)One call, then zero. assert is a macro, and when NDEBUG is defined it expands to nothing — so the expression, the function call inside it, and any side effect it would have had are all simply absent from the translation unit.
Two conclusions, and they are the two halves of this chapter.
It is free in production, which is why you can afford to assert liberally in internal code. There is no run-time cost to an assertion that is not compiled.
It must contain no side effect, because the side effect disappears with it. assert(advance()); is a bug whose symptom is that the program behaves differently in release — the hardest category of bug to diagnose, because the debug build is correct.
And a secondary effect worth seeing, because it is a practical friction:
build exit=0 warnings=2
warning: unused parameter 'pos'
warning: unused function 'advance'The release build warns, because the vanished asserts were the only users of pos and of advance. That is not a defect in the code; it is the mechanism being visible. The usual responses are (void)pos; next to the assert, a [[maybe_unused]] attribute on the parameter, or wrapping the helper in #ifndef NDEBUG.
3. What It Must Never Be Used For
The frozen purpose asks for this explicitly, and §2 is the reason behind every item.
Validating input from outside the program. A file's contents, a network message, a command line, a user's entry. Those can be wrong in a program that has no bugs, and the check must exist in the shipped binary.
Checking that a resource acquisition succeeded. A file that may not exist, a device that may not respond, an allocation that may fail. Chapter 22.1 §1's exceptional category — these want a throw or a returned status.
Anything with a side effect. Measured: it vanishes.
Anything expensive you intend to keep. An assert that verifies a sorted invariant over a million elements is fine as a debug check and is not a production guarantee. If you need the check in release, write an if.
As the only check on a security boundary. An assertion removed in release is not a defence.
The test that decides it, from Chapter 22.1 §6, is one question: can this condition occur in a program that has no bugs? If yes, it is not an assert. A file that might not exist: yes. An index beyond the end of a container the caller just sized: no.
4. What It Is Good For
Four categories, and they are the mirror of §3.
Preconditions. What the caller must guarantee — §1's example. This is the most common and most valuable use, because it moves the diagnosis from the callee's wrong output to the caller's wrong argument.
Postconditions. What you guarantee on the way out:
auto result = compact(v);
assert(result.size() <= v.size() && "compaction cannot grow the range");Class invariants. Chapter 22.5's measurement is the motivating case — a count that must equal a container's size. A private assertInvariant() called at the end of every mutating member function would have caught the count=2 names=1 state that chapter measured:
void assertInvariant() const { assert(count == (int)names.size()); }Unreachable code. A default: in a switch over an enum where every value is handled, or the end of a function that always returns earlier:
default: assert(false && "unhandled Status value"); return "?";C++23 adds std::unreachable() for this, which also tells the optimiser.
The common thread: all four are statements about your own program's consistency, never about the world.
5. The Compile-Time Sibling
static_assert checks a condition at compile time, costs nothing at run time, and — critically — is present in every build configuration.
static_assert(sizeof(std::uint32_t) == 4, "this code assumes a 32-bit uint32_t");static_assert checked two assumptions with no run-time cost at all,
in BOTH build configurationsWhere it applies it is strictly better than assert: it fires at build time rather than on a run that happens to hit the case, it cannot be compiled out, and it costs nothing.
What it can check is anything that is a constant expression — Chapter 21.2's subject. Type properties, sizes, alignments, template arguments, and the results of constexpr functions.
And this curriculum has a measured instance of its value. Chapter 21.2 §3 applied static_assert to the frozen register-mask example at every width and the build failed at width == 32, naming "shift count 32 >= width of type 'unsigned int' (32 bits)" — real undefined behaviour in a reference example, found at build time, because a constant expression may not contain undefined behaviour. The three assertions at easy widths passed and told me nothing; the fourth found the bug.
The lesson transfers directly: assert the boundaries, not the middle. Zero, one, the maximum, the maximum plus one.
Chapter 18.10 §6 covers static_assert inside a template, where it is the pre-C++20 way to improve a diagnostic — and Chapter 21.8 §7 measured why a concept is better still, since a static_assert fires but the body instantiates anyway and produces a second error.
6. Making assert Better
Three improvements that cost little.
The && "message" idiom from §1. The printed expression then explains itself.
A project macro that survives release, for the checks you genuinely want in production:
#define CHECK(cond, msg) \
do { if (!(cond)) { logFatal(__FILE__, __LINE__, #cond, msg); std::abort(); } } while (0)That is not an assert and should not be called one — it is a real run-time check that happens to abort. Having both, with different names, is the right arrangement: assert for impossible, CHECK for must-not-happen-but-could.
Library hardening, which is the measured gap-filler for this module. Chapter 21.5 §6 found that dereferencing an empty std::optional was caught by nothing except _LIBCPP_HARDENING_MODE, and Chapter 20.3 §5 found the erase-in-a-loop bug caught by nothing at all. Those are library preconditions, and the library's own assertions are off by default:
-D_LIBCPP_HARDENING_MODE=_LIBCPP_HARDENING_MODE_DEBUGTurning it on in the test configuration is the single highest-value line in this chapter, because it activates thousands of assertions somebody has already written.
7. Where to Put Them
A short policy, because assertion discipline is mostly about placement.
At the top of a function, for preconditions. Before any work, so the diagnosis points at the caller.
At the end of a mutating member, for the invariant. Chapter 22.5's broken count would have been caught here.
Not in a hot inner loop, even though they are free in release — the debug build becomes unusable, and an unusable debug build is one nobody runs.
Not duplicating a check the caller already made. An assert on a value you just validated with an if is noise.
One condition per assert. Two conditions in one gives a diagnostic that does not say which failed.
And the asymmetry worth stating: assert liberally in internal code, and not at all in a public API's input validation. Internal code's callers are you, so a violated precondition is your bug. A public API's callers are other people, so a violated precondition is input — and §3 applies.
8. The Three Mechanisms Side by Side
This chapter completes Chapter 22.1's taxonomy, so the comparison belongs here.
assert | static_assert | an exception | a returned status | |
|---|---|---|---|---|
| when checked | run time, debug only (measured) | compile time, always (measured) | run time, always | run time, always |
| present in release | no (measured 0 calls) | n/a — already checked | yes | yes |
| run-time cost | zero in release | zero always | ~3,400 ns when thrown (22.1 §4) | ~0.3 ns (22.1 §4) |
| recoverable | no — abort | n/a | yes | yes |
| for | your program's own consistency | a compile-time fact | an exceptional failure | an expected failure |
| never for | input, resources, side effects (measured) | anything run-time | a frequent failure | something unhandleable |
| names the location | file, line, expression | file, line, message | only what you put in what() | nothing automatic |
9. What to Actually Do
- Assert preconditions at the top of internal functions. The diagnosis then points at the caller.
- Assert the class invariant at the end of every mutating member. Chapter 22.5's broken
countis the motivating case. - Never put a side effect in an
assert. Measured: 1 call in debug, 0 in release. - Never assert anything that can happen in a correct program — input, files, devices, allocations.
- Use
&& "message"so the printed expression explains itself. - Prefer
static_assertwherever the condition is a constant expression. Always present, zero cost. - Assert the boundaries, not the middle. Chapter 21.2 §3's three passing assertions found nothing; the fourth found real UB.
- Turn on
_LIBCPP_HARDENING_MODEin the test build. It activates assertions somebody else already wrote, and Chapter 21.5 §6 measured it as the only tool that caught an empty-optionaldereference. - Have a separate
CHECKmacro for conditions you want verified in release, and do not call it an assert. - One condition per assertion.
- Mark operands
[[maybe_unused]]where the release build would otherwise warn. - Keep them out of hot loops, so the debug build stays usable.
10. Mistakes Worth Making Once
A side effect inside an assert. Measured: the release build behaves differently and the debug build is correct, which is the worst diagnostic position available.
assert on a file existing. Not there in release, and the condition is real.
assert as input validation on a public API. The check is removed exactly where untrusted callers exist.
Two conditions in one assert. The diagnostic does not say which failed.
An expensive assert in a hot loop. The debug build becomes too slow to run, so nobody runs it, so none of your assertions fire.
Forgetting NDEBUG orphans the operands. Measured two warnings; [[maybe_unused]] or (void)x is the fix.
Using assert where static_assert would work. A run-time check for something the compiler could have proved.
Asserting only the comfortable cases. Chapter 21.2 §3: the boundary assertion is the one that finds the bug.
11. Interview Relevance
The frozen curriculum marks this chapter High, and the "must never" question is the discriminating one.
"What is assert for?" Stating an assumption about your own program so a violation stops at the point of the mistake. The strong answer adds that it aborts rather than throwing, because there is nothing to recover to.
"What must it never be used for?" Input validation, resource checks, or anything with a side effect — because it is not present in a release build. The measurement, 1 call then 0, is the cleanest way to make the point.
"What is the test for whether something should be an assert?" Can this happen in a program with no bugs? If yes, it is not an assert.
"assert or static_assert?" static_assert wherever the condition is a constant expression: always present, zero cost, checked at build time. And it found real undefined behaviour in a register-mask helper in this curriculum.
"What happens to the operands in a release build?" They may become unused and warn — measured two warnings — which is [[maybe_unused]]'s job.
"How would you make a library's internal preconditions fire?" _LIBCPP_HARDENING_MODE in the test configuration. Knowing it exists is unusual and it was the only tool that caught an empty-optional dereference.
12. What You Should Be Able to Say Now
- That
assertaborts rather than throwing, because a violated precondition has nothing to recover to. - That its value is stopping at the point of the mistake, with a file, a line and the expression.
- That it is compiled out under
NDEBUG— measured, a side effect inside one ran 1 time in debug and 0 in release. - That this one fact is both why it is free in production and why it can never validate input, resources or anything that occurs in a correct program.
- That the release build may warn about operands the vanished asserts orphaned — measured two warnings.
- The four good uses: preconditions, postconditions, class invariants, unreachable code.
- That
static_assertis strictly better where it applies: always present, zero cost, build-time. - That asserting the boundaries is what finds bugs — Chapter 21.2 §3 found real UB at the fourth assertion.
- That
_LIBCPP_HARDENING_MODEactivates the library's own assertions and was the only tool that caught Chapter 21.5 §6's defect.
13. Exercises
Exercise 1 — the side effect. Reproduce §2 in both configurations. Then find a side effect subtle enough that a reviewer would miss it — an iterator increment, a counter, a pop().
Exercise 2 — the orphaned operand. Build a function whose only use of a parameter is inside an assert, compile with NDEBUG, and fix the warning three ways: (void), [[maybe_unused]], and #ifndef NDEBUG.
Exercise 3 — the invariant. Take Chapter 22.5's Config, add a private assertInvariant(), call it at the end of every mutating member, and show that the no-guarantee version now aborts on the failure path.
Exercise 4 — sort ten checks. List ten conditions from your own code and decide for each: assert, static_assert, throw, or returned status. Record the question that settled each one.
Exercise 5 — boundaries. Pick a constexpr function of your own and static_assert it at zero, one, the maximum and the maximum plus one. Report anything that fails.
Exercise 6 — hardening. Build your test suite with _LIBCPP_HARDENING_MODE set and record how many new failures appear. Investigate each.
Exercise 7 — CHECK. Implement §6's macro, use it where a condition must be verified in release, and write the two-sentence note explaining to a reviewer why it is not an assert.
Exercise 8 — the wrong tool. Find an assert in real code that is validating something external. Convert it and explain what would have happened in release.
14. Summary
assertstates an assumption about your own program and aborts on violation, naming the file, line and expression — it does not throw, because there is nothing to recover to.- Its value is stopping at the point of the mistake rather than producing a plausible wrong answer downstream.
- Measured: it is compiled out under
NDEBUG. A side effect inside one ran 1 time in the debug build and 0 times in the release build. - That one fact is both halves of the chapter: it is free in production, and it can never validate input, a file, a device, an allocation, or anything that occurs in a correct program.
- Measured: the release build emitted 2 warnings —
unused parameterandunused function— because the vanished asserts were their only users.[[maybe_unused]]is the fix. - The test is one question: can this happen in a program with no bugs? If yes, it is not an assert.
- Four good uses: preconditions, postconditions, class invariants, and unreachable code.
static_assertis strictly better where it applies — present in every configuration, zero cost, checked at build time — and Chapter 21.2 §3 used it to find real undefined behaviour in a register-mask helper.- Assert the boundaries, not the middle. Three passing assertions found nothing; the fourth found the bug.
_LIBCPP_HARDENING_MODEin the test build activates the library's own preconditions, and Chapter 21.5 §6 measured it as the only tool catching an empty-optionaldereference.
Every mechanism in this module assumes somebody is watching when the failure happens. Chapter 22.9 drops that assumption, which is the realistic case for the work this curriculum is aimed at: a model that ran for four hours overnight and failed once, with nobody at the terminal. The question is then not how the failure was signalled but what it left behind — and the chapter measures the two things that decide whether a log is usable, including a formatting mistake that cost 136 times the correct version for lines that were being discarded anyway.
Standards & specifications
- Governing standard
- ISO/IEC 14882 (the C++ language standard)(opens ISO/IEC (JTC 1/SC 22/WG 21) in a new tab)
Defines the C++ language and its standard library — the type system and templates, the object model and object lifetime, initialisation and overload resolution, the memory model and the containers, algorithms and utilities the library provides. Like C's standard it is a FAMILY of editions (C++98/03, C++11, C++14, C++17, C++20, C++23) and the editions are NOT interchangeable: a feature this curriculum relies on may be unavailable, differently specified or merely proposed in another edition, so each lesson names the edition its claim depends on. It does not define any foreign-function interface to a hardware description language, nor what a compiler actually emits, nor platform behaviour on real silicon — those are the implementation's and the device's contracts. Working drafts published by WG21 are freely available and are what most engineers read day to day; a draft is not the published standard.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the C++ Programming curriculum.
