How to use new C/C++ preprocessor features in Visual Studio (e.g. __VA_OPT__)

I once again experienced the pain of working with C++ variadic macros in Visual Studio. This particular issue was wanting to insert a comma in my macro output after the __VA_ARGS__, but only when variable args are passed. I found a solution in the new C++20 preprocessor API and thought I'd share how it works and how to enable it.

My example macro looks like this:

// Custom error logger that lets a caller supply an error message
// with a printf format and variable args. I want the user message to
// come first and then some OS specific error details.
#define LogOSError(CODE, USER_MSG, ...) { \
    Log(USER_MSG " code: 0x%x, os: %s", __VA_ARGS__, CODE, OSMessage(code)); \
}

LogOSError(123, "Failed to write file.");
// Preprocessor output:
Log("Failed to write file." " code: 0x%x, os: %s", , 123, OSMessage(123));
// The above code won't compile because there's an extra comma where the __VA_ARGS__ is used.

LogOSError(123, "Failed to write file '%s'.", "secrets.txt");
// Preprocessor output:
Log("Failed to write file '%s'." " code: 0x%x, os: %s", "secrets.txt", 123, OSMessage(123));
// This compiles because we have a populated __VA_ARGS__.

// How do we make this work for both cases???

We can make both cases work by using the new __VA_OPT__ preprocessor identifier that's included in the C++20 standard. It's usage looks like this: __VA_OPT__(X), where X will be inserted when __VA_ARGS__ is populated. All we need to do is replace X with a comma character!

My macro now looks like this:

// Include a comma iff we're given a populated __VA_ARGS__.
#define LogOSError(CODE, USER_MSG, ...) { \
    Log(USER_MSG " code: 0x%x, os: %s", __VA_ARGS__ __VA_OPT__(,) CODE, OSMessage(code)); \
}

It turns out that support for this and other new preprocessor things were added to MSVC starting in Visual Studio 2019 v16.5. It's sad that it took until 2021 to get this feature, but whatever, that's the state of software for you.

The Microsoft docs state that you only need to pass the -Zc:preprocessor compiler option to enable the new features, however that wasn't the case for me on Visual Studio 2019 v16.11.13. I also needed to set the C++20 standard mode option -std:c++20. Without the mode you'll see errors saying the __VA_OPT__ identifier doesn't exist. And conversly, setting only the standard mode will result in your __VA_OPT__(X) identifiers being replaced with (X).

Here are the new options I'm passing to cl.exe

-std:c++20 -permissive -Zc:preprocessor -wd5105

The -permissive option ignores all of the warnings about my existing code not adhering to the C++ standard. The disabled 5105 warning is to suppress the macro expansion having undefined behaviour message coming out of the Windows 10 SDK...sigh.

I use the compiler driver directly from a script, so if you're compiling from within Visual Studio then you're on your own. Just look for relevant GUI labels.

That's it! See the full list of changes in v16.5 for more details. And if you enjoy writing macros or seeing the crazy shit people come up with in order to have basic metaprogramming then check out this post on recursive macros with __VA_OPT__.

Published October 19, 2021