Skip to content

Repository files navigation

Asserted: light enhanced C++17 assertions

Build Status

Assertion illustration

asserted(inBuffer==nullptr,
         "Unexpected non null input buffer", inBuffer, inBufferSize, !cmDecompressor);

asserted is a library making a trade-off between weight and features:

  • lighter is the standard library
  • full featured is for instance libassert

It eases C++ development at low cost by bringing much needed features.

Features

  • Context display via variable content
  • Stacktrace dump (requires libunwind to build on Linux, and addr2line at run time for symbol resolution)
  • Signals override to catch crashes
  • Automatic breakpoint in an attached debugger on crash
  • Compile-time enabling of group of assertions
  • No "unused variable" when assertions are disabled
  • Single header
  • Linux & Windows support

Install

  • Direct method: copy the single asserted.h inside your project

  • CMake method: install the library on your system:

mkdir build && cd build && cmake .. && make install

and declare it in your project's CMakeLists.txt with find_package(asserted)

Documentation

Reference API

The full API is made of 3 variants:

// Assertion with possibility to pass context variables
asserted(cond, ...);

// Variant which depends on the NDEBUG flag (as standard assert)
debug_asserted(cond, ...);

// Variant which is enabled per group
group_asserted(group_name, cond, ...); 

All of them:

  • dump the content of the provided context variables
    • Supports integral types, pointers and std::string
  • dump the stacktrace if enabled
    • Enabled by default on Windows
    • On Linux, requires libunwind-dev at build time and the compilation flag ASSERTED_WITH_STACKTRACE=1, plus addr2line reachable on $PATH at run time to resolve addresses into function names and file:line (falls back to raw addresses otherwise - see Optional dependencies)
  • are fully removed at compile time if disabled

Examples

// Equivalent to standard assertions (therefore subjected to NDEBUG)
debug_asserted(a == b);

// Enhanced with some context variables, evaluated only if the assertion fails
asserted(a == b, a, b, this, std::sin(a));

// The result of this one is shown below
asserted(idx < size, "Out of bound", idx, size);

asserted(idx < size, 'Out of bound', idx, size);

Examples with groups

Groups of assertions can be selectively disabled:

// The value of 1 enables the group, 0 disables it at compile-time (no run-time cost)
#define ASSERTED_GROUP_ENGINE 1

group_asserted(ENGINE, a == b);

// The result of this one is shown below
group_asserted(ENGINE, a == b, "Please refer to requirement 314P", a, b);

group_asserted(ENGINE, a == b, 'Please refer to requirement 314P', a, b);

Example of caught crash

If signals catching is not disabled, just including the library is enough to enable the feature.

Below is an example:

Crash caught

Configuration

The library is configured through compile time flags:

Flag Name Description Default
ASSERTED_WITH_STACKTRACE Enable the stacktrace dump if set to 1 0 on Linux
1 on Windows
ASSERTED_NO_ASSERT Disable all assertions at compile time if set to 1 0 (assertions enabled)
ASSERTED_NO_SIGNALS Disable the signal catching if set to 1 0 (signals caught)
ASSERTED_NO_SIGINT Disable the SIGINT signal catching if set to 1 0 (sigint handled)
ASSERTED_NO_DEBUG_BREAK Disable automatically breaking into an attached debugger on crash if set to 1 0 (enabled)
ASSERTED_NO_COLOR Disable the terminal colors if set to 1 0 (colored dump)
ASSERTED_CRASH_MSG_SIZE Max size in bytes of the formatted crash message 2048
ASSERTED_ALT_STACK_SIZE Size in bytes of the alternate signal stack (Linux/Unix), used so a SIGSEGV from stack overflow still has room to run the handler 262144 (256 KiB)
ASSERTED_ADDR2LINE_BIN Binary used on Linux/Unix to resolve stacktrace addresses (looked up via $PATH, or pass an absolute path) "addr2line"

⚠️ Prefer applying the same configuration everywhere. Translation units built with different flag values no longer collide at the ABI level, but each one still independently installs its own signal handlers at startup: only the last one constructed stays active process-wide (each restores the previous one on the way out), so mixing configurations is well-defined but still confusing. Keep it consistent unless you have a specific reason not to.


⚠️ Enabling the stacktrace dump on Linux requires the libunwind-dev library to be present at build time, and the addr2line command to be present on $PATH at run time (see Optional dependencies) for function names and file:line to be resolved. Without it, frames are still caught and printed, just as raw addresses.


⚠️ The signal/crash handling is installed by a static initializer, active only once it has run. A crash occurring inside another translation unit's global constructor that happens to run first (initialization order across TUs is unspecified) will not be caught.

Benchmarks

The purpose of such benchmark is mainly to check on the weight of the library. It was run on an average laptop.

Raw results from the internal benchmark (run test/run_suite.py -s performance):

===================================================================================
| asserted - Header include                        | 0.126 s                      |
| asserted - Header include - fully disabled       | 0.000 s                      |
| asserted - Library code size (-O2)               | 0 bytes                      |
| asserted - assertion code size 0 variable (-O2)  | 12 bytes/log                 |
| asserted - assertion code size 1 variable (-O2)  | 60 bytes/log                 |
| asserted - assertion code size 4 variables (-O2) | 132 bytes/log                |
| asserted - assertion compilation speed (-O0)     | 4613 log/s                   |
| asserted - assertion compilation speed (-O2)     | 3291 log/s                   |
| asserted - assertion runtime 0 variable          | 0.12 ns or 8343.6 Mlog/s     |
| asserted - assertion runtime 1 variable          | 0.12 ns or 8448.5 Mlog/s     |
| asserted - assertion runtime 4 variables         | 0.12 ns or 8162.5 Mlog/s     |
===================================================================================

FAQ

I do not see the logged stracktrace when a crash occurs
Displaying the stack trace at all requires two conditions: - presence of the stacktrace library at build time (native on Windows, `libunwind-dev` on Linux) - presence of the ASSERTED_WITH_STACKTRACE=1 directive as a compilation flag (for *every* compilation units)

On Linux, function names and file:line additionally require addr2line to be reachable on $PATH at the time of the crash (see Optional dependencies); without it, the frames are still printed, just as raw addresses instead.

Does the automatic debugger breakpoint do anything when not running under a debugger?
No. It is only ever triggered after actively checking that a debugger is attached (`IsDebuggerPresent()` on Windows, the `TracerPid` field of `/proc/self/status` on Linux/Unix) - a normal, undebugged run never raises anything and is completely unaffected. On Linux, a defensive fallback signal handler is additionally installed so that even if the check's assumption were ever wrong, the process would simply continue rather than being killed. Set `ASSERTED_NO_DEBUG_BREAK=1` to disable the feature entirely.
There are many compilation errors about ASSERTED_PRIV_IF(...) and asserteg(...)
The requirement to fully remove the code related to assertions at compile time implies using macros, so that the library is also efficient without optimization.

Using a group of assertion has a strong requirement: the group name must be defined, else a cascade of errors indeed appears.

Just check that the group is properly defined in all cases and that it has the desired value: 0 for disabled, 1 for enabled.

Optional dependencies

No dependency, with one exception: displaying the stacktrace on Linux involves two things, one at build time and one at run time.

  • Build time: the libunwind library, needed to capture raw return addresses. Add it with apt install libunwind-dev on Ubuntu.
  • Run time: the addr2line command (part of binutils, apt install binutils on Ubuntu - usually already present), used to resolve those addresses into function names and file:line. This is a deliberate design choice: resolving addresses in-process (as this library used to, and as libdw-based tools still do) means calling into code that allocates memory from inside the crash handler itself, which risks a deadlock if the crash was itself caused by heap corruption. Spawning addr2line as a short-lived subprocess avoids that, at the cost of it having to be present on whichever machine the crash actually happens on, not just the one the code was built on. If it isn't found, or fails for any reason, the stacktrace still gets printed - just as raw addresses instead of resolved ones. ASSERTED_ADDR2LINE_BIN (see Configuration) overrides the binary name/path used, if addr2line isn't reachable via $PATH or a different tool is preferred.

License

Released under the MIT license

About

Single-header C++17 enhanced assertions

Topics

Resources

Stars

Watchers

Forks

Contributors

Languages