Don’t pass lambdas (or other multi-line entities) as parameters to macros

Consider this macro:

#ifdef DEBUG
#define LOG(value) LogValue(value)
// In production, evaluate but don't log.
#define LOG(value) (value)

This seems not entirely unreasonable, but bad things happen if you pass a multi-line entity as the macro parameter.

// Suppose this is line 12
LOG(zap_if([&](auto&& item)
 ... decide whether to zap this item ...

You will never to be able to debug this lambda, because the preprocessor slurps up the entire macro parameter list, and then spits the text back out when the macro is expanded.

But when it spits the text out, it spits it all into one giant line of code. The result is as if you had written

LogValue(zap_if([&](auto&& item) { ... decide whether to zap this item ... });

Therefore, all compiler errors in the ... decide whether to zap this item ... are reported on line 12. Your IDE will just send you to that line and tell you, "Good luck!"

But wait, the pain doesn't stop there.

Even it you manage to fix the compiler errors in the code, you won't be able to debug it. You'll put the cursor inside the lambda on line 14 and tell the debugger, "Set a breakpoint here," and the debugger will say, "There is no code here for me to set a breakpoint on." Because there really is no code there. All the code you wrote on line 14 got mashed into the huge line of regurgitated text back on line 12.

To make your life sane, don't pass lambdas or other multi-line entities as macro parameters. Break the lambda out into a place where it won't be part of a macro.

auto result = zap_if([&](auto&& item)
 ... decide whether to zap this item ...


Bonus chatter: The C++ Core Guidelines recommends

Scream when you see a macro that isn't just used for source control (e.g., #ifdef)

On the other hand, you may be forced into using macros that you didn't write, such as the SUCCEEDED macro provided by COM.

Comments (6)
  1. Henke37 says:

    Seems more like a tool limitation than an issue with macros per see. Does the precompiler forget how to attribute each part of the logical line to the matching physical line?

    Of course, macros are still evil.

    1. Darran Rowe says:

      The pre-processor seems to just strip newlines from the parameter in the macro. So if you have a macro:
      #define LOG(value) (value)
      and you use it in a multi line way:
      LOG([]() {
      int a = 1;
      If you preprocess this and check the compiler output, it will be:
      ([]() { int a = 1; });
      I can’t remember off of the top of my head where this is mentioned in the standard, or if it is, but considering Raymond wrote this article, you would assume that he verified that this is a standards thing rather than a tooling issue. He didn’t restrict this to just VC after all.

    2. True. This is a quality of implementation issue. The standard does not dictate the format of diagnostics, nor does it require the existence of a debugger (much less mandate how the debugger behaves).

    3. 'k says:

      >Does the precompiler forget how to attribute each part of the logical line to the matching physical line?

      I don’t think the preprocessor steps were clarified in the 1978 edition of “The C programming language”. In later editions (eg 1988) Brian W. Kernighan and Dennis M. Ritchie did include the following

      Under A . 12 Preprocessing

      “Lines that end with the backslash character \ are folded by deleting the backslash and the following newline character. This occurs before division into tokens

      Might have been after the ANSI C standard was ratified…

  2. sukru-t says:

    Seems like CLang / Clion combo is a bit less confused on this issue. It would still make some mistakes, but for most basic cases it would work fine.

    I would assume VSCode would also benefit from the Clang services as well.

    1. At the very least I seem to recall that clang errors include the locations of all the macros that were expanded as part of that source line, just in case the error relates to the use of one of them.

Comments are closed.

Skip to main content