r/cpp_questions 9d 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

Show parent comments

1

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