16
template <class F, class... Args> 
void for_each_argument(F f, Args&&... args) { 
    [](...){}((f(std::forward<Args>(args)), 0)...); 
}

It was recently featured on isocpp.org without explanation.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
NoSenseEtAl
  • 28,205
  • 28
  • 128
  • 277
  • Analogous to variadic arguments. – nullpotent Jan 23 '15 at 13:13
  • 1
    For a similar construct that evaluates all arguments in the order given, see [this question of mine](http://stackoverflow.com/q/17339789/256138). – rubenvb Jan 23 '15 at 13:25
  • heh https://twitter.com/SeanParent/status/558330478541803522 never takes long for these to get capitalized on SO :) – sehe Jan 23 '15 at 13:39

2 Answers2

25

The short answer is "it does it not very well".

It invokes f on each of the args..., and discards the return value. But it does so in a way that leads to unexpected behavior in a number of cases, needlessly.

The code has no ordering guarantees, and if the return value of f for a given Arg has an overloaded operator, it can have unfortunate side effects.

With some white space:

[](...){}(
  (
    f(std::forward<Args>(args)), 0
  )...
);

We will start from the inside.

f(std::forward<Args>(args)) is an incomplete statement that can be expanded with a .... It will invoke f on one of args when expanded. Call this statement INVOKE_F.

(INVOKE_F, 0) takes the return value of f(args), applies operator, then 0. If the return value has no overrides, this discards the return value of f(args) and returns a 0. Call this INVOKE_F_0. If f returns a type with an overriden operator,(int), bad things happen here, and if that operator returns a non-POD-esque type, you can get "conditionally supported" behavior later on.

[](...){} creates a lambda that takes C-style variadics as its only argument. This isn't the same as C++11 parameter packs, or C++14 variadic lambdas. It is possibly illegal to pass non-POD-esque types to a ... function. Call this HELPER

HELPER(INVOKE_F_0...) is a parameter pack expansion. in the context of invoking HELPER's operator(), which is a legal context. The evaluation of arguments is unspecified, and due to the signature of HELPER INVOKE_F_0... probably should only contain plain old data (in C++03 parlance), or more specifically [expr.call]/p7 says: (via @T.C)

Passing a potentially-evaluated argument of class type (Clause 9) having a nontrivial copy constructor, a non-trivial move constructor, or a non-trivial destructor, with no corresponding parameter, is conditionally-supported with implementation-defined semantics.

So the problems of this code is that the order is unspecified and it relies on well behaved types or specific compiler implementation choices.

We can fix the operator, problem as follows:

template <class F, class... Args> 
void for_each_argument(F f, Args&&... args) { 
  [](...){}((void(f(std::forward<Args>(args))), 0)...); 
}

then we can guarantee order by expanding in an initializer:

template <class F, class... Args> 
void for_each_argument(F f, Args&&... args) { 
  int unused[] = {(void(f(std::forward<Args>(args))), 0)...}; 
  void(unused); // suppresses warnings
}

but the above fails when Args... is empty, so add another 0:

template <class F, class... Args> 
void for_each_argument(F f, Args&&... args) { 
  int unused[] = {0, (void(f(std::forward<Args>(args))), 0)...}; 
  void(unused); // suppresses warnings
}

and there is no good reason for the compiler to NOT eliminate unused[] from existance, while still evaluated f on args... in order.

My preferred variant is:

template <class...F>
void do_in_order(F&&... f) { 
  int unused[] = {0, (void(std::forward<F>(f)()), 0)...}; 
  void(unused); // suppresses warnings
}

which takes nullary lambdas and runs them one at a time, left to right. (If the compiler can prove that order does not matter, it is free to run them out of order however).

We can then implement the above with:

template <class F, class... Args> 
void for_each_argument(F f, Args&&... args) { 
  do_in_order( [&]{ f(std::forward<Args>(args)); }... );
}

which puts the "strange expansion" in an isolated function (do_in_order), and we can use it elsewhere. We can also write do_in_any_order that works similarly, but makes the any_order clear: however, barring extreme reasons, having code run in a predictable order in a parameter pack expansion reduces surprise and keeps headaches to a minimum.

A downside to the do_in_order technique is that not all compilers like it -- expanding a parameter pack containing statement that contains entire sub-statements is not something they expect to have to do.

Edgar Rokjān
  • 17,245
  • 4
  • 40
  • 67
Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • [expr.call]/p7 says that "Passing a potentially-evaluated argument of class type (Clause 9) having a nontrivial copy constructor, a non-trivial move constructor, or a non-trivial destructor, with no corresponding parameter, is conditionally-supported with implementation-defined semantics." It's still bad, but not UB. – T.C. Jan 23 '15 at 19:35
  • @t.c. how does that distinguishable from UB? I guess it is conditionally permitted in `constexpr` where UB is banned. Implementaions have always been free to define UB however they want (except in `constexpr`?) – Yakk - Adam Nevraumont Jan 23 '15 at 19:39
  • 1
    Using something conditionally-supported that is not supported by a particular implementation requires the implementation to issue a diagnostic. And the implementation is required to document everything conditionally-supported that it doesn't support. (And since the semantics here is implementation-defined when it's supported, that must be documented as well.) – T.C. Jan 23 '15 at 19:41
  • @T.C. Ok. I think I added enough weasel words to make the situation clear, plus the quote you found in the standard. Given how easy it is to fix the problem (the `void` cast) it really should just be done. Plus, `operator,` could cause surprising side effects if overridden anyhow. – Yakk - Adam Nevraumont Jan 23 '15 at 20:05
  • Er... assuming there's no custom `operator,`, the original code doesn't pass any non-POD types, does it? Each argument is just an `int` with a value of zero. –  Jan 23 '15 at 20:24
  • @hvd yes, assuming there is no `operator,` override on the return type of `f`. But the function `f` is passed in by client code, and the code should not make any assumptions about what it will return. Especially considering how easy it is to prevent the problem. – Yakk - Adam Nevraumont Jan 23 '15 at 20:28
  • Yeah, agreed that the code should be using `(void(f(std::forward(args))), 0)` or equivalent, but the primary reason for that should be to prevent any custom `operator,` being called in the first place, and after that, the warnings about certain non-trivially-copyable being passed through `...` aren't really applicable, as the code then already protects against that. (BTW, the unspecified order of evaluation seems to have been intentional. From a reply on Twitter: "Very useful if you don't care about evaluation order (better name than for_each_?). My use was implementing when_all().".) –  Jan 23 '15 at 20:35
  • @hvd If intentional as claimed, it is a design error. Client programmers should not have to decode that statement to use it, and the function name does not scream "warning, this may not happen in the order you may expect, and might change order next compiler version or on a different platform". Increasing testing load by orders of magnitude for no good reason: an unordered implementation that screams "unordered" is great, but the default one shouldn't be unordered. I'd guess it wasn't unordered by design, but rather unordered by accident, noticed as unordered, and back-explained as ok. – Yakk - Adam Nevraumont Jan 23 '15 at 20:41
13

Actually it calls function f for each argument in args in unspecified order.

[](...){}

create lambda function, that does nothing and receives arbitrary number of arguments (va args).

((f(std::forward<Args>(args)), 0)...)

argument of lambda.

(f(std::forward<Args>(args)), 0)

call f with forwarded argument, send 0 to lambda.

If you want specified order you can use following thing:

using swallow = int[];
(void)swallow{0, (f(std::forward<Args>(args)), 0)...};
Community
  • 1
  • 1
ForEveR
  • 55,233
  • 2
  • 119
  • 133
  • But I suppose the order in which the calls are made is arbitrary/undefined? – rubenvb Jan 23 '15 at 13:18
  • 1
    @rubenvb Correct. Unspecified. – Barry Jan 23 '15 at 13:21
  • why does lambda has (...) ? – NoSenseEtAl Jan 23 '15 at 13:22
  • 3
    @NoSenseEtAl Because it has to take one 0 for each argument you pass in. There's no way to write "lambda that takes N args" so this is just "lambda that takes anything". – Barry Jan 23 '15 at 13:23
  • 1
    @NoSenseEtAl In case you're wondering what it is supplied with (that `, 0`), you can't have a succession of `void, void, void` if `f` returns `void`, But you *can* ignore what `f` returns and just use a definite value. – WhozCraig Jan 23 '15 at 13:25
  • 1
    A couple `(void)` casts to defend against overloaded comma operators won't hurt. – T.C. Jan 23 '15 at 16:35
  • To add to @WhozCraig's comment, it protects against any return type that cannot be passed to variadic functions. `void` is one of them, but most non-trivially-copyable types won't work either. –  Jan 23 '15 at 19:25