Блог пользователя EnDeRBeaT

Автор EnDeRBeaT, 27 часов назад, По-английски

Introduction

Rust is a quite popular programming language with a lot of different takes on how to write good code. Some people use it in competitive programming with varying success, but in general, C++ is the king.

That doesn't mean that we can't take great ideas from this language, and one of Rust's shining jewels is it's macro system.
Describing why it is better than "find and replace" thing we have in C++ is not the topic of this blog. We are here to steal from Rust, and today we will steal one of the coolest utilities for a competitive programmer: dbg! macro!

dbg!

The dbg! macro in Rust has simple functionality: it evaluates the expression, prints it's to stderr and returns it. Last part is very, very useful, and you will see why.

Example:

let a = 2;
let b = dbg!(a * 2) + 1;
//      ^-- prints: [src/main.rs:2:9] a * 2 = 4
assert_eq!(b, 5);

Look, we evaluated a * 2 mid expression and b still became 5. Isn't that cool?

Not cool? Here's a crazier example:

fn factorial(n: u32) -> u32 {
    if dbg!(n <= 1) {
        dbg!(1)
    } else {
        dbg!(n * factorial(n &mdash; 1))
    }
}

dbg!(factorial(4));

Which produces the following output:

[src/main.rs:2:8] n <= 1 = false
[src/main.rs:2:8] n <= 1 = false
[src/main.rs:2:8] n <= 1 = false
[src/main.rs:2:8] n <= 1 = true
[src/main.rs:3:9] 1 = 1
[src/main.rs:7:9] n * factorial(n &mdash; 1) = 2
[src/main.rs:7:9] n * factorial(n &mdash; 1) = 6
[src/main.rs:7:9] n * factorial(n &mdash; 1) = 24
[src/main.rs:9:1] factorial(4) = 24

Now, do you think that it's a great feature? We can print an expression, an expression in another expression, hell, we can even inject into the if statement with it!

By that time, you are probably convinced that you can't live without this function. Or you just want to see how this feature can be implemented in C++. Either way, you won't be disappointed.

Let's steal the macro

On the basic level this function just prints the expression and its value (well it also prints filename, row and column, but that isn't that hard to do), and then returns the value.

It is very easy to obtain expression as a string literal in C++:

#define TOSTRING(expr) #expr

std::cout << TOSTRING(1 123 * 128 01); // prints 1 123 * 128 01

For "printing and returning" part, we can just use a regular template function. So, our basic draft for a debug macro will be this:

template<typename T>
T debugln(std::string_view str, T&& expr) {
    std::cerr << str << " = " << expr << '\n'; // assuming expr is even printable
    return expr;
}
#define dbg(expr) debugln(#expr, expr)

And it is already almost working:

int a = dbg(5); // prints 5 = 5
int b = dbg(a * 2) + 5; // prints a * 2 = 10
std::cout << a << ' ' << b; // prints 5 15

Using dbg in if statements is a 2 symbol fix, we just need to realize that (x) <=> x hence we can just wrap our macro body in parentheses.

template<typename T>
T debugln(std::string_view str, T&& expr) {
    std::cerr << str << " = " << expr << '\n';
    return expr;
}
#define dbg(expr) (debugln(#expr, expr))

int main() {
    int a = dbg(5); // prints 5 = 5
    int b = dbg(a * 2) + 5; // prints a * 2 = 10
    if dbg(a * 2 + 5 == b) { // prints a * 2 + 5 == b = 1
        std::cout << "Macro is working!!"; // prints Macro is working!!
    }
}

Wait, that's all?

For the single value case, yes. But there's another thing about dbg! macro in Rust: it works with multiple values...

let (a, b) = dbg!("The answer to life is", 42); // print
//      ^-- prints: [src/main.rs:1:14] "The answer to life is" = "The answer to life is"
//                  [src/main.rs:1:14] 42 = 42

It prints each argument on a new line and returns a tuple.

I mean, it's probably not too hard to add, right?

WRONG

That's where C++ macro system shines, and by that I mean that here it sucks. By the end of the blog our code will look so disgusting and foul it would make the devil blush.

So let's start!

__VA_OPT__ macro

In C++20 (God bless C++20) we got a new macro called __VA_OPT__.
The use is pretty straightforward: if our macro is variadic (has ... as one of the arguments) and ... is non-empty, __VA_OPT__(content) gets replaced by content, and expands to nothing otherwise.
This is a very useful macro, which will aid us severely. Right now we can use it to make a tuple if we get more than one argument.

#define dbg(head, ...) __VA_OPT__(std::make_tuple)(FOR_EACH(head, __VA_ARGS__))

The change is simple: we added ... in macro definition. If dbg(expr) is called, an evaluation of this expression will be returned. If something like dbg(x, y) is called, __VA_OPT__ will be triggered and we will make a tuple of all values that FOR_EACH makes.

Wait...

What the fuck is FOR_EACH?

FOR_EACH is obviously something we will have to implement. On the high level, this function will replace each argument it was given with something (in case of __VA_ARGS__, it will do something with each argument in it).

So, can we iterate arguments in a C++ macro?

The answer is no*

The asterisk

Well, we kinda can.

Iteration is definitely out of question, but we can try recursion. According to cppreference (link), you can create pseudo recursion using something like the following code:

#define EMPTY
#define SCAN(x)     x
#define EXAMPLE_()  EXAMPLE
#define EXAMPLE(n)  EXAMPLE_ EMPTY ()(n-1) (n)
EXAMPLE(5) // expands to EXAMPLE_()(5 &mdash; 1)(5)
SCAN(SCAN(EXAMPLE(5))) // expands to EXAMPLE_()(5 &mdash; 1 &mdash; 1)(5 &mdash; 1)(5)

How does it work? I am probably not the best person to explain this. Without going in too much details, you can't recurse in macros: When compiler scans the replacement list of a function macro X and it sees the same macro X, it won't replace it.
But once compiler is out of the replacement list of X, it becomes available for replacement once again.

Let's see it on a better example than cppreference gives us, and make a macro that just repeatedly creates 1s.

#define EMPTY
#define SCAN(x)     x
#define ANOTHER_ONE()  ONE
#define ONE() 1 ANOTHER_ONE EMPTY ()()

SCAN(ONE())
// Expands to
1 1 ANOTHER_ONE()()

Let's now expand this step by step (this may be wrong, this feature is very weird, and not properly documented in C++ ISO):

SCAN(ONE()) // ONE() -> 1 ANOTHER_ONE EMPTY ()()
// if we don't have EMPTY, ANOTHER_ONE() would have mapped to ONE disabling further expansion
SCAN(1 ANOTHER_ONE EMPTY ()()) // ONE is not replaceable, EMPTY -> 
SCAN(1 ANOTHER_ONE ()()) // ONE is replaceable, SCAN(1 ANOTHER_ONE ()()) -> 1 ANOTHER_ONE ()()
1 ANOTHER_ONE ()() // SCAN is not replaceable, ANOTHER_ONE() -> ONE
1 ONE() // ONE() -> 1 ANOTHER_ONE EMPTY ()()
1 1 ANOTHER_ONE EMPTY ()() // EMPTY -> 
1 1 ANOTHER_ONE ()() // ANOTHER_ONE is replaceable, but the expansion is already over.

EMPTY macro here is important, without breaking up ANOTHER_ONE and its parentheses, ANOTHER_ONE will be called BEFORE ONE has expanded it's arguments, which will render it unexpandable.

Now with that out of the way, let's build our iteration/recursion.

Gluing everything together

Each SCAN adds only a single iteration, but we can nest macros to get exponentially more scans.

#define SCAN(...) SCAN3(SCAN3(SCAN3(__VA_ARGS__)))
#define SCAN3(...) SCAN2(SCAN2(SCAN2(__VA_ARGS__)))
#define SCAN2(...) SCAN1(SCAN1(SCAN1(__VA_ARGS__)))
#define SCAN1(...) SCAN0(SCAN0(SCAN0(__VA_ARGS__)))
#define SCAN0(...) __VA_ARGS__

Since we are going for multiple arguments, SCAN takes multiple arguments.

Now, what do we do with our findings? In the example, there was an annoying ANOTHER_ONE ()() at the end of recursion, how do we remove it? By using __VA_OPT__!

We obviously halt recursion when we run out of arguments, so we can just write the following code:

#define EMPTY
#define FOR_EACH_ONCE(head, ...) debugln(#head, head) __VA_OPT__(, FOR_EACH_RESTART EMPTY ()(__VA_ARGS__))
// if ... is non-empty __VA_OPT__ will continue the iteration by calling FOR_EACH_RESTART
#define FOR_EACH_RESTART() FOR_EACH_ONCE

Note how we also added , inside __VA_OPT__ in case we have more than one argument.

Our FOR_EACH macro:

#define FOR_EACH(...) SCAN(FOR_EACH_ONCE(__VA_ARGS__))

Straightforward, just put our FOR_EACH_ONCE inside our huge SCAN to start the "recursion".

And with dbg macro we have announced earlier we get:

#define dbg(expr, ...) __VA_OPT__(std::make_tuple) (FOR_EACH(expr, __VA_ARGS__))


int main() {
    int a = dbg(5); 
    int b = dbg(a * 2) + 5;
    dbg(a * 2 + 5, b); 
    if dbg(a * 2 + 5 == b) {
        std::cout << "Macro is working!!";
    }
}

Output:

5 = 5
a * 2 = 10
b = 15
a * 2 + 5 = 15
a * 2 + 5 == b = 1
Macro is working!!

Making it prettier

Well, it's working very well apart from cases when there are nested dbg.

int main() {
    int a = dbg(5); 
    int b = dbg(dbg(a * 2) + 5);
}

// the output is:
// 5 = 5
// a * 2 = 10
// (debugln("a * 2", a * 2)) + 5 = 15

You see the last one? It unwraps the statement which is, of course, ugly, and also not how Rust handles this case.

Sadly there is no way to avoid that because of macro rescanning rules.

However, usually, the cases where you need to nest dbg are cases with a single arguments so we can... unwrap first pass of FOR_EACH!

#define dbg(head, ...) __VA_OPT__(std::make_tuple) \
    (SCAN(debugln(#head, head) __VA_OPT__(, FOR_EACH_RESTART EMPTY ()(__VA_ARGS__))))

Dirty? As fuck. But this is C++ macro tutorial, this is nothing compared to the crimes we have committed so far.

Filename and Line

I have omitted the column because I couldn't find a proper way of it working.

This is pretty unnecessary for a competitive programmer, but it's also pretty educational.

The plan is simple: we just prepend "[filename:line]" string literal to #name in our debugln calls.

Fun fact, you can easily append string literals by just writing them next to each other:

constexpr std::string_view a = "Hello," " World";
static_assert(a == "Hello,World"); // Compiles

We can retrieve the current filename and line by using macros __FILE_NAME__ and __LINE__. Since __LINE__ expands into an integer, we have to stringify it, and since C++ macros are C++ macros, we have to do it through two indirections (because of the same macro expansion rules which let us make dbg prettier).

#define TO_STRING(x) TO_STRING2(x) // __LINE__ evaluates to the integer before going into TO_STRING2
#define TO_STRING2(x) #x // and here it becomes an actual string literal (i love c++)
#define FOR_EACH_ONCE(head, ...) debugln("[" __FILE_NAME__ ":" TO_STRING(__LINE__) "] " #head, head) __VA_OPT__(, FOR_EACH_RESTART EMPTY ()(__VA_ARGS__))

int main() {
    int a = dbg(5);
    int b = dbg(dbg(a * 2) + 5);
}
// Output
// [a.cpp:27] 5 = 5
// [a.cpp:28] a * 2 = 10
// [a.cpp:28] dbg(a * 2) + 5 = 15

Closing thoughts

And here what our monumental efforts and macro hacks resulted into:

trigger warning: c++ macros

While it isn't one-to-one implementation of Rust dbg!, it is pretty close. Should you use it? Your choice. However, if you are in onsite competitions, I highly recommend to at least implement dbg for a single argument. It's just:

#ifdef _DEBUG
    #define dbg(x) (debugln(#x, x))
#elif
    #define dbg(x) (x)
#endif

template<typename T>
T debugln(std::string_view str, T&& expr) {
    std::cerr << str << " = " << expr << '\n';
    return expr;
}

And you will already have 90% of the dbg! power.

constexpr version?

We experienced great pain, because we tried to implement Rust macro using mostly C capabilities (apart from __VA_OPT__). But C++ possesses great compile time computation capabilities which enable us to write more coherent code and mostly as performant as our macro trickery.

However, this is out of topic of this blog, if you want to see dbg! done C++ way, let me know :)

  • Проголосовать: нравится
  • +111
  • Проголосовать: не нравится

»
10 часов назад, # |
  Проголосовать: нравится 0 Проголосовать: не нравится

Thank you for this helpful blog!

»
8 часов назад, # |
  Проголосовать: нравится 0 Проголосовать: не нравится

where do you learn all this from? I wanna make my own macros too. preferably some tutorials for the complex cpp stuff you used here.

»
8 часов назад, # |
Rev. 2   Проголосовать: нравится 0 Проголосовать: не нравится

This is useful. Thank you!

Now, my terminal is not showing stderr in red for some reason so I changed this line
std::cerr << "\x1b[31m" << str << " = " << expr << "\x1b[0m" << '\n';
Can this cause problems? and are there any ways to color things printed to stderr?

  • »
    »
    8 часов назад, # ^ |
      Проголосовать: нравится 0 Проголосовать: не нравится

    As far as I am aware, no, this will not cause any problems, ANSI escape codes (what you did) are pretty much the easiest way to create colors.

»
8 часов назад, # |
Rev. 2   Проголосовать: нравится 0 Проголосовать: не нравится

helpful blog, but how do i incorporate this to my already existing dbg file?

#ifdef DEBUG
#define dbg(x...) cerr << "\e[91m"<<__func__<<":"<<__LINE__<<" [" << #x << "] = ["; _print(x); cerr << "\e[39m" << endl;
#else
#define dbg(x...)
#endif

_print(x) supports vectors, pairs etc.

  • »
    »
    7 часов назад, # ^ |
      Проголосовать: нравится 0 Проголосовать: не нравится

    hi, I updated the bigger macro in the spoiler for drop-in replacement (just change the _DEBUG to DEBUG).

    For your case just change your debugln to have your previous dbg implementation like:

    template<typename T>
    T debugln(std::string_view str, T&& expr) {
        std::cerr << "\e[91m" << "[" << str << "] = [";
        _print(expr);
        std::cerr << "\e[39m" << std::endl;
        return expr;
    }