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