Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C++11 initializer lists on variadic template's parameters: why isn't this working

Enclosing a variadic template's parameters in initializer lists should assure that they're evaluated in order, but isn't happening here:

#include <iostream>
using namespace std;


template<class T> void some_function(T var)
{
   cout << var << endl;
}

struct expand_aux {
    template<typename... Args> expand_aux(Args&&...) { }
};

template<typename... Args> inline void expand(Args&&... args) 
{
   bool b[] = {(some_function(std::forward<Args>(args)),true)...}; // This output is 42, "true", false and is correct
   cout << "other output" << endl;
   expand_aux  temp3 { (some_function(std::forward<Args>(args)),true)...  }; // This output isn't correct, it is false, "true", 42
}

int main()
{
   expand(42, "true", false);

   return 0;
}

How come?

like image 209
Paul Avatar asked Apr 15 '13 14:04

Paul


2 Answers

This seems to be a bug. The output should be the one you are expecting.

While there is no guarantee on the order of evaluation of the arguments of a constructor call in general, there is a guarantee on the order of evaluation of expressions in an braced initializer list.

Per Paragraph 8.5.4/4 of the C++11 Standard:

Within the initializer-list of a braced-init-list, the initializer-clauses, including any that result from pack expansions (14.5.3), 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 ]

like image 72
Andy Prowl Avatar answered Oct 17 '22 07:10

Andy Prowl


As noted, your problem is a compiler bug. Your code, as written, should evaluate its arguments in order.

My advice would be to be explicit about what you want to do, and what order you are doing it in, and avoid abusing the comma operator (as an aside, your code could behave strangely if some_function returned a type that overrides operator,) or using initializer lists guarantees (which, while standard, are also relatively obscure).

My go to solution is to write and then use do_in_order:

// do nothing in order means do nothing:
void do_in_order() {}
// do the first passed in nullary object, then the rest, in order:
template<typename F0, typename... Fs>
void do_in_order(F0&& f0, Fs&&... fs) {
  std::forward<F0>(f0)();
  do_in_order( std::forward<Fs>(fs)... );
}

which you use like this:

do_in_order( [&]{ some_function(std::forward<Args>(args)); }... );

you wrap the action you want to do in an anonymous nullary full-capture lambda, then use ... to create a whole set of instances of said lambdas and pass to do_in_order, which calls them in order via perfect forwarding.

This should be easy for a compiler to inline and reduce to a sequence of calls. And it says what it does directly, and doesn't require strange void casts, the use of the comma operator, or arrays whose value is discarded.

like image 36
Yakk - Adam Nevraumont Avatar answered Oct 17 '22 08:10

Yakk - Adam Nevraumont