r/cpp_questions • u/zaphodikus • 2d 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));
3
u/IyeOnline 1d ago
I would strongly recommend to just use a proper control structure rather than doing crazy things to fit the entire thing into a single statement.
Using an argument multiple times IMO isnt as big of an issue. It is very rare that things you are comparing are not trivial expressions. Furthermore: The expression re-use only happens in case of an assertion failure. If your expression somehow is not idempotent, that fact itself (or at least the fact that you are using it in an assertion) is the bigger issue. Working around that in the assertion macro is not a good solution.
Because that would really mean creating a macro for each temporary type for each parameter I might want to print surely
Well no. auto exists. Even without auto, you can get around this by using lambda captures: https://godbolt.org/z/3sdx5xPE5. But realistically you should use auto.
I pass the message in the second macro feels like a total hack
It sort of is, but see my link above. variadic arguments can help here to at least allow you to use commas. if you want to avoid the odd stream insertion operators in there, you would need something like std::format or the {fmt} library.
1
u/zaphodikus 1d ago
I don't really know what you mean when your say "proper control structure". I'm writing code, there is no structure to it because I'm in unfamiliar territory, and hence asking for clues as to where I'm lost.
Thanks for that example code, I dont understand quite a bit of it though, specifically the
...on the first#define ASSERT_EQ(LHS, RHS, ... ) \I think the rest of it parses in my brain though. But the
__VA_ARGS__won't parse if I pass more than one arg. I'm guessing that it's a C++ 20 feature. I'm still on C++17.I am guessing the
}()at the end of the lambda just invokes it? Which replaces my use of anif {}or ado {} while (0)which is just a better way because it's one scope?Basically you are saying that it's really up to me to only ever use this as a test, and I should ALWAYS use a temporary to prevent ever having idempotent or side effects for parameters. What is a useful rule for macros in general I guess.
I do use the
{fmt}library, and that did occur to me as a better way to produce a message. I'm just rubbish at templates and not yet using{fmt}for more than just a simple string in one unrelated place. I'll definitely go down that route, as it lets be forget about the__VA_ARGS__ellipsis confusion though. Thanks.1
2
u/alfps 1d ago
Not sure what you're asking, but some observations:
- It's a good idea to follow C++ convention and use
ALL UPPERCASEnames for macros. - Send error messages to the standard error stream, e.g. via
std::cerr, not the standard output stream. - Don't use magic numbers like
8for OS or application-dependent exit codes; use names.
Re the last point, do you mean Posix ENOEXEC or Windows ERROR_NOT_ENOUGH_MEMORY, or is the 8 an application specific error code?
You can simplify things by letting your macros just delegate to inline functions.
1
u/zaphodikus 1d ago
ALL_UPPERCASEyeah good suggestion, was not going to do that until I was certain I wanted a macro not a function library, but deffo helps to nail this down as aMACROgoing forward.- I'm learning some markdown :-)
std::cerrexcellent move , I have a more complex version of this where I call a function in the macro which also tees the output to the log file, but definitely a smart improvement`ERROR_NOT_ENOUGH_MEMORYwas not my intended result code, but I have 8 result codes in my app already for various errors, and I just chose the literal for one of them here to illustrate. BUT very good for anyone else reading this to learn some better coding style, because this above code looks sloppy even if it is just a quick sample new project I compiled, still it needs to be clean! I'll post an updated snippet in a moment.1
u/alfps 1d ago
Note that Windows programmers routinely use tools such as Microsoft's
errlookto check the message associated with an exit code.As an alternative to
errlookyou can use a more limited batch file, e.g.@echo off & setlocal (powershell -c "[ComponentModel.Win32Exception] %1" 2>nul) || ( echo.!%~n0 failed. 1>&2 )… or just work in Powershell directly if you can stand it.
All such tools end up calling Windows'
FormatMessagefunction.1
u/zaphodikus 1d ago edited 1d ago
I'm writing my own tool here, so it's a bit like robocopy (not really). 0 means no errors codes 1-4 are fatal but higher codes are fatal codes
// Exit codes constexpr int EXIT_NORMAL = 0; // The tool exited normally/SUCCESS constexpr int EXIT_BADARGS = 1; // A commandline or environment parameter was incorrect constexpr int EXIT_ABORT = 2; // Unexpected or fatal program/hardware error constexpr int EXIT_FAILED = 3; // Negative result - tool exited normally with FALSE constexpr int EXIT_ENGINEERROR = 4; // PE is very unhappy // Exitcodes 5 and upwards are NON-FATAL codes constexpr int EXIT_PRINTINGERROR = 5; // The test print under-ran or other print defect1
u/alfps 1d ago
Windows does support custom exit codes for an application with notions of success, warning and error, namely 32-bit
HRESULTvalues with the "customer" bit (bit 29) set. You can create such value viaMAKE_HRESULT. But it's an insanely complex scheme.An error
HRESULThas the msb set. All other values are success, with 0 is full success and any other value as warning/info success. In particular the value 1 denotes success as anHRESULT, but denotes an error as simple error code, and which exact error that is depends on whether one assumes Windows API or C/C++ convention.And to support
FormatMessageyou'd have to generate a "message DLL".1
u/zaphodikus 1d ago
I'm reading the codes in Python, so that Python is a small bit of glue code code would then have to strip the HRESULT mask off to see if it needs to retry or not. I mean if this was portable and posix, would we also pack exit codes still? I'm keen to not get too clever with what is essentially a tool inside a jenkins job.
1
u/alfps 1d ago edited 1d ago
if this was portable and posix, would we also pack exit codes still
"When in Rome do as the Romans do".
I haven't really thought about this before, I've not encountered the situation of a program possibly producing a warning exit code.
But now I think that I would probably have a basic executable with the simple yet unorthodox scheme you sketched, that is well suited for a Python driver but incompatible with e.g. and-or syntax in command interpreters. Then I'd just have a tiny wrapper executable that translated all the warning exit codes to plain 0, possibly contingent on command line option. That would work both in Windows and Posix environments.
1
u/fortsnek274 1d ago
For best code size, I just put the info in a static constexpr struct, and pass it to a central function.
2
u/zaphodikus 1d ago
I working with so much huge chunk of data in huge MB chunks, that code or data size is the least of my worries. BUT does help to see if I can get used to using constexpr more and more often.
1
u/fortsnek274 1d ago
It also keeps
#include <iostream>out of the header if nothing else needs it.2
1
u/mredding 1d ago
Assertions is for invariants - statements that must be true, or the program is in a fundamentally corrupted, unrecoverable state. That's why they terminate the program, because they proved the literally-impossible happened, and they happen all the time.
Validation is a runtime concern, and you don't terminate a program over invalid data. Files get corrupt, people fat-finger inputs, clients and servers may not agree on authentication or version compatibility. This doesn't mean an invariant was fundamentally invalidated. You don't shut down a daemon, service, or interactive program over this stuff.
Assertions don't work for validation because they compile out.
Console utilities are a slightly different story - it is conventional that a utility is not interactive, that if there is an error, it complains loudly, and terminates. Because it's not interactive, you don't get an opportunity to fix it in-situ, instead, you run it again with correct initial inputs.
Test frameworks are for exercising your code in test. Assertions only run when that function executes, but test frameworks empower you to assure a code path gets executed in scenario, and you can exercise any arbitrary scenario beyond YOU AND YOUR dev environment. You can validate more than just the assertions in the code.
macros really should be UPPERCASE
Not always true. I'd call this a nominal convention. assert is a macro, and it's not in uppercase. Inlining function-like macros are a common idiom, and they're not often written in all capitals.
It depends on what you're trying to do. I'd be fine if your assertion-like macro was in lowercase. I would argue that assume_true is a bad name, because I would expect it's used to bias the branch predictor, something like [[likely]] or __builtin_expect.
when printing an error use STDERR not STDOUT
Again, that depends. Succeed quietly, fail loudly. Is the error a message for the user? Or is it a diagnostic?
If something goes wrong, you have to tell the user - whether it's a simple "an error occurred", an exit(N); or SIGABRT (but those are REALLY obscure for a naive user), or an HTTP 404... Ideally you signal/message SOMETHING, perhaps more than one of these things. You have to know your intended audience and how they need to use the information.
So standard output is still on the table. You have to tell the user something, and often there will be something on standard output. And what separates a warning from an error is that mere warnings mean the process is still going to complete, and the work will produce a result. It might not be what the user expected. Typically you don't tell the user too much about warnings.
Diagnostics go on standard error. What was the error? Here is where you dump details about errors and warnings.
Standard output and standard error are both file handles. The terminal will redirect standard error to the terminal by default. So just because it all goes to the same place by default, they got there by different data paths. The user can redirect that file handle to wherever they want.
It's conventional that a user doesn't invoke an executable directly - what they don't see is that they're actually invoking a bash script that preps the environment for the application, redirects standard error to the system log, and runs the program binary on behalf of the user. Take a look at /bin and /usr/bin, you'll find a lot of bash scripts.
What I would do is write code as constexpr as possible, because you can embed unit-test-like static_assert right in the implementation, entire test scenarios. The tests either pass, or the code doesn't compile. It makes invalid code unrepresentable in your program - because you can't get that far.
Validation is handled by types:
class foo {
int x;
constexpr static bool valid(const int x) noexcept { return x == 42; }
foo() = default;
friend std::istream &operator >>(std::istream &is, foo &f) {
if(int x; is >> x && valid(x)) {
f.x = x;
} else {
is.setstate(std::ios_base::failbit);
}
return is;
}
friend std::istream_iterator<foo>;
public:
constexpr explicit foo(const int x) noexcept : x{x} { if(!valid(x)) throw; }
};
So then your code could look something like:
if(auto iter = std::istream_iterator<foo>{in_stream}, end{}; iter != end) {
use(*iter);
} else {
handle_error_on(in_stream);
}
The stream stores the state of the previous IO operation. A failure is itself a recoverable error, typically a failed parse, but also invalid data that makes for invalid state for the type. An integer that isn't 42 isn't a foo. If I were VERY generous, I'd either try to move the read pointer back, or barring that, put back the characters - and that would be done in the extraction operator, but I'm not going to bang all that out here. This implementation should also respect the exception mask.
That steam operator is also a great place to implement parsers, and get low level. We don't have to extract through the stream operator - we could instantiate the sentry, and then access the stream buffer directly, typically through std::istreambuf_iterator. It has a bulk IO interface, or if you have written your own, you could dynamic cast (a constant time operation that can be branch predicted) and access more optimal code paths you built in.
Also notice you cannot create an instance of foo that is invalid. The only public requires a parameter for initialization, and the ctor throws; you can't create a foo in an indeterminate state. Only the stream iterator can access the default ctor, which means the iterator has to be attached to the stream to get to the foo instance. This means you can't get access to an invalid foo in the first place.
I typically go further and make a type foo with only an insertion operator, and a friend foo_extractor class that can only be constructed by the stream extraction iterator; it overloads a cast operator to foo. The point is, if foo has an extractor friend, then once you have a valid foo, you can extract to it and get an invalid foo; by separating the two, we can make it so that the only way you could possibly get your hands on an invalid foo is by type punning - which I can't stop you.
1
u/zaphodikus 1d ago edited 1d ago
As usual I don't begin to understand many of the words above, so unlikely to be able to maintain this if I make it any smarter.
I have to look up so many words,
invariant, is effectively greek. I dont know much about types and the new casts, less whatpunningmeans. I'm just wanting to have the program stop when it detects a fatal set of conditions. Whether the cause is human or hardware error is a separate debate. If my program is ill-formed I probably want to use the assert macro to tell me that early on while I am coding. I'm still getting used to the various ways of doing that consistently. A lot of this is still dark magic to me. Sorry, but that's my limit. Hope it can help the next person though.
2
u/CptCap 1d ago
You can use variadic macros to feed something like std::format.
For example
#define message(...) std::cout << std::format(__VA_ARGS__) << std::endl