TL;DR

Any program can have two classes of issues:

  1. Runtime problems: situations programmer foresaw and handled; expected. E.g. invalid inputs, no access to resource, unreachable network, …

  2. Bugs: programmer mistakes; unexpected. E.g. buffer overflow, access violation, …

There are different tools to deal with each; don’t conflate one with the other.

An assertion is just a validation of a programmer expectation; it “fires” when the expectation is betrayed.

(1) is familiar to most; use one of your language-provided feature including if, try-catch-throw, etc. that work at runtime. (2) needs early spotting of failing assumptions, changing invariants, unmet pre-/post-conditions at development-time1.

Assertions

int read_image(string path) {
  if !os.exists(path) {                // (1) runtime issue
    file_missing_dialog.show(path);
  }
  Image i = open_image(path);
  REQUIRE(i.resource_id != nullptr,    // (2) possible bug in open_image
          "Resource ID unassigned for opened image");
  cache(r.resource_id);
}
Example: Using REQUIRE to check resource ID generation by open_image

With asserts you’re checking your own code for possible bugs. In the above code we try to cache the most recently opened image with its resource ID; before doing so we make sure such an internal ID was properly assigned to the opened image with a REQUIRE macro. This check will be stripped from the final, released binary. It’d only exist in debug builds which developers and testers would use.

Aren’t assertions if s anyway?

Though asserts are made of if statements, that’s true only for debug builds; they don’t fire on non-debug builds2. C and C++ have a preprocessor stage, so most codebases define some assertion macro which compile to a no-op on non-debug builds and on a debug build – run by the developer or tester – define it as valid if; upon firing, an assertion helps the developer better understand the issue e.g. log a message, invoke the debugger3, etc.

#ifdef NDEBUG          // usually defined for non-debug builds
#  define REQUIRE(expr, msg) ((void) expr)            // no-op
#else
#  define REQUIRE(expr, msg) if (expr) {                     \
                               std::cerr << __FILE__ << ':'  \
                                         << __LINE__ << ": " \
                                         << msg;             \
                             }
#fi

You can have a variety of assertion macros like REQUIRE, DCHECK4, ASSERT, ENSURE, etc. to suit your needs and cases. check seems to be a minimal, header-only C library with basic assertions macros.

Why strip assertions in non-debug builds?

It’s a pointless wastage of CPU cycles, power, etc. to retain assertions in non-debug builds.

A software’s end-user seldom care/know about its technicalities5. Oblivious to its design and code, they can’t make sense of a failed assertion’s output — logs, running under debugger, etc. Contrastingly, programmers (and testers) specifically run debug builds just to catch a failing assumption early on. Stripping of assertions is one of the reasons6 why debug builds are slow and non-debug builds are fast — lesser if checks/branches.

Comparison

Issue Source When Check Example
Error External Runtime if, try-catch if (bad_handle) { … }
Bug Internal Development Time Assertion macros ENSURE(valid_count)

Key difference is source: errors are due to the environment, user, etc. You simply have to check for them at runtime. However, assertions are to check your subsystems (module, function, component, etc.) for buggy code. This is when some expected state isn’t reached or some condition or invariant isn’t met. This is internal, within developer’s control. They can be avoided before the program hits the end-user by the programmer checking what broke the assumption.

Example with VLC

Let’s say we’re developers of the popular media player VLC. Checking for a bad/corrupt input (avi/mkv file) should be done with a good ‘ol if that works at runtime.

However, whether VLC’s own source filter7 is always feeding normalized float data (within [0, 1]) to its decoding filter8 is something to be checked by an assert; checking this with a runtime if in Release builds would be a waste of end-user CPU’s time and energy.

Though a plain if also works as an assertion, a good programmer would use the right tool for the right job.

Footnotes


1

The unspotted failed assumptions eventually become bugs; hopefully reported by an annoyed (but sincere) end-user.

2

Non-debug builds generally have optimizations turned on and debug information stripped. Release builds, published to end-user, is a common example.

3

Different compilers have different ways of invoking the debugger; MSVC has DebugBreak, GCC on POSIX systems have raise(SIGTRAP); platform-independant abstractions are available too.

4

To denote a check that works only in the Debug build configuration.

5

A technical end-user can always build the debug version of an open-source software. With closed source software, you’re at the mercy of a corporation to offer debug builds.

6

Others include higher optimization levels, linking to non-debug builds of dependencies, etc.

7

Component feeding data to the decoder.

8

Component processing data before sending to sink (display/speakers).