Carbonite Asserts

Options to diagnose badly behaving code

Overview

The assertion framework in Carbonite is designed to be usable for a variety of situations, extensible, and performant.

At its core, an assertion framework allows logging a message and/or stopping in the debugger if an unexpected situation occurs. This allows a programmer to intervene and observe a malformed situation at the point where it first happens. Various options are available for debug and release builds. If the assert is not handled, a crash is triggered whereby the state of the program ideally is saved for later investigation.

The basic types of asserts provided by Carbonite are as follows:

Note

Static asserts are best. If checks can be done at compile time (or link time), use static_assert.

Default Implementation and Overriding

Debug asserts and Runtime check asserts can be enabled or disabled by default or by overriding. The CARB_ASSERT macro has an additional macro CARB_ASSERT_ENABLED that will be set to zero if debug asserts are not enabled, and non-zero if debug asserts are enabled. This can be used to conditionally enable code that may do additional work or may add members if debug asserts are enabled:

struct MyClass
{
#if CARB_ASSERT_ENABLED
    bool checkValue{};
    MyClass(bool value) : checkValue(value) {}
#else
    MyClass() = default;
#endif

    void foo()
    {
        CARB_ASSERT(checkValue);
    }
};

Warning

Adding extra members conditionally (as shown above) must not change the ABI for plugins. This is recommended for header-only classes only.

Runtime check assertion macro (CARB_CHECK) similarly has an additional macro CARB_CHECK_ENABLED that is set to zero if runtime check asserts are not enabled, and non-zero if runtime check asserts are enabled, and can be used in conditional code just as CARB_ASSERT_ENABLED is.

The CARB_ASSERT_ENABLED and CARB_CHECK_ENABLED macros are two-way. The default implementation defines CARB_ASSERT_ENABLED to match the value of CARB_DEBUG, effectively enabling debug asserts for debug builds and disabling them for non-debug builds. The default implementation of CARB_CHECK_ENABLED is always set to 1 making it always enabled. However, this behavior can be overridden by defining CARB_ASSERT_ENABLED or CARB_CHECK_ENABLED to the desired value, either on the compiler command line (or in your build scripts such as premake5.lua or Makefile) or before including carb/Defines.h. This allows, for instance, enabling CARB_ASSERT for release or optimized builds, or disabling CARB_CHECK for certain builds.

Warning

Assertion expressions should not have side effects! When disabled, the expression is not evaluated.

Similarly, the CARB_ASSERT macro can be specified on the compiler command line or defined before including carb/Defines.h. This allows the assert behavior to be customized as desired. The default implementation of CARB_ASSERT and CARB_CHECK are the same (but enabled at different times) and is as follows:

#    define CARB_IMPL_ASSERT(cond, ...)                                                                                \
        (CARB_LIKELY(cond) ||                                                                                          \
         ![&](const char* funcname__, ...) CARB_NOINLINE {                                                             \
             return g_carbAssert ?                                                                                     \
                        g_carbAssert->reportFailedAssertion(#cond, __FILE__, funcname__, __LINE__, ##__VA_ARGS__) :    \
                        ::carb::assertHandlerFallback(#cond, __FILE__, funcname__, __LINE__, ##__VA_ARGS__);           \
         }(CARB_PRETTY_FUNCTION) ||                                                                                    \
         (CARB_BREAK_POINT(), false))

This macro essentially boils down to a boolean expression, which takes advantage of the lazy-evaluation boolean OR (||) operator. It is effectively doing (check1 || check2 || check3), where check1 is the expression that you’re asserting. If it succeeds, great: execution continues. Also note the use of CARB_LIKELY: this tells compilers to optimize expecting that the result will produce true. If check1 fails, check2 is evaluated. This calls the Assert Handler and passes a stringification of the expression, along with any additional arguments passed to the variadic portion of the CARB_ASSERT macro. If the Assert Handler returns true, execution continues as if the assert expression had not failed. If false is returned however, check3 is evaluated. This is not really a check at all, but merely invokes CARB_BREAK_POINT and evaluates false to the entire expression.

This macro has a few other tricks. In order to attempt to generate more ideal assembly, the call to the assertion handler is in a lambda that is declared as CARB_NOINLINE to prevent inlining (due to a bug in some versions of Microsoft Visual C++, this lambda also accepts variadic arguments in an attempt to prevent inlining). By avoiding inlining the lambda, the call to the assertion handler (which should be called rarely) can be removed from the fast path of code execution and prevent wasted bytes in the instruction cache.

Generally speaking, it is better to use the default behavior of CARB_ASSERT and CARB_CHECK and instead handle assertions using the Assert Handler.

Assertion Handlers

When a CARB_ASSERT, CARB_CHECK or even CARB_FATAL_UNLESS assertion fails, the first step is to call the Assertion Handler. This gives the application the chance to notify the user about the assert, log the assert, attach a debugger, etc. If the Carbonite Framework is started and built-in plugins are loaded, g_carbAssert will be set to the built-in carb::assert::IAssert plugin, and carb::assert::IAssert::reportFailedAssertion() will be called when an assert occurs. If the Framework is not started, the carb::assertHandlerFallback() is called instead.

To override the Assertion Handler for the local module (EXE/DLL/SO), you can set the g_carbAssert global variable to a different implementation of the carb::assert::IAssert structure any time after carb::assert::registerAssertForClient() is called. Example:

// Override the assert handler with our test one
class ScopedAssertionOverride : public carb::assert::IAssert
{
    carb::assert::IAssert* m_prev;
    std::atomic_uint64_t m_count{ 0 };

    static carb::assert::AssertFlags setAssertionFlags(carb::assert::AssertFlags, carb::assert::AssertFlags)
    {
        return 0;
    }

    static uint64_t getAssertionFailureCount()
    {
        return static_cast<ScopedAssertionOverride*>(g_carbAssert)->m_count.load();
    }

    static bool reportFailedAssertionVimpl(const char*, const char*, const char*, int32_t, const char*, ...)
    {
        static_cast<ScopedAssertionOverride*>(g_carbAssert)->m_count.fetch_add(1);
        return false;
    }

public:
    ScopedAssertionOverride()
        : carb::assert::IAssert({ &setAssertionFlags, &getAssertionFailureCount, &reportFailedAssertionVimpl })
    {
        m_prev = std::exchange(g_carbAssert, this);
    }
    ~ScopedAssertionOverride()
    {
        g_carbAssert = m_prev;
    }

    auto& count()
    {
        return m_count;
    }
};

While it may be possible to override the Assert Handler globally for an application, this is not currently tested or supported.

Debug Asserts

Debug asserts are performed via CARB_ASSERT and enabled when CARB_ASSERT_ENABLED is non-zero. By default, these checks are available only when CARB_DEBUG is defined (i.e. debug builds). As such, they are best used for algorithmic checks and to ensure that changes to a system meet expectations. Since they are typically only enabled for debug builds, they are less useful if debug builds are not in wide usage. Keep in mind that the condition checked by CARB_ASSERT is compiled out when not enabled, and therefore must have no side-effects.

Runtime Check Asserts

Runtime checks are performed via CARB_CHECK and enabled when CARB_CHECK_ENABLED is non-zero. By default, these are always on, but can be disabled if a consumer of Carbonite desires. CARB_CHECK is not intended to be included in Shipping builds but is designed to be included in optimized builds that will have a wide range of developers and QA personnel running them. However, since Carbonite currently only packages Debug and Release builds, CARB_CHECK is turned on for both builds. This macro is therefore forward-looking to a potential future where Debug, Checked and Shipping builds are provided, where Checked would have CARB_CHECK enabled, but Shipping builds would not. Keep in mind that the condition checked by CARB_CHECK is compiled out when not enabled, and therefore must have no side-effects.

Fatal Conditions

CARB_FATAL_UNLESS exists to terminate an application when a check fails. This should only be used when gracefully handling the error condition is impossible, and continuing to execute can lead to difficult-to-diagnose instability or data corruption. In contrast to CARB_ASSERT and CARB_CHECK, this macro is always enabled and can never be disabled, even in Release or hypothetical future Shipping builds, but may be overridden similarly to the other assertion macros.

Due to specific behavior to always terminate, CARB_FATAL_UNLESS is similar to, but different than the other asserts:

#        define CARB_FATAL_UNLESS(cond, fmt, ...)                                                                      \
            (CARB_LIKELY(cond) ||                                                                                      \
            ([&](const char* funcname__, ...) CARB_NOINLINE {                                                          \
                if (false)                                                                                             \
                    ::printf(fmt, ##__VA_ARGS__);                                                                      \
                g_carbAssert ? g_carbAssert->reportFailedAssertion(#cond, __FILE__, funcname__, __LINE__, fmt, ##__VA_ARGS__) : \
                   ::carb::assertHandlerFallback(#cond, __FILE__, funcname__, __LINE__, fmt, ##__VA_ARGS__);           \
             }(CARB_PRETTY_FUNCTION), std::terminate(), false))

CARB_FATAL_UNLESS can be overridden as described above, but a program is malformed if execution is allowed to continue past CARB_FATAL_UNLESS when the expression returns false; this is undefined behavior.

How to use these macros

CARB_ASSERT has the lowest exposure, CARB_CHECK has middle exposure and CARB_FATAL_UNLESS has absolute exposure. These are varying degrees of guardrails to keep your program in check in a wide variety of circumstances. The principles listed below are not meant to be exhaustive, but to serve as a guide for usage.

In general, failing an assert should always be considered fatal, just at different points of the development cycle. As such, they should be used for things that absolutely should never ever fail based on the current authorship of the program. Asserts are an excellent practice to ensure that the expectations and assumptions of the code are called out in a program- enforced manner, as opposed to comments that may explain intent but are not enforced. In other words, asserts tell future programmers what the code assumes so that changes to the code either maintain those assumptions or require them to change.

Asserts lose effectiveness if they are not enabled. For instance, if use of CARB_ASSERT is prolific, but CARB_ASSERT_ENABLED is always false (because debug builds are too slow, say), then CARB_ASSERT is effectively useless.

Use CARB_ASSERT for programmer-only checks or debug unit tests

CARB_ASSERT is typically only compiled into debug builds, which do not have wide exposure (running several instances of the program). As such, this makes it useful for algorithmic checks where it is not desirable to run always, but as long as code has not been changed will always succeed. When a programmer starts making changes to the code with debug builds, the asserts will be enabled which ensures that the programmer continues to meet the existing assumptions of unchanged code.

A simple example of this is: .. code:: c++

int a = 2; // … int b = 2; // … CARB_ASSERT((a + b) == 4); // proceed with validated assumption that a+b == 4

As this code is written, there is no possible way that the assert would fail. However, if a programmer starts making changes to this code and decides that a should equal 3, then the assert will fail and indicate to the programmer that the code has an assumption that a + b should always equal 4. The programmer will then have to investigate why this is and evaluate the situation.

As a side note, sometimes it makes sense to have conditional code that is only intended to be enabled when changes are being made to a system as the cost to assert is very high. For instance, perhaps you have a binary-tree implementation with a validate() function that will walk the entire tree and ensure that it is built correctly. This is cost-prohibitive to run in most cases, but it is also unnecessary to run unless the tree algorithm is changing. It would be better to have the calls to validate() compiled out unless specifically enabled:

#define BINARY_TREE_CHECKS 0 // Enable if you are working on this system

// ...

#if BINARY_TREE_CHECKS
    CARB_ASSERT(validate()); // Run full validation (costly!)
#endif

Use CARB_CHECK for more exposure

Since CARB_CHECK is always enabled by default (but can be disabled if desired), code utilizing it will run on many more instances and therefore has much greater exposure. This can be useful to find issues in complex code and non-deterministic code (especially multi-threading code).

Like CARB_ASSERT, CARB_CHECK should only be used for situations where the assertion will never fire unless changes are being made to the code, and will let future programmers understand the assumptions made in the code. However, if debug builds are not utilized, or CARB_ASSERT_ENABLED is never true, it may make more sense to use CARB_CHECK instead.

As it has greater exposure, CARB_CHECK can also be used in situations to check non-deterministic execution, such as with multi-threaded code. This can be used to find edge cases or to find 1-in-1000[0…] bugs with generally very little runtime cost.

Use CARB_FATAL_UNLESS to catch unrecoverable issues as early as possible

The CARB_FATAL_UNLESS macro, which always guarantees to terminate the program if the assertion fails, should be used for unrecoverable issues, as early as possible. For instance, a heap free() function may determine that memory is corrupt, and should use CARB_FATAL_UNLESS to inform the user and terminate the program, since continuing on merely leads to further instability and eliminates information that is currently present that could be used to diagnose the issue.

If continuing on could lead to data corruption, CARB_FATAL_UNLESS should be used to halt the program with a message to the user.

Another example of using CARB_FATAL_UNLESS is system or runtime-library calls that should never fail. For instance, in a RAII mutex class that wraps pthread’s mutex_t, the API functions such as pthread_mutex_init() are allowed to fail in various ways, but in practice should not fail unless given bad data or the system is experiencing instability. It is appropriate to use CARB_FATAL_UNLESS if pthread_mutex_init() returns anything other than zero, as this is generally not an error that can be recovered.

CARB_FATAL_UNLESS requires a printf-style user message be provided, and as much information should be imparted in the message as possible.

Providing your own assert macro

It is entirely possible that none of the above macros suffice for your needs. Perhaps you want to allow side-effects (i.e. always execute even when disabled) or to log failure conditions. In these cases, it is adviseable to create your own assert macro rather than overriding the definition of any of the above macros. This will also prevent breaking assumptions that are present in using the Carbonite macros.