Search code examples
c++11templatesparameter-pack

What is the simplest way to process the values of a template parameter pack in the correct order without using fold-expressions (C++11)


I would like to back-port the following code to C++11:

template<unsigned i>
static void bar() { /* some code with compile-time optimizations for each value i */ }

template <unsigned... I>
void f()
{
  ((bar<I>()),...);
}

The order of invocation of 'bar' for each value of the parameter pack I is important - which I believe is working in the C++17 implementation above because the fold-expression is using the comma operator.

I initially went for an explicitly recursive implementation:

template <unsigned... I>
typename std::enable_if<sizeof...(I) == 0>::type g() {}

template <unsigned head, unsigned... I>
void g()
{
  bar<head>();
  g<I...>();
}

Which seems to work but requires two implementations for g(). While trying to get down to a single function, I read that the pack expansion would happen in make_tuple and thought that this would work:

template<unsigned... I>
static void h1()
{
    std::make_tuple( (bar<I>(), 0)... );
}

unfortunately, this won't provide any guaranty on the order of execution - in fact with gcc the execution order is exactly the inverse. Alternatively, I could use a braced-init-list:

template<unsigned... I>
static void h2()
{
  using expand = int[];
  expand{ 0, ( bar<I>(), 0) ... };
}

This seems to preserve the order with gcc but I couldn't really figure out if this is only a coincidence.

So, the specific questions are:

  • will the implementation of h2 guaranty the correct execution order?
  • are there alternative implementations that I missed?

Solution

  • You are correct that your h1 does not guarantee evaluation order in any version of C++, since initializations of function parameters are indeterminately sequenced. Your h2 does guarantee evaluation order in C++11 and later. And in fact, any method which puts the calls as elements of a { braced list } will guarantee it.

    All versions of C++ since C++11 contain the paragraph [dcl.init.list]/4:

    Within the initializer-list of a braced-init-list, the initializer-clauses, including any that result from pack expansions ([temp.variadic]), are evaluated in the order in which they appear. That is, every value computation and side effect associated with a given initializer-clause is sequenced before every value computation and side effect associated with any initializer-clause that follows it in the comma-separated list of the initializer-list. [Note: This evaluation ordering holds regardless of the semantics of the initialization; for example, it applies when the elements of the initializer-list are interpreted as arguments of a constructor call, even though ordinarily there are no sequencing constraints on the arguments of a call. – end note]