Writing assert() in C++

So we all know assert() from C’s Standard Library, right? If we implemented it ourselves as a macro, it might look something like this:

#define assert(x, msg) \
    if(!(x)) { fprintf(stderr, "Fatal error: %s\n", msg); abort(); }

With C++, any type that has operator! defined is compatible with this macro. We can then write:

std::ifstream infile("myfile.txt");
assert(infile, "could not open file");

We would want at least as much flexibility in a C++ version, as well as wanting to do things the C++ way – throwing an exception so that all local and global destructors are called, before the program exits cleanly (likely via a try/catch pair). We could rely on the type used in the assert being implicitly convertible to bool, as std::ifstream is, but there is another way.

Templates and r-value references to the rescue! (Other names used for r-value references depending on context are universal references or forwarding references.) We can start off our C++ assert() in the following way:

template <typename T>
inline void cpp_assert(T&& assertion) {
    if (!assertion) {
        throw;
    }
}

The above code is a complete, fully working code fragment, however, we’re not done, as we’d still like:

  • to specify what kind of exception to throw, possibly a std::exception or one defined in <stdexcept>, but not necessarily
  • to be able to configure build-wide assertions on or off in one place
  • to be able to log non-fatal assertion failures to a logging stream, such as std::cerr, without terminating the program

So without further ado we introduce a fully functional header file with functions cpp_assert() and log_assert() defined inline:

#include <exception>
#include <iostream>
#include <string_view>

extern constexpr bool ReleaseBuild = false, AssertionsBuild = true;

template<typename T, typename E = std::exception>
inline void cpp_assert(T&& assertion, const E throwing = {}) {
    if constexpr (AssertionsBuild) {
        if (!assertion) {
            throw throwing;
        }
    }
}

template<typename T>
inline void log_assert(T&& assertion,
                        const std::string_view log_msg = {},
                        std::ostream& out = std::cerr,
                        const char *file = __FILE__,
                        const int line = __LINE__) {
    if constexpr (!ReleaseBuild) {
        if (!assertion) {
            out << file << '(' << line << "): *** assertion failed: " << log_msg << '\n';
        }
    }
}

As can be seen, cpp_assert() hopefully only generates code (the function potentially being both inline and empty) if the constexpr variable AssertionsBuild is set to true, and throws an exception of template type E only if operator! applied to forwarded assertion of type T yields true. The log_assert() functions is similar and only generates code if ReleaseBuild is set to false, logging to an output stream in the format used by Visual Studio. Changing either of these global variables will force a rebuild of all files referencing this header.

Here is a sample main() to test it fully:

#include "cpp_assert.hpp"
#include <fstream>

class Error{};

int main() {
    std::cerr << "entering main()\n";
    std::ifstream in{"myfile.txt"};
    log_assert(in, "cannot access myfile.txt");
    cpp_assert(in, Error{});
    std::cerr << "exiting main() normally\n";
    return 0;
}

Try running the program with or without myfile.txt being available, and with the two build flags using all the combinations, and see if the output is what you expect each time. Also observe whether your compiler elides any empty calls as efficiently as the assert() macro when NDEBUG is set. Feel free to adapt this code (based on the techniques described) for your own projects; in the future I plan to change the header file into a C++ module, and use std::source_location instead of the __FILE__ and __LINE__ pre-processor macros.

Resources: Download or browse the source code

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s