Reputation: 16039
The C++20 standard says in Function Call, 7.6.1.3/8:
The initialization of a parameter, including every associated value computation and side effect, is indeterminately sequenced with respect to that of any other parameter.
Indeterminately sequenced (as opposed to unsequenced) ensures that side effects affecting the same memory region are not undefined behavior. Cppreference gives the following examples:
f(i = -2, i = -2); // undefined behavior until C++17 f(++i, ++i); // undefined behavior until C++17, unspecified after C++17
The change in C++17 doesn't seem to be in the quoted section though whose wording essentially stayed the same through the centuries decades. (OK; in n3337 it is only a note.)
And a simple example elicits warnings from both gcc and clang:
void f(int fa, int fb);
void m() // contains code calling f()
{
int a = 11;
f(++a, ++a);
cout << "after f(): a=" << a << '\n';
}
<source>:6:7: warning: multiple unsequenced modifications to 'a' [-Wunsequenced]
f(++a, ++a);
^ ~~
gcc also produces unintuitive code, incrementing a
twice before moving its value into both parameter registers. That contradicts my understanding of the standard wording.
Fellow stackoverflow user Davis Herring mentioned in a comment that while the initialization is sequenced, the evaluation of the argument expressions is not.
Am I misinterpreting this wording? Is cppreference wrong? Are the compilers wrong, especially gcc? What, if anything, changed in C++17 regarding specifically function parameters?
Upvotes: 18
Views: 1029
Reputation: 93
c++17 also added [dcl.init]/18:
If the initializer is a parenthesized expression-list, the expressions are evaluated in the order specified for function calls ([expr.call]).
This clause talks about sequencing of argument expressions and links them to rules for function calls, so i think this actually implies that "associated value computation" includes evaluation of the argument expressions.
Upvotes: 0
Reputation: 473946
If your question is whether "initialization of a parameter" involves evaluating the expression(s) that are part of its initializer... of course it does. Initializing a parameter works exactly like initializing any other object ([dcl.init]/1):
The process of initialization described in this subclause applies to all initializations regardless of syntactic context, including the initialization of a function parameter ([expr.call]), the initialization of a return value ([stmt.return]), or when an initializer follows a declarator.
Emphasis added.
The entirety of [dcl.init] describes the process of initializing objects, but in all cases, it involves evaluating the initializer expression(s). That therefore fits under the "every associated value computation and side effect" part of the rule about sequencing.
Any compiler which doesn't do this for the parameter initializer expression(s) is in error.
Compilers can warn about whatever they want. Indeed, compiler warnings are frequently code that is technically valid but unreasonable.
The main point of even changing this part of the standard is not to make trivial nonsense like f(++a, ++a)
into semi-reasonable code. It's for dealing with cases where the writer of the code has no idea that the same object is being referenced in multiple places. Consider:
template<typename ...Args>
void g(Args &&args)
{
f((++args) ...);
}
Pre-C++17, what this code was valid depends entirely on whether the user passed two references to the same object, and a compiler could not reasonably warn about the potential problems. Post-C++17, this code is safe (modulo compiler bugs).
So giving out warnings for easily detectable confusing code is not just valid, but good.
Upvotes: 6