Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

C++  Module 13 – Error handling

Assertions, preconditions and [[assume]]

Check your own assumptions with assert and static_assert, validate preconditions at API boundaries, and see what a false [[assume]] does to C++ results.

  • Intermediate
  • 20 minutes
  • Examples run with Clang 21.0.0, GCC 16.2.0 and Python 3.14.8
  • By MySmartCoPilot
Free core, Pro depth: 8 of 20 minutes free
Free: A failed assert names its condition and stops the program · NDEBUG removes an assert, and the work inside it · Checks while compiling cost nothing at run time · 2 more sections. With a pass: 2 in-depth parts and 3 interview answers, about 12 minutes.

What you will learn

  • Check internal assumptions with assert and static_assert
  • Validate preconditions at API boundaries in every build
  • Explain why [[assume]] is dangerous, with the code it changes

Before you start

mark_at(marks, 3) asks a vector of three marks for a fourth, and an assert stops the program before the read, with exit status 134. assertion: A check of something the programmer believes must be true, such as assert(i < v.size()): a false one prints the condition, the file and the line, then stops the program. assert disappears from builds that define NDEBUG. state what a programmer believes must be true at a point in the code, and stop the program when it is not. assert(condition) checks while the program runs and disappears completely from builds that define NDEBUG: A macro that most release builds define. Where it is defined before <cassert> is included, assert(anything) expands to ((void)0), so neither the check nor any work inside its condition is left in the program., as most release builds do; static_assert(condition) checks while the program compiles and costs nothing at run time.

precondition: What a function requires from its callers before it is called, such as an index inside the array. A public function checks its preconditions in every build, because code it does not control can break them. are what a function requires from its callers, such as an index inside the array. Check preconditions at public boundaries, where code you do not control can break them, in every build, by throwing or returning an error, and use assertions for the internal assumptions that only a bug in your own code can break. [[assume]]: A C++23 attribute, [[assume(condition)]], that tells the optimiser to treat a condition as true without checking it. If the condition is false while the program runs, the behaviour is undefined, and nothing reports it., new in C++23, tells the optimiser to treat a condition as true without checking it at all.

A failed assert names its condition and stops the program

mark_at asserts that the index is inside the vector. The string after && is always true, so it changes nothing but the message:

bad_index.cpp C++ · bad_index.cpp
#include <cassert>
#include <cstddef>
#include <print>
#include <vector>

double mark_at(const std::vector<double>& marks, std::size_t i) {
    assert(i < marks.size() && "mark_at: index out of range");
    return marks[i];
}

int main() {
    std::vector<double> marks{72.5, 64.0, 91.5};
    std::println("{}", mark_at(marks, 2));
    std::println("{}", mark_at(marks, 3));
}
One good call and one bad call, with both compilers Python · assert_session.py
from session import run

# A failed assert with both compilers: on macOS both use the C library's assert, so the message is the same.
run("""
clang++ -std=c++23 -Wall -Wextra bad_index.cpp -o bad_index && ./bad_index
g++-16 -std=c++23 -Wall -Wextra bad_index.cpp -o bad_index-gcc && ./bad_index-gcc
""")

Output

$ clang++ -std=c++23 -Wall -Wextra bad_index.cpp -o bad_index && ./bad_index
91.5
Assertion failed: (i < marks.size() && "mark_at: index out of range"), function mark_at, file bad_index.cpp, line 7.
Abort trap: 6
[exit status 134]
$ g++-16 -std=c++23 -Wall -Wextra bad_index.cpp -o bad_index-gcc && ./bad_index-gcc
91.5
Assertion failed: (i < marks.size() && "mark_at: index out of range"), function mark_at, file bad_index.cpp, line 7.
Abort trap: 6
[exit status 134]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 assert_session.py

Step through the two calls, the good index first and then the one past the end:

Interactive · message by message Two calls of mark_at, one with a bad index Step through bad_index.cpp. The assert sits on the first line of mark_at; watch what it does with each index.
Two calls of mark_at, one with a bad index, step 6 of 6. Messages so far: main to mark_at: mark_at(marks, 2); mark_at to main: returns 91.5; main to mark_at: mark_at(marks, 3); mark_at to the C library: condition, file, line 7.mainmarks: 72.5, 64.0, 91.5mark_atassert(i < marks.size() …)the C libraryabort: exit status 134mark_at(marks, 2)returns 91.5mark_at(marks, 3)condition, file, line 7

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. mark_at(marks, 2). main asks for the mark at index 2. The vector holds three marks, so i < marks.size() is 2 < 3, true: the assert does nothing.
  2. 91.5. mark_at returns marks[2], and main prints 91.5.
  3. mark_at(marks, 3). Now main asks for index 3, one past the last mark.
  4. The belief breaks. 3 < 3 is false, so the assert fails before marks[3] is ever read. The string after && is always true; it only makes the message say what the check is for.
  5. The message. assert prints the condition as written, the function, the file and the line: Assertion failed: (i < marks.size() && "mark_at: index out of range"), function mark_at, file bad_index.cpp, line 7.
  6. std::abort. Then it calls std::abort, which raises SIGABRT, signal 6: the shell prints Abort trap: 6 and reports exit status 134. The bad value never travels further.

The first call passes and prints 91.5. The second fails, and assert prints the condition as written, the function, the file and the line, then calls std::abort, so the exit status is 134, 128 plus the number of SIGABRT. The two compilers print the same message because on macOS both use the C library’s assert. A failed assertion means a bug: the program stops at the line where its belief broke, before a wrong value travels further.

Checkpoint

Check: What a failed assert does

  1. Question 1 of 2 What does a failed assert print before it stops the program, with both compilers on this Mac?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The condition as written, the function, the file and the line

    assert prints the condition as written, the function, the file and the line, then calls std::abort. The string after && appears in the message because it is part of the condition.

  2. Question 2 of 2 ./bad_index stops at the failed assert. What exit status does the shell report?

    Type a number.

    A hint

    assert calls std::abort, which raises SIGABRT, signal 6.

    Show the answer: Question 2 of 2

    Answer: 134

    std::abort raises SIGABRT, signal 6, and a shell reports a process ended by a signal as 128 plus its number: 128 + 6 = 134.

NDEBUG removes an assert, and the work inside it

load_settings fills a map and reports whether it could. Calling it inside assert looks like a careful check:

vanishing.cpp C++ · vanishing.cpp
#include <cassert>
#include <map>
#include <print>
#include <string>

// Fills `out` and reports whether it could.
bool load_settings(std::map<std::string, int>& out) {
    out = {{"port", 8080}, {"retries", 3}, {"workers", 8}};
    return true;
}

int main() {
    std::map<std::string, int> settings;
    assert(load_settings(settings));  // the call is inside the assert, so NDEBUG removes it
    std::println("settings loaded: {}", settings.size());
}
A debug build and an NDEBUG build, with both compilers Python · vanishing_session.py
from session import run

# The same program as a debug build and as a release build with NDEBUG, with both compilers.
run("""
clang++ -std=c++23 -Wall -Wextra vanishing.cpp -o vanishing && ./vanishing
clang++ -std=c++23 -Wall -Wextra -DNDEBUG vanishing.cpp -o vanishing-release && ./vanishing-release
g++-16 -std=c++23 -Wall -Wextra vanishing.cpp -o vanishing-gcc && ./vanishing-gcc
g++-16 -std=c++23 -Wall -Wextra -DNDEBUG vanishing.cpp -o vanishing-gcc-release && ./vanishing-gcc-release
""")

Output

$ clang++ -std=c++23 -Wall -Wextra vanishing.cpp -o vanishing && ./vanishing
settings loaded: 3
$ clang++ -std=c++23 -Wall -Wextra -DNDEBUG vanishing.cpp -o vanishing-release && ./vanishing-release
settings loaded: 0
$ g++-16 -std=c++23 -Wall -Wextra vanishing.cpp -o vanishing-gcc && ./vanishing-gcc
settings loaded: 3
$ g++-16 -std=c++23 -Wall -Wextra -DNDEBUG vanishing.cpp -o vanishing-gcc-release && ./vanishing-gcc-release
settings loaded: 0

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 vanishing_session.py

Step through the line in a debug build and in a build with NDEBUG:

Interactive · step by step The same line in a debug build and an NDEBUG build Step through main of vanishing.cpp in both builds. The call to load_settings sits inside the assert; watch whether it runs.
The same line in a debug build and an NDEBUG build, step 6 of 6. Showing debug build, -DNDEBUG build (loaded = load_settings(…)), settings (port, retries, workers), settings, printed (settings loaded: 3), printed (settings loaded: 0). In focus: -DNDEBUG build.fillsdebug buildassert(load_settings(…))-DNDEBUG buildloaded = load_settings(…)settingsport, retries, workerssettingsan empty mapprintedsettings loaded: 3printedsettings loaded: 0

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. One line, two builds. Both builds compile the same main: an empty map, then assert(load_settings(settings)), then a line that prints the size of the map.
  2. Debug: the call runs. Without NDEBUG, assert evaluates its condition. load_settings runs, fills the map with port, retries and workers, and returns true, so the assert passes.
  3. settings loaded: 3. The debug builds of both compilers print settings loaded: 3.
  4. NDEBUG: the line is gone. With NDEBUG defined where <cassert> is included, assert(anything) becomes ((void)0). Its argument is never evaluated, so the call to load_settings vanishes with it.
  5. settings loaded: 0. The map stays empty, and both release builds print settings loaded: 0, without an error or a warning.
  6. The fix. Do the work on its own line and assert on the result: bool loaded = load_settings(settings); assert(loaded);, with [[maybe_unused]] on loaded so that release builds do not warn.

The debug builds load 3 settings; the release builds load none. When NDEBUG is defined where <cassert> is included, assert(anything) becomes ((void)0), so its argument is never evaluated, and the call vanished with it. Both compilers built the release programs without a warning, even with -Wall -Wextra, so nothing in the build output points at the missing call.

Work inside an assert

assert(load_settings(settings)); loads nothing in a release build. Do the work on its own line and assert on its result: bool loaded = load_settings(settings); assert(loaded);, with [[maybe_unused]] on loaded so that release builds do not warn about a variable they never read.

Checkpoint

Check: NDEBUG and work inside an assert

  1. Question 1 of 2 This program is built with -DNDEBUG. What happens to the call to connect()?

    Read the code, then choose one answer.

    #include <cassert>
    
    bool connect();
    
    int main() {
        assert(connect());
        // ... use the connection ...
    }
    Show the answer: Question 1 of 2

    Answer: It never runs, because NDEBUG turns the whole assert, argument included, into nothing

    With NDEBUG defined where <cassert> is included, assert expands to ((void)0), so its argument is never evaluated. Make the call on its own line, keep the result in a variable, and assert on the variable.

  2. Question 2 of 2 Which code loads the settings in every build and still checks the result in debug builds?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: bool loaded = load_settings(settings); assert(loaded);

    Never put work inside an assert: do the work on its own line and assert on its result, so that removing the assert removes nothing else.

Checks while compiling cost nothing at run time

A message header must be exactly 8 bytes to match a wire format, and static_assert makes the compiler check it:

wire.cpp C++ · wire.cpp
#include <cstdint>

// The header of a message on the wire: exactly 8 bytes, in this order.
struct Header {
    std::uint16_t type;
    std::uint32_t length;
    std::uint16_t flags;
};

static_assert(sizeof(Header) == 8, "Header must match the 8-byte wire format");

int main() {}
A static_assert that fails, with both compilers Python · static_assert_session.py
from session import run

# A static_assert that fails: the program does not compile with either compiler.
run("""
clang++ -std=c++23 -c wire.cpp -o wire.o
g++-16 -std=c++23 -c wire.cpp -o wire-gcc.o
""")

Output

$ clang++ -std=c++23 -c wire.cpp -o wire.o
wire.cpp:10:15: error: static assertion failed due to requirement 'sizeof(Header) == 8': Header must match the 8-byte wire format
   10 | static_assert(sizeof(Header) == 8, "Header must match the 8-byte wire format");
      |               ^~~~~~~~~~~~~~~~~~~
wire.cpp:10:30: note: expression evaluates to '12 == 8'
   10 | static_assert(sizeof(Header) == 8, "Header must match the 8-byte wire format");
      |               ~~~~~~~~~~~~~~~^~~~
1 error generated.
[exit status 1]
$ g++-16 -std=c++23 -c wire.cpp -o wire-gcc.o
wire.cpp:10:30: error: static assertion failed: Header must match the 8-byte wire format
   10 | static_assert(sizeof(Header) == 8, "Header must match the 8-byte wire format");
      |               ~~~~~~~~~~~~~~~^~~~
  • the comparison reduces to '(12 == 8)'
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 static_assert_session.py

Neither compiler builds the program. Both show the values they compared, 12 against 8: the std::uint32_t member must start at a multiple of 4, so 2 bytes of padding follow type, and 2 more follow flags, as the lesson on memory layout, alignment and padding explains. Step through the bytes, as declared and with length moved to the front:

Interactive · step by step Why Header takes 12 bytes, and how to make it 8 Step through the bytes of Header as declared, then with length moved to the front. The static_assert compares the total with 8.
Why Header takes 12 bytes, and how to make it 8, step 6 of 6. Showing type, padding, length, flags, padding, length, type, flags, length first. In focus: length, type, flags, length first.typebytes 0 and 1paddingbytes 2 and 3lengthbytes 4 to 7flagsbytes 8 and 9paddingbytes 10 and 11lengthbytes 0 to 3typebytes 4 and 5flagsbytes 6 and 7length first8 bytes: passes

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. type. type is a std::uint16_t, two bytes, so it takes bytes 0 and 1.
  2. Padding. length is a std::uint32_t, which must start at a multiple of 4. The next free byte is 2, so 2 bytes of padding follow type.
  3. length. length takes bytes 4 to 7.
  4. flags. flags, another std::uint16_t, takes bytes 8 and 9.
  5. 2 more bytes. 2 more bytes of padding follow flags, so the struct takes 12 bytes. static_assert(sizeof(Header) == 8) is false, and both compilers stop the build, showing 12 == 8.
  6. length first. With length moved to the front, every member starts at a multiple of its own size without any padding: bytes 0 to 3, 4 and 5, 6 and 7. The header is 8 bytes and the static_assert passes.

Moving length to the front makes the header 8 bytes. Rule P.5 of the C++ Core Guidelines prefers compile-time checks like this one to run-time checks wherever the condition is known while compiling.

Checkpoint

Check: static_assert while compiling

  1. Question 1 of 2 What does static_assert(sizeof(Header) == 8) cost when the program runs?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Nothing, because it is checked while the program is compiled

    static_assert is evaluated by the compiler. If the condition is false, the program does not compile; if it is true, nothing of it remains in the program.

  2. Question 2 of 2 Header has a std::uint16_t type, a std::uint32_t length and a std::uint16_t flags. Which member orders make sizeof(Header) equal 8 on these platforms?

    Choose every answer that is right.

    A hint

    A std::uint32_t must start at a multiple of 4.

    Show the answer: Question 2 of 2

    Answer:

    • type, flags, length
    • length, type, flags

    Padding appears when a member would start at a position that is not a multiple of its own size. Putting the two 16-bit members next to each other, before or after length, removes all four bytes of padding.

Who can break a condition decides how to check it

Four kinds of check differ in when, if ever, they run:

static_assert checks while compiling, assert in debug builds, boundary checks in every build, and [[assume]] never.static_assert(condition)checked while compiling:the program does not build if falseassert(condition)checked while running, in debug builds;NDEBUG removes it, condition and allif (!condition) throw / return an error,or CHECK(condition)checked in every build, at API boundaries[[assume(condition)]]never checked: the optimiser believes it,and a false condition is undefined behaviour

When each kind of check runs

Text description of the diagram

The diagram is a column of four boxes, from the earliest check to none at all.

  1. static_assert(condition) is checked while the program compiles: if the condition is false, the program does not build.
  2. assert(condition) is checked while the program runs, but only in debug builds: defining NDEBUG removes it, the condition included.
  3. A check at an API boundary, such as an if that throws or returns an error, or a CHECK macro, runs in every build.
  4. [[assume(condition)]] is never checked: the optimiser believes the condition, and if it is false the behaviour is undefined. This box has a dashed border.

Preconditions of a public function belong to the third row, because its callers include code you do not control, such as a file a user wrote. The row’s CHECK is a macro that stops the program with a message in every build, an assert that nothing removes. The function’s own internal helpers can rely on assertions, because a broken condition there is a bug in the same code. The standard library follows the same split: its hardened standard library: A standard library that checks the preconditions of calls such as operator[] and front() while the program runs, and stops it when one fails, as libc++'s hardening modes and libstdc++ with _GLIBCXX_ASSERTIONS do. C++26 makes such hardened implementations part of the standard. More in “std::vector: the container to reach for first” check its own preconditions, such as the index given to operator[], in release builds too.

[[assume]] belongs to no row of checks at all. The optimiser treats its condition as a fact and may remove code that would only matter if the condition were false; if the condition is false at run time, the behaviour is undefined behaviour: What a program does when it breaks a rule for which the C++ standard sets no requirements, such as reading past the end of an array. The compiler may assume it never happens, so the result can change with the compiler and the optimisation level. More in “Object lifetime and undefined behaviour in C++”, and nothing reports it.

Key idea

Ask who can break the condition: what users and other code supply is checked in every build, your own internal assumptions get an assert, and [[assume]] checks nothing at all.

Checkpoint

Check: Who can break a condition, and the check it gets

  1. Question 1 of 2 A public function receives a page number typed by a user, and the page must exist. How should the function check it?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Test it in every build and report the error to the caller, for example by throwing or returning an error

    Input from users is not a bug in the program, so the check must stay in release builds and give the caller a way to respond. assert is for conditions that only a bug in your own code can break.

  2. Question 2 of 2 Which of the four kinds of check never runs, in any build?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: [[assume(condition)]]

    static_assert runs while compiling, assert while running in debug builds, a boundary check in every build, and [[assume]] never: a false assumption is undefined behaviour, reported by nothing.

Practise: put each check where it belongs

Nine questions on the four kinds of check, the idea you have answered least well first, each choice explained.

Practice that adapts to you

Practise this lesson

9 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 9 When does assert(condition) check its condition?

    Choose one answer.

    Show the answer: Question 1 of 9

    Answer: While the program runs, in builds that do not define NDEBUG

    assert checks while the program runs and disappears completely from builds that define NDEBUG; static_assert checks while the program compiles.

  2. Question 2 of 9 An assert fails in your test run. What does that tell you?

    Choose one answer.

    Show the answer: Question 2 of 9

    Answer: Your own code has a bug, because a belief about it turned out false

    A failed assertion means a bug: the program stops at the line where its belief broke, before a wrong value travels further, and the message names the condition and the line.

  3. Question 3 of 9 With NDEBUG defined where <cassert> is included, what does assert(x > 0) become?

    Choose one answer.

    Show the answer: Question 3 of 9

    Answer: ((void)0)

    NDEBUG turns every assert into ((void)0), so neither the check nor anything inside its condition is left in the program.

  4. Question 4 of 9 A debug build writes run.log, and the release build never creates it. The code opens the file with assert(log.open("run.log"));. Why?

    Choose one answer.

    Show the answer: Question 4 of 9

    Answer: The release build defines NDEBUG, so the call to open vanished with the assert

    This is load_settings again: the debug builds loaded 3 settings and the release builds none. Do the work on its own line and assert on its result.

  5. Question 5 of 9 What happens when the condition of a static_assert is false?

    Choose one answer.

    Show the answer: Question 5 of 9

    Answer: The program does not compile, and the compiler reports the assertion

    A false static_assert stops the build. Clang and GCC both showed the comparison that failed, 12 == 8, which points straight at the padding.

  6. Question 6 of 9 Which of these conditions can a static_assert check?

    Choose one answer.

    Show the answer: Question 6 of 9

    Answer: sizeof(Header) == 8

    static_assert needs a constant expression, such as facts about types and constants. Rule P.5 of the C++ Core Guidelines prefers such compile-time checks wherever the condition is known while compiling.

  7. Question 7 of 9 A function contains [[assume(x >= 0)]];, and a caller passes -3. What does the C++ standard say about the result?

    Choose one answer.

    Show the answer: Question 7 of 9

    Answer: Nothing at all, because the behaviour is undefined

    A false assumption is undefined behaviour. The optimiser may already have used the assumption to simplify the code, so the result can be anything, and nothing reports it.

  8. Question 8 of 9 A private helper of your own image module receives an index that the public function has already checked. How should the helper check it?

    Choose one answer.

    Show the answer: Question 8 of 9

    Answer: With assert, because only a bug in your own module can break it

    Preconditions of a public function belong in checks that run in every build; internal helpers can rely on assertions, because a broken condition there is a bug in the same code.

  9. Question 9 of 9 Which of these keeps checking the standard library's own preconditions, such as the index given to operator[], in release builds?

    Choose one answer.

    Show the answer: Question 9 of 9

    Answer: A hardened mode of the standard library

    The standard library follows the same split as your code: its hardened modes check its own preconditions, such as the index given to operator[], in release builds too.

What [[assume]] changes in the generated code, and what a false one does Pro: opens with a Pro, Premium or Ultimate pass

An assumption is a promise to the optimiser, and the clearest way to see what it buys and what it risks is to read the instructions the compiler generates. Here one function is compiled with and without an assumption by both compilers, then called with a value that breaks it.

In this part:

  • Two halving functions in arm64 assembly, with and without the assumption
  • A false assumption, reproduced: three answers to one call
  • When an assumption is worth its risk, and how to keep it honest

1 diagram · 2 runnable examples (C++ (halve.cpp), C++ (halve_main.cpp), Python) · about 5 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

A precondition policy for every build, with an exercise Pro: opens with a Pro, Premium or Ultimate pass

Choosing between an assertion, a check that stays in release builds and an error a caller can handle depends on who can break the condition. This part gives that choice as a policy, a macro for checks that must never disappear, and an exercise on a small matrix type.

In this part:

  • Who checks what: a policy for public and internal code
  • A check that stays in release builds
  • Your turn: checks for a matrix type

4 runnable examples (C header, C++, Python) · about 5 minutes

Opening this part with your pass…Pro part. It opens with the same pass as the part above.

Interview questions

  1. Fresher · Warm-up

    What is the difference between assert and static_assert?

    static_assert(condition, message) is checked by the compiler: the condition must be a constant expression, a false one stops the build with the message, and nothing of it remains in the program, so it costs nothing at run time. It suits facts about types and constants, such as sizeof(Header) == 8. assert(condition), from <cassert>, is checked while the program runs: a false condition prints the condition, the file and the line, then calls std::abort. It checks run-time values, such as an index, and it disappears entirely when NDEBUG is defined, as in most release builds, so its condition must have no side effects. A good answer adds that both document assumptions for the reader, and that neither is meant for input from users, which needs a check that stays in every build.

  2. Fresher to mid

    A crash happens only in release builds. In the code you find assert(queue.pop(job)); and assert(init_logging());. Explain the bug, how you would find every such case in a large code base, and how you would stop new ones from appearing.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  3. Mid to senior

    Your image library's public Image::pixel(x, y) receives coordinates that callers compute, some from user files. Should it assert, throw, return an error or use [[assume]]? Defend a policy, including what the library's own filters do in their inner loops.

    Model answer, Pro. First step and follow-ups

  4. Mid to senior

    A pull request adds [[assume(width % 16 == 0)]] to a hot loop of an image filter and claims a 20 % speed-up. What do you check before you approve it, and what would make you reject it?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • Assertions check your own beliefs: assert in debug builds, static_assert while compiling, at no run-time cost.
  • NDEBUG removes an assert and everything inside it, so never put work in its condition.
  • Check preconditions at public boundaries in every build, and report them to the caller as errors.
  • [[assume]] is a promise, never a check: when it is false, the behaviour is undefined.

Terms in this lesson

[[assume]]
A C++23 attribute, [[assume(condition)]], that tells the optimiser to treat a condition as true without checking it. If the condition is false while the program runs, the behaviour is undefined, and nothing reports it.
assertion
A check of something the programmer believes must be true, such as assert(i < v.size()): a false one prints the condition, the file and the line, then stops the program. assert disappears from builds that define NDEBUG.
NDEBUG
A macro that most release builds define. Where it is defined before <cassert> is included, assert(anything) expands to ((void)0), so neither the check nor any work inside its condition is left in the program.
precondition
What a function requires from its callers before it is called, such as an index inside the array. A public function checks its preconditions in every build, because code it does not control can break them.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.