Skip to content

Discrepancy in preprocessor behavior between ##__VA_ARGS__ and __VA_OPT__ when variadic argument is a macro that expands to nothing #162609

@lhish

Description

@lhish

Summary

I have encountered a behavioral inconsistency between the ##__VA_ARGS__ preprocessor extension (supported by Clang and GCC) and the C++20 standard __VA_OPT__ feature. The issue arises when a variadic macro argument is itself a macro that expands to an empty token sequence.

It appears that ##__VA_ARGS__ determines whether to remove the preceding comma based on the presence of argument tokens before expansion, while __VA_OPT__ makes its determination based on the result after the argument has been expanded. This leads to different outcomes in a situation where I would expect identical behavior.

My expectation is that in both scenarios, the result should be the same (an empty string in my example), as the effective content of the variadic arguments is empty. I am filing this issue to ask whether this is an intended difference in behavior or a potential bug in the preprocessor implementation.

Code to Reproduce

The following code, based on the setup in my Compiler Explorer session, demonstrates the problem. Note that the macros F(...) and H(...) are defined to isolate and show the differing behaviors of ##__VA_ARGS__ and __VA_OPT__ respectively.

#define F(...) ,##__VA_ARGS__
#define H(...) __VA_OPT__(,)
#define DEFER(x) x EMPTY()
#define str(...) #__VA_ARGS__
#define SELF(x) x
#define EMPTY()
#define VA(...) __VA_ARGS__
#include<iostream>
int main() {
  std::cout<<"VA_ARGS:"<<SELF(DEFER(str)(F(VA())))<<std::endl;
  std::cout<<"VA_OPT:"<<SELF(DEFER(str)(H(VA())))<<std::endl;
}

Link to Compiler Explorer Demonstrating the Issue

Note: The core of this issue lies in the differing expansions of F(VA()) and H(VA()). The other macros (DEFER, str, SELF, etc.) simply form a harness to capture the preprocessor's output and print it as a string for observation.

Observed Behavior

When compiled with Clang, the program produces the following output:

VA_ARGS:,
VA_OPT:
  • The ##__VA_ARGS__ version prints a comma because the preprocessor considers VA() to be a non-empty argument before its expansion.
  • The __VA_OPT__ version prints nothing (an empty string) because it evaluates the arguments after expanding VA() and correctly determines the argument list is empty.

Expected Behavior

I expect both lines to produce the same result, with the output string being empty in both cases. The output should be:

VA_ARGS: 
VA_OPT: 

The rationale is that the semantic intent of both ##__VA_ARGS__ and __VA_OPT__ is to handle cases where variadic arguments are absent. When an argument like VA() is passed, which has no tokens after expansion, it should be treated as an absence of arguments, and the conditional comma should be removed by both mechanisms.

Analysis and Context

As this behavior pertains to ##__VA_ARGS__, which is a non-standard extension, there is no C++ standard definition to reference. However, its purpose has largely been superseded by the standard __VA_OPT__ in C++20. The behavior of __VA_OPT__ (evaluating after expansion) feels more correct and intuitive.

The discrepancy suggests that the check for "emptiness" happens at different stages of preprocessing for these two features. This inconsistency can lead to subtle bugs and makes it difficult to reason about preprocessor behavior, especially when writing code that might rely on such extensions for compatibility with older standards. Is this divergence in logic intended, or could the behavior of the ##__VA_ARGS__ extension be considered for alignment with the more robust, standard-defined logic of __VA_OPT__?

Metadata

Metadata

Assignees

No one assigned

    Labels

    clang:frontendLanguage frontend issues, e.g. anything involving "Sema"

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions