Search code examples
c++preprocessor

Unwanted extra "empty" argument appears when passing in a macro to another macro instead of passing things directly


Problem (concise)

(Full minimal code follows) Somehow, when I pass a simple list of arguments to my macro, everything is fine.

#pragma message STRINGIZE( ( DUMMY_WRAPPER (initial_argument, arg2, arg3)))

During compilation of the above, I see the expected output from the compiler/preprocessor:

#pragma message: ( initial_argument { {initial_argument, arg2} , {initial_argument, arg3} }; 2)

However, when I try to define a macro with the arguments and pass that into DUMMY_WRAPPER, I get an "extra" empty argument. Example:

#define ARGS initial_argument, arg2, arg3
#pragma message STRINGIZE( ( DUMMY_WRAPPER (ARGS) ))

Compiler output (compare to correct output):

#pragma message: ( initial_argument { {initial_argument, arg2} , {initial_argument, arg3} , {initial_argument, } }; 3 )

How do I get rid of this extra argument?

Full code (with explanations)

Nota Bene: I acknowledge this is probably really bad macro abuse, and there are much better ways to do meta-programming with C++, but I'm just trying to get this to work quickly.

I used GCC/G++ for compilation.

Here is working code + compiler output for you to test / experiment with online easily: https://godbolt.org/z/wGFbrK

#define COMMA() ,

// routines for stringizing macros, similar to BOOST_PP_STRINGIZE
#define STR1(x) #x
#define STRINGIZE(x) STR1(x)

// routine & subroutines for argument counting
#define NARG(...)                                                        \
  NARG_(__VA_ARGS__, 5, 4, 3, 2, 1, 0)
#define ARG_N(_1,_2,_3,_4,_5, N, ... ) N
#define NARG_(...) ARG_N(__VA_ARGS__)

// routines for "looped" macro expansion, for processing lists of macro arguments
#define LOOP_1(M, C, D, x) M(C, x)
#define LOOP_2(M, C, D, x, ...) M(C, x) D()                              \
  LOOP_1(M, C, D, __VA_ARGS__)
#define LOOP_3(M, C, D, x, ...) M(C, x) D()                              \
  LOOP_2(M, C, D, __VA_ARGS__)
#define LOOP_4(M, C, D, x, ...) M(C, x) D()                              \
  LOOP_3(M, C, D, __VA_ARGS__)
#define LOOP_5(M, C, D, x, ...) M(C, x) D()                              \
  LOOP_4(M, C, D, __VA_ARGS__)

// routine for concatenating things, used here to expand loop routine names, i.e. LOOP_ + 3 => LOOP_3
#define CAT(a, ...) PRIMITIVE_CAT(a, __VA_ARGS__)
#define PRIMITIVE_CAT(a, ...) a##__VA_ARGS__

// **** TOP-LEVEL ****
// (code using above routines to demonstrate the problem)

// lists the first argument, i.e. C in the loop, and some other argument
#define LIST_FIELD(arg1, a, ... ) {arg1, a}

#define DUMMY_WRAPPER(arg1, ...) DUMMY( arg1, __VA_ARGS__)

#define DUMMY( arg1, ...)                                                   \
    DUMMY_2(arg1, NARG(__VA_ARGS__), __VA_ARGS__)

#define DUMMY_2( arg1, field_count, ...)                                    \
    DUMMY_3(arg1, CAT(LOOP_, field_count), __VA_ARGS__)                     \
    field_count

#define DUMMY_3( arg1, loop, ...)                                           \
    arg1 {                                                                  \
        loop(LIST_FIELD, arg1, COMMA, __VA_ARGS__)   \
    };

#pragma message STRINGIZE( ( DUMMY_WRAPPER (initial_argument, arg2, arg3)))
#define ARGS initial_argument, arg2, arg3
#pragma message STRINGIZE( ( DUMMY_WRAPPER (ARGS) ))

(Some) Research that may be related (not sure)

https://gustedt.wordpress.com/2010/06/08/detect-empty-macro-arguments/

Not sure if this is can explain things here, though, intuitively I think not...


Solution

  • When your compiler sees this: DUMMY_WRAPPER (ARGS)

    It will use ARGS as the first parameter of DUMMY_WRAPPER, even if the expansion of ARGS contains commas.

    This can be solved by removing the first parameter from DUMMY_WRAPPER, and only using __VA_ARGS__:

    #define DUMMY_WRAPPER(...) DUMMY(__VA_ARGS__)
    

    OR by wrapping DUMMY_WRAPPER in an another macro:

    #define DUMMY_WRAPPER_2(...) DUMMY_WRAPPER(__VA_ARGS__)
    

    In fact, your code shouldn't compile at all, since a ... macro parameter must receive at least one argument (which can be empty). DUMMY_WRAPPER(x) is invalid, but DUMMY_WRAPPER(x,) and DUMMY_WRAPPER(x,y) are ok. (This might have been changed in C++20, I'm not sure about it.)

    GCC and Clang refuse to compile the code if you add -pedantic-errors.


    I would also recommend you to use a different approach to looping. The one you're using requires you to generate O(n) code to process n list elements.

    It's possible to do the same thing with O(1) code if you change the macro syntax a bit:

    #include <iostream>
    
    #define STR(...) STR_(__VA_ARGS__)
    #define STR_(...) #__VA_ARGS__
    
    #define END(...) END_(__VA_ARGS__)
    #define END_(...) __VA_ARGS__##_end
    
    #define BODY(x) [x]
    #define BODY_a(x) BODY(x) BODY_b
    #define BODY_b(x) BODY(x) BODY_a
    #define BODY_a_end
    #define BODY_b_end
    
    #define LOOP(seq) END(BODY_a seq)
    
    int main()
    {
        // Prints `[1] [2] [3]`
        std::cout << STR(LOOP( (1)(2)(3) )) << '\n';
    }
    

    Here, LOOP can process any number of elements, without the need for boilerplate macros.

    It's less flexible though, as you can't pass any information to the loop body from outside. But it should be enough for your needs.

    And here's a version that inserts commas between elements:

    #include <iostream>
    
    #define STR(...) STR_(__VA_ARGS__)
    #define STR_(...) #__VA_ARGS__
    
    #define END(...) END_(__VA_ARGS__)
    #define END_(...) __VA_ARGS__##_end
    
    #define BODY(x) [x]
    #define BODY_0(x)   BODY(x) BODY_a
    #define BODY_a(x) , BODY(x) BODY_b
    #define BODY_b(x) , BODY(x) BODY_a
    #define BODY_0_end
    #define BODY_a_end
    #define BODY_b_end
    
    #define LOOP(seq) END(BODY_0 seq)
    
    int main()
    {
        // Prints `[1] , [2] , [3]`
        std::cout << STR(LOOP( (1)(2)(3) )) << '\n';
    }