12

I need to remove the last trailing comma from a list of macro arguments (because they will be eventually expanded into template arguments where the trailing comma is not admitted).

So I need a macro remove_trailing_comma() which called like remove_trailing_comma(arg1, arg2, arg3, ) expands to arg1, arg2, arg3.

I've tried with different combinations of varargs and __VA_OPT__ but I seem to not be able to do it.

For example:

#define discard_trailing_comma(arg, ...) \
    arg __VA_OPT__(,) discard_trailing_comma(__VA_ARGS__)

discard_trailing_comma(1, 2, 3, )

does not work (with g++ 10) because expands to 1 , discard_trailing_comma(2, 3,), I do not know why (are macro not expanded recursively?)

Is this possible in C++20?

gigabytes
  • 3,104
  • 19
  • 35
  • You should be using templates and parameter packs in C++. – Goswin von Brederlow Jun 07 '22 at 15:18
  • 3
    The question is intentionally narrow, but my use case is not one I can solve with parameter packs. – gigabytes Jun 07 '22 at 15:19
  • Is it possible for you to fix the problem where the trailing comma is introduced? I cannot imagine of a way to remove it afterwards, because the empty argument is still an argument (meaning `__VA_ARGS__` is not empty) and will cause `__VA_OPT__` to emit its argument. – UniversE Jun 07 '22 at 15:26
  • is it always 3 arguments and the trailing `,` ? – 463035818_is_not_an_ai Jun 07 '22 at 15:31
  • no it is not always 3 arguments, that's just an example :) – gigabytes Jun 07 '22 at 15:32
  • somewhat related: https://stackoverflow.com/q/24009797/4117728 – 463035818_is_not_an_ai Jun 07 '22 at 15:34
  • how about adding a dummy argument (instead of removing the `,`) and adjust the template to ignore the last parameter ;) – 463035818_is_not_an_ai Jun 07 '22 at 15:37
  • I cannot touch the template nor do I have a dummy value (because I'm generic in the type of the values that are passed to the macro) – gigabytes Jun 07 '22 at 15:43
  • How many arguments can you have at maximum? Or is it unbounded? – Eljay Jun 07 '22 at 15:43
  • You can always use a private class as dummy element and strip it via template. – Goswin von Brederlow Jun 07 '22 at 16:16
  • Assuming all macros are actually named `discard_trailing_comma`, isn't this problem better suited by a find-and-replace script that gets run as part of the build process? I admire trying to keep the solution native C++, but I have a feeling the solution would be more complex. – jdc Jun 07 '22 at 16:31
  • Macros cannot be expanded recursively, no. – CoffeeTableEspresso Jun 07 '22 at 17:25
  • I don't know if there is a solution or not, but in both cases, I'm sure this a perfect scenario to have future developer being "really thankful" for such a thing. Sarcasm was on. Fix this at the root is the right way to make it clear. – Federico Jun 07 '22 at 17:27
  • I would solve this at template level. Add a `void` at the end, then strip it with templates (which is annoying to do, but less annoying than doing it with the preprocessor). – HolyBlackCat Jun 18 '22 at 21:25

3 Answers3

10

You'll probably should go with @eljay's answer, but if you need to support way more arguments, here is one that supports ~2000 arguments in 22 lines and adding more lines grows that number exponentially.

#define E4(...) E3(E3(E3(E3(E3(E3(E3(E3(E3(E3(__VA_ARGS__))))))))))
#define E3(...) E2(E2(E2(E2(E2(E2(E2(E2(E2(E2(__VA_ARGS__))))))))))
#define E2(...) E1(E1(E1(E1(E1(E1(E1(E1(E1(E1(__VA_ARGS__))))))))))
#define E1(...) __VA_ARGS__

#define EMPTY()
#define TUPLE_AT_2(x,y,...) y
#define TUPLE_TAIL(x,...) __VA_ARGS__

#define CHECK(...) TUPLE_AT_2(__VA_ARGS__,0,)
#define EQ_END_END ,1

#define SCAN(...) __VA_ARGS__
#define CAT(a,b) CAT_(a,b)
#define CAT_(a,b) a##b

#define LOOP_() LOOP
#define LOOP(x,y,...) CAT(LOOP, CHECK(EQ_END_##y))(x,y,__VA_ARGS__)
#define LOOP1(x,...) (TUPLE_TAIL x)
#define LOOP0(x,y,...) LOOP_ EMPTY() ()((SCAN x, y),__VA_ARGS__)

#define DTC(...) E4(LOOP((),    __VA_ARGS__ END))

DTC (1, 2, 3, 4, 5, 6, 7, 8, 9,) // expands to: (1, 2, 3, 4, 5, 6, 7, 8, 9)

Let me try to explain this.

Firstly, when LOOP is called inside E4() it can recursively call itself, which is done in LOOP0. The simplest example for this would be #define LOOP(...) __VA_ARGS__ LOOP_ EMPTY() ()(__VA_ARGS__) which repeats the argument until the recursion limit, which is bound by the nesting of E4. Understanding DEFER and OBSTRUCT macros already explains the behavior quite well, so I'll skip that part of the explanation.

Now the idea is the following: We loop over every argument until we reach the last one, where we've inserted an END mark. Whilst doing that, we construct a new argument list, but this also stops when we reach the END mark.

CAT(LOOP, CHECK(EQ_END_##y)) branches to LOOP1 if the argument y holds the end mark END and otherwise to LOOP0.

LOOP1 appends a new argument to our argument list in x using (SCAN x, y). Since we start off with an empty argument list, we'll end up with a leading empty argument, which we can trivially remove on LOOP0.

PS: This concept can be trivially extended to E5, E6, ..., although there is a larger overhead using that because once the recursion ends the preprocessor still needs to rescan the result till the recursion limit. If you want to remedy that, you can use something like the continuation machine from order-pp, which can actually terminate, but it's about 150 loc.

Edit, I just revisited this and realized that using x to build up the tuple is quite inefficient, here is a version that doesn't do that:

#define E4(...) E3(E3(E3(E3(E3(E3(E3(E3(E3(E3(__VA_ARGS__))))))))))
#define E3(...) E2(E2(E2(E2(E2(E2(E2(E2(E2(E2(__VA_ARGS__))))))))))
#define E2(...) E1(E1(E1(E1(E1(E1(E1(E1(E1(E1(__VA_ARGS__))))))))))
#define E1(...) __VA_ARGS__

#define EMPTY()
#define FX(f,x) f(x)
#define TUPLE_AT_2(x,y,...) y
#define TUPLE_TAIL(x,...) __VA_ARGS__

#define CHECK(...) TUPLE_AT_2(__VA_ARGS__,0,)
#define EQ_END_END ,1

#define CAT(a,b) CAT_(a,b)
#define CAT_(a,b) a##b

#define LOOP_() LOOP
#define LOOP(x,...) CAT(LOOP, CHECK(EQ_END_##x))(x,__VA_ARGS__)
#define LOOP1(x,...) 
#define LOOP0(x,...) LOOP_ EMPTY() ()(__VA_ARGS__),x

#define DTC(...) (FX(TUPLE_TAIL,E4(LOOP(__VA_ARGS__ END))))

DTC (1, 2, 3, 4, 5, 6, 7, 8, 9,) // expands to: (1, 2, 3, 4, 5, 6, 7, 8, 9)
camel-cdr
  • 643
  • 4
  • 8
  • 1
    I already +1 this amazing answer. That's some powerful Preprocessor-fu demonstrated! (I still hate the preprocessor with all my heart, regardless I can appreciate the expertise necessary to implement this solution.) – Eljay Jun 08 '22 at 14:29
8

Assuming you have a bounded number of parameters, you could make a delegate function-like macro as a helper to choose the appropriate implementation macro.

#define GET_DTC(_1,_2,_3,_4,NAME,...) NAME
#define DTC4(_1,_2,_3,_4) _1,_2,_3
#define DTC3(_1,_2,_3) _1,_2
#define DTC2(_1,_2) _1
#define DTC1(_1)
#define discard_trailing_comma(...) \
    GET_DTC(__VA_ARGS__, DTC4, DTC3, DTC2, DTC1)(__VA_ARGS__)

int arr[] = {
discard_trailing_comma(1, 2, 3, )
};
Eljay
  • 4,648
  • 3
  • 16
  • 27
1

There are already good answers here for removing a trailing comma, but they require quite a lot of boilerplate pre-processor code.

However, from the example code, my guess would be that the list from which it is desired to remove the trailing comma is itself the result of pre-processor macros expansion (use of "X macro" C pre-processor coding pattern maybe?).

In this case, it seems the code that generates the list contents could be modified to have a leading comma instead of a trailing one, which is much simpler to remove:

#define REMOVE_FIRST(X, ...) __VA_ARGS__
const int some_const_array[] = { REMOVE_FIRST(,1,2,3) };
// -> expands to just 1,2,3

With the actual contents of the initializer list coming from X-macro expansion:

#define REMOVE_FIRST(X, ...) __VA_ARGS__
// Indirection macro required to perform expansion of macro parameters
#define EXPAND_AND_REMOVE_FIRST(...) REMOVE_FIRST(__VA_ARGS__)

#define SOME_COLLECTION_DEF(X) \
  X(item1, 1, whatever) \
  X(item2, 2, one_wants_to) \
  X(item3, 3, include_in_here)

#define INITIALIZER_LIST_VALUE(name, value, other) ,value

const int values[] = {
  EXPAND_AND_REMOVE_FIRST(
    SOME_COLLECTION_DEF(INITIALIZER_LIST_VALUE) )
  // SOME_COLLECTION_DEF(INITIALIZER_LIST_VALUE) expands to ,1 ,2 ,3
  // and first comma is removed by EXPAND_AND_REMOVE_FIRST
  // resulting in just 1 ,2 ,3
};
Shadocko
  • 1,186
  • 9
  • 27