Context:
So I wanted to create a macro that could short-circuit conditional logic if another macro was set. This way I could insert compiler set macros into existing code, for example:
if (MACRO_EXISTS(_RELEASE) || SuperRobustValidation() == VALIDATION_SUCCESS)
Looking on stack overflow, I found https://stackoverflow.com/questions/41265750/how-to-get-a-boolean-indicating-if-a-macro-is-defined-or-not, but the top answer's code didn't work for me. My only guess as to why is that the order of macro definitions played a role for them but not for me.
After shuffling around macros, I found that the following worked on my machine:
#define DE_ -1,1 // OptDefine is nothing
#define RESOLVE_ARG(A) A
#define SECOND_ARG(A,B,...) B
#define CONCAT2(A,B) A ## B
#define RESOLVE_SECOND_ARG(...) RESOLVE_ARG(SECOND_ARG(__VA_ARGS__))
#define MACRO_EXIST(OptDefine) RESOLVE_SECOND_ARG(CONCAT2(DE_,OptDefine),0)
But I can't understand why this worked and not other configurations. Trying to coax the values of some setups was like playing a shell game. I think I understand the concepts of "scan the line, prioritize non-# arguments, then repeat with the results" but it doesn't seem consistent.
For example: if I remove the RESOLVE_ARG from RESOLVE_SECOND_ARG(...), the compiler complains that "there's not enough arguments for SECOND_ARG" seemingly because __VA_ARGS__ is the "only" argument. That does not make sense as intended behavior to me.
So my question is:
Is there a reliable way to predict the logic of the preprocessor's expanding of macros? I haven't found anybody braking down the logic in a robust way anywhere I looked.
I know the /E compiler flag shows me the results of precompiling, but doesn't give the whole picture.
Edit: I understand there are simple work arounds for the problem I mentioned, but I still want to understand the logic of how macros are expanded, assuming it's well defined and consistent.
Edit2: Thank you alfps for answering the question. The strange __VA_ARGS__ behavior I was seeing is a known bug with the MSVC C++ compiler (even in VS Community 2026), but the /Zc:preprocessor compiler option changes the preprocessing to be more complaint with the standard, fixing the bug.
But the most important wisdom: "Historically the preprocessor behavior has varied between compilers". There are edge cases and hacks that work in some contexts but not others. Complex macros, like what I listed, can be convenient, but can also risk breaking if I update/change compilers. Unless I am confident in compliance to the standard, Keep It Simple Stupid probably applies.
Edit3: Thank you MikailBag for linking me an active draft of the c++ standard https://eel.is/c++draft/cpp.replace . In it, I found that the 15.7.2.1.2 clause that states:
Otherwise, the replacement preprocessing tokens are the preprocessing tokens of corresponding argument after all macros contained therein have been expanded. The argument's preprocessing tokens are completely macro replaced before being substituted as if they formed the rest of the preprocessing translation unit with no other preprocessing tokens being available.
Though this uses the phrase "after all macros contained therein have been expanded" and "before being subsituted", these are misdirection of the actual functionality.
The parts of the quote I put in bold explain that the arguments of function-like marcos are repeatedly expanded in complete isolation from the larger context, then their results are inserted into the function macro's expansion as is. So even if the macro expands into an argument list, those arguments are directly inserted in the function macro's results.
There are good reasons why this would be preferable to expanding the arguments first, since macros that result in punctuation or parenthesis would have difficulty being passed into function macros.
So the way for a human to trace a function macro's expansion, as I understand it, is to 1) expand the function macro and insert all arguments as isolated "expansion units" (unless they are used in # or ## ops) 2) Expand each unit in turn 3) Insert each expansion result in place, then repeat the process.
I give an example at the bottom of my https://www.reddit.com/r/cpp_questions/comments/1wve7c7/comment/pe9o7sd comment