Assertions & Contracts
An assertion is a promise a piece of code makes to itself — 'this condition will always be true when execution reaches this point, or something is badly broken' — and knowing when to make that promise, and when not to, is a habit that separates defensive code from brittle code.
Every function you write has implicit assumptions baked into it: divide(a, b) assumes b isn't zero, withdraw(amount) assumes amount is positive and no larger than the account balance, and a sorting function assumes its output really ends up sorted. Assertions make those assumptions explicit and checkable, turning a silent, implicit assumption into a statement the program actively verifies while you're developing and testing it. This page covers assert and <cassert> mechanically, the debug-versus-release distinction that trips people up, the precondition/postcondition/invariant vocabulary that gives assertions structure, and — just as importantly — where the boundary is between 'this is a job for assert' and 'this is a job for exceptions or std::expected instead.'
assert and <cassert>
assert is a macro from the <cassert> header. assert(condition); evaluates condition, and if it's false, the program prints a diagnostic message — including the failing expression, the file name, and the line number — and calls std::abort(), terminating immediately.
The && "divisor must not be zero" trick works because a non-empty string literal is always truthy in a boolean context, so it doesn't change whether the assertion passes or fails — it just attaches a human-readable message that shows up in the assertion failure output, which is far more useful during debugging than seeing only b != 0 printed with no context.
assert takes a plain runtime expression and is checked while the program is running. It's a completely different tool from static_assert, which checks a condition at compile time (static_assert(sizeof(int) == 4, "expects 4-byte int");) and produces a compile error rather than a runtime abort — reach for static_assert whenever the condition can actually be known at compile time.
#include <cassert>
int divide(int a, int b)
{
assert(b != 0 && "divisor must not be zero");
return a / b;
}
int main()
{
divide(10, 2); // fine
divide(10, 0); // assertion fails: program prints diagnostic and aborts
}Debug vs Release: What NDEBUG Does
The single most important fact about assert is that it's designed to disappear. If the macro NDEBUG is defined before <cassert> is included — which most build systems do automatically for release/optimized builds — every assert(...) expands to literally nothing, and the condition inside it isn't even evaluated.
In a debug build, calling risky(nullptr) trips the assertion and aborts with a clear message before the dereference ever happens. In a release build compiled with NDEBUG defined, the assert line vanishes entirely, so *ptr executes on a null pointer with no check at all — this is exactly why assert must never be the only thing standing between your program and a genuinely dangerous runtime error.
#include <cassert>
int risky(int *ptr)
{
assert(ptr != nullptr); // present in debug builds, GONE in NDEBUG builds
return *ptr; // undefined behavior in a release build if ptr IS null
}Preconditions, Postconditions, and Invariants
Assertions become far more useful once you organize them around three specific kinds of promises. A precondition is something that must be true when a function is called — assert at the top of the function body. A postcondition is something that must be true when a function returns — assert right before every return. An invariant is something that must stay true throughout an object's entire lifetime, checked at the boundaries of every operation that could disturb it.
The constructor's assert checks a precondition on its own argument. push's first assert checks a precondition on the caller (don't push onto a full stack), and its second assert checks a postcondition (the size actually stayed within capacity after pushing). check_invariant is called at the end of both mutating operations to confirm the class's core promise — never holding more elements than capacity_ — hasn't been broken by either operation.
#include <cassert>
#include <vector>
class BoundedStack
{
public:
explicit BoundedStack(int capacity) : capacity_(capacity)
{
assert(capacity_ > 0); // precondition on the constructor argument
}
void push(int value)
{
assert(!is_full()); // precondition: caller shouldn't push a full stack
data_.push_back(value);
assert(static_cast<int>(data_.size()) <= capacity_); // postcondition
check_invariant();
}
int pop()
{
assert(!data_.empty()); // precondition
int value = data_.back();
data_.pop_back();
check_invariant();
return value;
}
bool is_full() const { return static_cast<int>(data_.size()) == capacity_; }
private:
void check_invariant() const
{
assert(static_cast<int>(data_.size()) <= capacity_); // invariant: never exceeds capacity
}
int capacity_;
std::vector<int> data_;
};Defensive Programming with Assertions
Defensive programming means writing code that actively checks its own assumptions instead of trusting that everything upstream was done correctly. Assertions are the primary tool for this during development: they turn 'I assume this switch statement covers every enum value' into a checked fact, so that adding a new enum value later and forgetting to handle it somewhere produces an immediate, loud failure during testing instead of a silent bug discovered months later in production.
assert(false && ...) after the switch is a defensive net: if Direction ever gains a fifth value and this switch isn't updated to handle it, execution falls through to this line and aborts loudly during testing, instead of silently returning "unknown" (or worse, undefined behavior) unnoticed until much later.
#include <cassert>
enum class Direction { North, South, East, West };
const char *direction_name(Direction dir)
{
switch (dir)
{
case Direction::North: return "North";
case Direction::South: return "South";
case Direction::East: return "East";
case Direction::West: return "West";
}
assert(false && "unhandled Direction value"); // unreachable if every case is covered
return "unknown";
}Contract-Style Programming
The precondition/postcondition/invariant discipline above is often called 'design by contract' — a term from Eiffel, a language that built this idea directly into its syntax. C++ has never had first-class contract syntax in a shipped standard (a Contracts proposal has been through several rounds of standardization discussion for future C++ versions), so C++ codebases approximate contracts with conventions: assert calls for the checks themselves, and clear documentation naming each precondition and postcondition, often reinforced with small wrapper macros.
EXPECTS and ENSURES are just assert underneath, but naming them this way makes the function's contract self-documenting at the call site — a reader immediately knows which checks are preconditions on the caller and which are promises about the return value, without needing extra comments to explain what each assert is for.
#include <cassert>
#define EXPECTS(cond) assert((cond) && "precondition violated")
#define ENSURES(cond) assert((cond) && "postcondition violated")
int safe_index(const int *arr, int size, int index)
{
EXPECTS(arr != nullptr);
EXPECTS(index >= 0 && index < size);
int result = arr[index];
ENSURES(true); // no meaningful postcondition beyond returning a valid element here
return result;
}What Assertions Are Not For
The single most common misuse of assert is reaching for it to validate data that comes from outside the program's control — user input, file contents, network responses, command-line arguments. All of that is checked in release builds too, and assert is compiled away entirely under NDEBUG, so using it for user-facing validation means the check silently vanishes in production and invalid data flows straight through unchecked.
parse_port's assert only protects debug builds; a release build compiled with NDEBUG accepts any int at all, including negative numbers or values far outside the valid port range. parse_port_safe throws unconditionally in every build configuration, which is what user-facing (or file-facing, network-facing) input validation actually needs.
#include <cassert>
#include <stdexcept>
#include <string>
// WRONG: user input validated with assert
int parse_port(int port)
{
assert(port > 0 && port <= 65535); // vanishes in release; user could pass 99999
return port;
}
// RIGHT: user input validated with an error-handling mechanism that's always active
int parse_port_safe(int port)
{
if (port <= 0 || port > 65535)
throw std::invalid_argument("port must be between 1 and 65535");
return port;
}Best Practices
Write assertions for conditions you believe can never be false if every caller is using your code correctly — not for conditions that might legitimately be false because of bad external input. Keep assertion conditions free of side effects, since NDEBUG can strip them away entirely. Give assertions a descriptive message using the && "message" trick, since a bare failing expression is often much less informative than a sentence explaining what invariant was actually broken. Don't rely on assertions as your only line of defense in code that ships to users with NDEBUG defined — pair critical checks with an always-on fallback (an if that throws, logs, and returns a safe default) for anything where being wrong in production would be genuinely dangerous, like a security check or a bounds check on untrusted data.
What actually happens when NDEBUG is defined and an assert statement is compiled — does the condition inside it get evaluated?
No. When NDEBUG is defined, assert(condition) expands to essentially nothing, and the condition expression inside it is not evaluated at all. This matters most when the condition contains a function call with a needed side effect, because that call simply never runs in a build compiled with NDEBUG.
Distinguish a precondition, a postcondition, and an invariant using the BoundedStack example.
A precondition is a requirement checked at the start of an operation, like push asserting !is_full() before inserting. A postcondition is checked right before an operation returns, like push confirming the size stayed within capacity_ after inserting. An invariant is a property that must hold across the object's whole lifetime, checked at the end of any operation that could disturb it — here, that the stack's size never exceeds its declared capacity.
Why is it a mistake to validate a user-supplied file path or numeric input using assert instead of an exception or error code?
assert is stripped out entirely in release builds compiled with NDEBUG, which is the configuration most software actually ships in. Using assert as the only check on external input means that check silently disappears in production, letting malformed or malicious input flow through completely unchecked, whereas an exception or error-code check remains active in every build configuration.
What's the difference between static_assert and assert, and when would you use each?
static_assert checks a condition at compile time and, if it fails, produces a compile error — use it for conditions knowable purely from types and constants, like checking sizeof(int) == 4 or that a template parameter satisfies some compile-time property. assert checks a condition at runtime and aborts the program if it fails when it's actually reached during execution — use it for conditions that depend on runtime values, like a function argument or an object's current state.
Why does assert(false && "unhandled case") placed after a switch statement over an enum count as good defensive programming, rather than dead code?
It's unreachable as long as the switch handles every current enum value, but it becomes a loud, immediate failure the moment someone adds a new enum value and forgets to add a corresponding case — instead of that oversight silently falling through to whatever comes after the switch and causing a subtle bug discovered much later.
Write a function withdraw(double &balance, double amount) that asserts its preconditions (amount must be positive, balance must be sufficient) and asserts a postcondition (balance never becomes negative), then demonstrate a successful withdrawal.
The two asserts before the subtraction check preconditions on the caller (a positive amount, and enough balance to cover it), and the assert after the subtraction checks a postcondition on the function's own work (the resulting balance genuinely isn't negative). Withdrawing 40 from a balance of 100 passes every check and leaves balance at 60.
What does this program do differently when compiled normally versus compiled with -DNDEBUG, and why?
Compiled normally, each assert(increment_and_check()) call actually runs increment_and_check(), incrementing counter both times (both calls return true since counter stays below 5), so the program prints 2. Compiled with -DNDEBUG, both assert statements are compiled away entirely, so increment_and_check() is never called at all, counter stays at its initial value of 0, and the program prints 0 instead — a clear illustration of why side effects must never live inside an assert condition.
Write a Rectangle class with width and height, using an invariant check (both dimensions must be positive) verified after construction and after every setter, and show it catching a bug when an invalid setter call is attempted.
check_invariant runs after the constructor and after both setters, so any operation that would leave the rectangle with a non-positive dimension is caught immediately at the point it happens, rather than surfacing later as a wrong area calculation somewhere else in the program. Uncommenting r.set_width(-1.0) would trigger the width_ > 0.0 assertion and abort with a message naming exactly which invariant was violated.
#include <cassert>
#include <iostream>
int counter = 0;
bool increment_and_check()
{
counter++;
return counter < 5;
}
int main()
{
assert(increment_and_check());
assert(increment_and_check());
std::cout << counter << '\n';
}