More About Variadic Templates

I gave an introduction to variadic templates last week. Today I will talk about some more features that have or will be added in that area in C++11, 14 and 17.

The sizeof… operator

The sizeof... operator is a special form of pack expansion. It simply returns the number of pack elements and works on both template parameter packs and function parameter packs:

template <class... Ts>
void printCount(Ts... args) {
  std::cout 
    << sizeof...(Ts) << ' '
    << sizeof...(args) << '\n';
}

// prints "3 3\n"
printCount(22, std::optional{0}, "!");

Note that, like sizeof, the sizeof... operator returns a compile time constant.

Variadic function templates: working on every argument

There are basically two ways to work on function parameter packs: work on everything at once using pack expansion, and recursively calling the same function, chewing off one argument at a time.

Pack expansion tricks

Sometimes we just want to call a function for each argument. However, pack expansion works only in places where comma separated lists are allowed. This is not a comma separated list, obviously:

  doSomething(arg1);
  doSomething(arg2);
  ...
  doSomething(argN);

So it’s not surprising that this won’t compile:

template <class... Args>
void doSomethingForAll(Args... const& args) {
  doSomething(args)...;
}

Luckily, we have std::initializer_list, so often it can be sufficient to use them as the place for the expansion:

template <class... Args>
void doSomethingForAll(Args... const& args) {
  auto x = {doSomething(args)...};
}

This will make x to be an std::initializer_list of whatever doSomething returns. However, since that might be void or a mix of different types, it won’t always compile. A trick then is to create an expression as the expansion pattern that calls the function but has always the same non-void type. Using the comma operator, this is easy:

template <class... Args>
void doSomethingForAll(Args... const& args) {
  auto x = {(doSomething(args),0)...};
}

Now, the function calls are just a side effect, and the result will be a std::initializer_list<int> filled with zeros. The compiler will warn about the unused x, but since we now know the type of the list and the fact that it’s unused, we can cast it to void to silence the warning. We can do the same in case a static analyzer eagerly warns about the unused doSomething returns or worse, if the doSomething return type has overloaded operator,:

template <class... Args>
void doSomethingForAll(Args... const& args) {
  (void)std::initializer_list<int>{
    ((void)doSomething(args),0)...
  };
}

One argument at a time

Suppose we want to print all the arguments of our function, separated by commas, to cout. We could use the above trick, with doSomething being a function that prints the value plus a comma. The problem then is with the last argument which should not be followed by a comma, but doSomething won’t be able to detect that. The straight forward approach is recursion:

template <class Arg>
void print(Arg const& arg) {
  std::cout << arg;
}

template <class Head, class... Tail>
void print(Head const& head, Tail const&... tail){
  std::cout << head << ", ";
  print(tail...);
}

Whenever we call print with more than one parameter, the first one will be printed and the rest will be passed to print – if that rest or our initial call have just one argument, the non-variadic overload kicks in and the recursion is ended.

With C++17 we have constexpr if and can reduce this function a bit:

template <class Head, class... Tail>
void print(Head const& head, Tail const&... tail){
  std::cout << head;
  if constexpr(sizeof...(tail) > 0) {
    std::cout << ", ";
    print(tail...);
  }
}

Here, the body of the if will only be compiled if tail contains at least one element. Without constexpr if this would result in a compile error since the compiler would not find the appropriate print function for a call with 0 arguments.

As always, any recursion can be converted into an iteration – which for variadic templates is pack expansion:

template <class Head, class... Tail>
void print1(Head const& head, Tail const&... tail){
  std::cout << head;
  (void)std::initializer_list<int>{((std::cout<<", "<<tail),0)...};
}

Nested packs expansion

I already had written about the simultaneous expansion of multiple packs, if they appear in the same expansion pattern. Something that might look similar at first sight is the expansion of nested packs: We can have a pack expansion pattern as part of another pack expansion pattern.

In such a case, the innermost pattern is expanded first, including simultaneous expansion of all the contained packs. Then the resulting outer pattern containing the expanded inner pattern is expanded and so on.

template <class T, class... Args>
auto pairWithRest(T const& t, Args const&... args) {
    return std::make_tuple(std::make_pair(t, args)...);
}

template <class... Args>
auto selfCartesianProduct(Args const&... args) {
    return std::tuple_cat(pairWithRest(args, args...)...);
}

auto cp = selfCartesianProduct(1, "!", 5.0);

In this example, pairWithRest is a simple variadic template function with a normal pack expansion of std::make_pair(t, args).... It returns a tuple of pairs. The interesting part is the call of that function in the selfCartesianProduct function: pairWithRest(args, args...)....

Here, the inner pattern is simply args.... During the example call, this gets expanded to 1, "!", 5.0, obviously. The outer pattern after that is pairWithRest(args, 1, "!", 5.0)..., which then gets expanded to pairWithRest(1, 1, "!", 5.0), pairWithRest("!", 1, "!", 5.0"), pairWithRest(5.0, 1, "!", 5.0).

This results in three tuples of pairs which then get concatenated via tuple_cat.

Fold expressions

With C++17 we get a nice new feature for function parameter packs. Imagine if you wanted to concatenate an expanded pattern not by a comma separated list but by using an operator. That’s what C++17’s fold expressions are for:

template <class... Args>
bool containsZero(Args const&... args) {
  return ((args == 0) || ...);
}

Here, the return expression is equivalent to ((args1 == 0) || (args2 == 0) || ... || (argsN == 0)). We can use a lot of binary operators in fold expressions, and they come in slightly different variants:

  • Unary right fold: (args + ...) is equivalent to (args1 + (args2 + ( ... + argsN))). If args is an empty pack, this is ill-formed for any operators but ||, && and , which will yield false, true and void(), respectively.
  • Binary right fold: (args * ... * X) is equivalent to (args1 * ( ... * (argsN * X)), where X is some expression that is not a parameter pack. If args is empty, this evaluates to X.
  • Unary left fold: (... | args) is equivalent to (((args1 | args 2) | ... ) | argsN), i.e. like unary right fold, but with left association. The restrictions of unary right fold apply.
  • Binary left fold: (X > ... > args) is equivalent to (((X > args1) > ... ) > argsN), obviously. (Yes, this will seldomly make sense…)

Variadic variadic template template parameters

No, I am not stuttering. I am presenting this only as a treat and won’t go too deep into it. We have template template parameters, i.e. template parameters that are themselves templates:

template <template <class A, class B> class Container>
Container<int, double> f(int i, double d) {
  return container<int, double>{i,d};
}

f<std::pair>(1, 2.3); //returns std::pair<int, double>
f<std::tuple>(1, 2.3);//returns std::tuple<int, double>

Of course, we can have variadic templates where the parameters are templates:

template <template <class A, class B> class... Containers> //...

Or templates, where the parameters are variadic templates:

template <template <class... As> class X> //...

Well, we can combine the two!

template<class F, template<class...> class... T>
struct eval { 
    F const& fun;
    eval(F const& f) : fun(f) {}
    auto operator()(T<int, double> const&... t) {
        return fun(t...);
    }
};

using PID = std::pair<int, double>;
using TID = std::tuple<int, double>;

int f(PID const&, TID const&) { return 22; }

int main() {
  eval<decltype(f), std::pair, std::tuple> x(f);
  auto xret = x(std::make_pair(22, 34.7), std::make_tuple(11, 47.11));
  std::cout << xret << '\n';
}

I think that should be enough for today 😉

Facebooktwittergoogle_plusredditlinkedinFacebooktwittergoogle_plusredditlinkedinby feather

10 Comments

  1. Niels

    I am a bit puzzled by the the first example of the “Pack expansion tricks” section. As I see it, this expansion:

    doSomething(args)…;

    ought to expand into a comma separated list of function calls, which is valid:

    doSomething(arg1), doSomething(arg2), doSomething(arg3);

    Of course I am missing something, but what?

    Thank you for some interesting posts!

    Reply
    1. Arne Mertz

      Hi Niels, thanks for asking!
      The problem is that pack expansion is only allowed in certain locations, e.g. initializer lists, braced initializers, template parameter lists, function argument lists, lists of base classes,… – basically the special case of sizeof... and anything that already is a list context.

      This is, however, not the case for a simple statement in a function, so, even though a comma separated list of function calls would happen to compile (because we have the comma operator), it is not allowed as a pack expansion location.

      Reply

  2. Ernst

    Hello Arne

    thank you very much for one more nice post!

    just a question:

    template  void doSomething(T t) {
        std::cout << t << '\n';
    }
    
    template  void doSomethingForAll(Args... a) {
        (void)std::initializer_list{(doSomething(a),0)...};
    }
    

    in this sample from your post, can doSomething be replaced with a lambda? if – yes, please example for a valid syntax.

    Reply
    1. minirop

      I would say “search&replace” so: ([](Args a) {}, 0)...

      Reply
      1. minirop

        silly me. just got a compiler and tried, I ended up with: ([](auto x){ print(x); }(args), 0)...)

        Reply
        1. Arne Mertz

          Yup, that’s like it. I’d probably use a named lambda to improve readability. The expansion then remaisn the same:

          template  void doSomethingForAll(Args... a) {
             auto doSomething = [](auto t){ std::cout << t << '\n'; };
              (void)std::initializer_list{(doSomething(a),0)...};
          }
          
          Reply
        2. Ernst

          the notation is not enough clear, could you explain how it’s recognized (by compiler) :
          [](auto x){ print(x); }(args)

          Reply
          1. Arne Mertz

            First: [](auto x){ print(x); } is recognized as a lambda. Since it is not assigned to anything this expression creates a temporary lambda object or closure object, to use the right name.
            Second: <temporary_closure_object>(args) is then recognized as a use of the function call operator on that temporary closure object.

            Does that answer the question?

          2. Ernst

            yes, thank you.
            just didn’t face with such usage of lambda earlier
            (args)

Leave a Reply

Your email address will not be published. Required fields are marked *