60

I would like to do

template<typename... ArgTypes> void print(ArgTypes... Args)
{
   print(Args)...;
}

And have it be equivalent to this quite bulky recursive chain:

template<typename T, typename... ArgTypes> void print(const T& t, ArgTypes... Args)
{
  print(t);
  print(Args...);
}

followed by explicit single-parameter specializations for every type I'd like to print.

The "problem" with the recursive implementation is that a lot of redundant code is generated, because each recursive step results in a new function of N-1 arguments, whereas the code I'd like to have would only generate code for a single N-arg print function, and have at most N specialized print functions.

rubenvb
  • 74,642
  • 33
  • 187
  • 332

3 Answers3

92

C++17 fold expression

(f(args), ...);

If you call something that might return an object with overloaded comma operator use:

((void)f(args), ...);

Pre-C++17 solution

The typical approach here is to use a dumb list-initializer and do the expansion inside it:

{ print(Args)... }

Order of evaluation is guaranteed left-to-right in curly initialisers.

But print returns void so we need to work around that. Let's make it an int then.

{ (print(Args), 0)... }

This won't work as a statement directly, though. We need to give it a type.

using expand_type = int[];
expand_type{ (print(Args), 0)... };

This works as long as there is always one element in the Args pack. Zero-sized arrays are not valid, but we can work around that by making it always have at least one element.

expand_type{ 0, (print(Args), 0)... };

We can make this pattern reusable with a macro.

namespace so {
    using expand_type = int[];
}

#define SO_EXPAND_SIDE_EFFECTS(PATTERN) ::so::expand_type{ 0, ((PATTERN), 0)... }

// usage
SO_EXPAND_SIDE_EFFECTS(print(Args));

However, making this reusable requires a bit more attention to some details. We don't want overloaded comma operators to be used here. Comma cannot be overloaded with one of the arguments void, so let's take advantage of that.

#define SO_EXPAND_SIDE_EFFECTS(PATTERN) \
        ::so::expand_type{ 0, ((PATTERN), void(), 0)... }

If you are paranoid afraid of the compiler allocating large arrays of zeros for naught, you can use some other type that can be list-initialised like that but stores nothing.

namespace so {
    struct expand_type {
        template <typename... T>
        expand_type(T&&...) {}
    };
}
Benjamin Buch
  • 4,752
  • 7
  • 28
  • 51
R. Martinho Fernandes
  • 228,013
  • 71
  • 433
  • 510
  • 1
    "but stores nothing" -- The compiler will likely allocate the same amount of space for that, since arguments need space too (and maybe more, for callstack info etc). And if you speculate on that getting optimized out, I think the array storage is just as-likely to be optimized out. – Xeo Jun 27 '13 at 10:02
  • 13
    It's funny how a more advanced C++ will inevitably lead to more advanced hacks to work around things not possible in a clear and concise way. Thanks for writing this up! – rubenvb Jun 27 '13 at 12:24
  • 4
    absolutely brilliant answer :) – Gabriel Oct 02 '15 at 17:17
  • 1
    Would it also be possible to just call an empty function ``namespace so { template expand_type(T&&...) {} }`` ? (or is there some potential dangerous side effect because of the compilere optimization?) – Gabriel Oct 02 '15 at 17:43
  • 7
    A function call doesn't guarantee evaluation order. `{}`-initialization does. – R. Martinho Fernandes Oct 02 '15 at 17:44
  • 1
    C++17 fold expression: `(print(Args), ...);` Keep it simple ;-) – Benjamin Buch Aug 03 '17 at 11:37
24

C++17 fold expression:

(f(args), ...);

Keep simple things simple ;-)

If you call something that might return an object with overloaded comma operator use:

((void)f(args), ...);
Benjamin Buch
  • 4,752
  • 7
  • 28
  • 51
7

You can use even more simple and readable approach

template<typename... ArgTypes> void print(ArgTypes... Args)
{
   for (const auto& arg : {Args...})
   {
      print(arg);
   }
}

I have played with both variants on compile explorer and both gcc and clang with O3 or O2 produce exactly the same code but my variant is obviously cleaner.

Anton Dyachenko
  • 169
  • 1
  • 9
  • 1
    Does this not take a copy regardless of what was passed in? Couldn't this be quite deadly when `print`ing a huge object? – rubenvb Jun 17 '16 at 09:38
  • 1
    Als, your link goes to an unrelated snippet. – rubenvb Jun 17 '16 at 10:00
  • 2
    While this does indeed look Pretty... pretty, you should probably write something like [...]`using value_type = std::common_type_t; for (auto const &arg : {static_cast(Args)...})`[...] in order to allow it to work with heterogeneous parameter packs. Also @rubenvb I believe your concerns are regarding the std::initializer_list<> being copy-initialized? – mbw Aug 01 '16 at 19:17
  • 1
    That is of course `std::common_type_t`. – mbw Aug 01 '16 at 19:27
  • Does this work if there are no arguments? – Magnar Myrtveit Sep 24 '21 at 10:58
  • no, but it is trivial to add an overload void print(){} that does nothing. And that is what I did in my project. – Anton Dyachenko Sep 27 '21 at 03:27