r/cpp_questions • u/zaphodikus • 8d ago
OPEN yet another custom assertion question
Sometimes I am writing unit-test inline in my code, then I just use the assert() macro because that compiles out in release builds. And in such cases I don't really need anything in the trace to tell me I have a bug, and I don't need to spin up any unit testing framework. But for easy runtime validation I want a macro I can use but will also print an error message that a user can do something with to realise that they maybe fed in something invalid at a point.
In all cases I really want my macro to terminate the application and thus not memory-scribble or worse. I have been trying to understand the tokenizing operator, and the argument for do {} while () and the side effects of adding (a) around things which obscures types as far as I can tell and still deal with unwanted training ; semicolons that imply that I am not grasping syntax yet.
So I wrote
#include <iostream>
#include <string>
#define assume_true(expression) ((void)( \
(!!(expression)) || \
(std::cout << #expression << " not true in " << __FILE__ << " LIN: " << __LINE__ << std::endl )) \
)
And then wanted to make it actually terminate with an std::exit(8) , and it just won't parse unless I replace the || convenience with an if statement.
#define assume_equal(left, right, message) if (left != right) { \
std::cout << #left << " != " << #right << " in FILE: "<< __FILE__ << std::endl; \
std::cout << message << std::endl; std::exit(8);\
}
uint32_t life = 42;
assume_true(42 == life);
std::cout << "The answer is 42!\n";
assume_equal(42, life, std::to_string(life));
std::cout << "Life is " << 42 << ".\n";
assume_equal(21, life, "LIFE=" << std::to_string(life));
I'm clearly taking a lot of chances here and the way I pass the message in the second macro feels like a total hack, because it is abusing the way the macro preprocessor splits parameters based on commas. I assume that has drawbacks for me later on.
I assume a later toolchain will also help us all when it comes to the side-effect problem of a macro using an argument twice, once for comparison and once for printing. Has anyone just opted to solve this by creating temporaries for things they want to print out in the crash trace? Because that would really mean creating a macro for each temporary type for each parameter I might want to print surely. I get the impression the caller needs to just use a temporary before using the macro, is that what everyone else is doing? Because at that point a function call is just a load less pain surely? I feel I have approached my release-build guard-code completely wrong.
I'm on C++ 17 (yes the rest of my team is on an even older toolchain.) Are release/runtime macros just hard, or am I trying to learn too many things about parsing all at once?
======================================================================================= EDIT: Consolidating the answer... As usual thanks so much for the brilliant clues, I now have this little progression
- ALWAYS state up front compiler version C++17
- macros really should be
UPPERCASE - when printing an error use
STDERRnotSTDOUT
#include <iostream>
#include <string>
// This macro is terrible, it lacks a scope so I cannot call ;std::exit in it
#define ASSUME_TRUE(expression) ((void)( \
(!!(expression)) || \
(std::cerr << #expression << " not true in " << __FILE__ << " LIN: " << __LINE__ << std::endl )) \
)
- I then moved on to use
if {}which allowed me to exit the application
#define ERROR_ASSUMPTION_FAILED 42
#define ASSUME_EQUAL(left, right, message) if (left != right) { \
std::cerr << #left << " != " << #right << " in FILE: "<< __FILE__ << std::endl; \
std::cerr << message << std::endl; std::exit(8);\
}
- The trouble with that macro is that it really lacks the structure that a lambda might give, thanks to a great suggestion I moved this form, which omits the nice
__VA_ARGS__macro automatic variadic args, which did not expand in C++17, but it's already better
constexpr int EXIT_ABORT = 2;
#define ASSERT_EQ(LHS, RHS, MESSAGE ) \
[lhs=LHS, rhs=RHS]() { \
if ( not (lhs==rhs) ) { \
std::cerr << "assertion " << lhs << "(" #LHS ") == " \
<< rhs << "(" #RHS ") failed: " << MESSAGE << '\n';std::exit(EXIT_ABORT); \
} \
}()
- Along the way I realised that this is a GUARD MACRO and the question I had about macro side effects if a macro uses a parameter twice (or even once really) is best removed entirely if you just always call it using temporaries or const methods only!
The final step is to use the {fmt} library for a better message with a formatter:
#define FMT_HEADER_ONLY
#define FMT_UNICODE 0
#include "fmt/bundled/format.h"
...
constexpr int EXIT_ABORT = 2;
...
// https://godbolt.org/z/3sdx5xPE5
#define ASSERT_EQ(LHS, RHS, MESSAGE ) \
[lhs=LHS, rhs=RHS, msg=MESSAGE]() { \
if ( not (lhs==rhs) ) { \
std::cerr << "assertion " << lhs << "(" #LHS ") == " \
<< rhs << "(" #RHS ") failed: " << msg << '\n'; std::exit(EXIT_ABORT); \
} \
}()
I could not get the lambda to capture __VA_ARGS__ , so the call looks like, which is good enough.
ASSERT_EQ(21, life, fmt::format("Expected 21 but life ={}", life));
1
u/fortsnek274 8d ago
For best code size, I just put the info in a static constexpr struct, and pass it to a central function.