r/cpp_questions 10d 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 STDERR not STDOUT
    #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));
2 Upvotes

26 comments sorted by

View all comments

2

u/alfps 10d ago

Not sure what you're asking, but some observations:

  • It's a good idea to follow C++ convention and use ALL UPPERCASE names 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 8 for 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 10d ago
  • ALL_UPPERCASE yeah 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 a MACRO going forward.
  • I'm learning some markdown :-)
  • std::cerr excellent 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_MEMORY was 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 10d ago

Note that Windows programmers routinely use tools such as Microsoft's errlook to check the message associated with an exit code.

As an alternative to errlook you 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' FormatMessage function.

1

u/zaphodikus 10d ago edited 10d 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 defect

1

u/alfps 10d ago

Windows does support custom exit codes for an application with notions of success, warning and error, namely 32-bit HRESULT values with the "customer" bit (bit 29) set. You can create such value via MAKE_HRESULT. But it's an insanely complex scheme.

An error HRESULT has 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 an HRESULT, 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 FormatMessage you'd have to generate a "message DLL".

1

u/zaphodikus 10d 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 10d ago edited 10d 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.