Function argument evaluation and side effects
Asked Answered
A

2

18

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?

Attrition answered 7/2, 2022 at 3:1 Comment(15)
I also understood that "the initialization of a parameter" includes the whole argument expression (aka the initializer). Here is an older comment by Richard Smith that I understood that way: groups.google.com/a/isocpp.org/g/std-discussion/c/Hr7UPtz_DNY/m/…Nosewheel
@Nosewheel Which change, specifically?Attrition
Referring to my deleted comment? I meant the sentence you quoted. Before C++17 there was no equivalent normative wording, only a similarly worded note and apparently no normative text to back it up (see linked comment above).Nosewheel
GCC bugs on the issue, one of which is open: gcc.gnu.org/bugzilla/show_bug.cgi?id=85557 gcc.gnu.org/bugzilla/show_bug.cgi?id=78734Nosewheel
@Nosewheel Yes, apparently to a deleted comment. I am not lawyer enough to understand that elevating a mere note to a regular sentence with equivalent wording is a signal to the compiler builders to abandon their faulty ways and walk the path of behavioral righteousness from then on. To me, it is an editorial change, not a semantic one. (Are there notes that are wrong?) That's why I thought you must be referring to some other change from 14 to 17.Attrition
See #21364898. From my understanding, the ISO directives say that notes are not normative, just informational. If a note was in conflict with other (normative) text, removing it would just be an editorial change. You can find several examples of that when searching the issues on the draft github: github.com/cplusplus/draft/issues?q=is%3Aissue+incorrect+note+. (At least that is my understanding as an outsider to the standardization process.)Nosewheel
@Nosewheel It may also be that the draft n4140 I looked at in lieu of the actual C++14 standard had already different wording in it. The proposal which targets C++17 eliminates explicit wording making argument evaluations unsequenced.Attrition
@Peter-ReinstateMonica: "To me, it is an editorial change, not a semantic one." If it's in the normative text, it's in the normative text.Leger
@Peter-ReinstateMonica: Overall, I'm not sure what you're looking for here. The standard is pretty clear about what "indeterminately sequenced" means. So your question seems to be "the standard says these are compiler bugs. Are these compiler bugs?"Leger
@NicolBolas I think the point of the question is what part of the function argument evaluation exactly is made indeterminately sequenced. Is it the whole argument expression or only the "last step" initializing the parameter from the evaluated argument? And if the latter is the case, what exactly counts as this last step?Nosewheel
@user17732522: So you think the question is whether "initialization" involves evaluating the initializer.Leger
@NicolBolas Yes or rather whether "every associated value computation and side effect" includes those of the whole initializer expression's evaluation, because if the answer is yes, this contradicts the comment referenced in the question. And if the answer is no, then GCC may not be in error for calling f with two equal values in the parameter.Nosewheel
@NicolBolas The standard is pretty unclear to me and others, as you can see. This is the first time I see both gcc and cadul sitting on obvious, and in the gcc case significant, bugs for many years (it was for me already a bug to all intents and purposes when the sequencing was just a note, and it's been normative for 5 years now). So when I see both compilers disagreeing with me, I'm suspicious of my understanding, and usually rightly so ;-).Attrition
@Peter-ReinstateMonica: The authors of gcc and clang are each prone to interpret the fact that the other performs an "optimization" as proof that the Standard allows it, regardless of whether the Standard actually does. In such cases, I think clang and gcc interpret the difference between what their compilers do and what the Standard says as being a defect in the Standard rather than a "bug" in their compiler.Eteocles
@Peter-ReinstateMonica: That attitude would be reasonable in some cases where badly-written parts of the Standard would cause a construct to be defined in cases that would be expensive to support but only be relevant in contrived situations, but gcc and clang ignore the Standard even in cases that are clear and unambiguous.Eteocles
L
6

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.

Leger answered 7/2, 2022 at 7:36 Comment(9)
Mere confusing code is a matter of taste. Warning about it should be left to analysis tools or comparable compiler options. Code with indeterminate results is more than confusing: It may be an inadvertent mistake. That a compiler may or even should warn about such dangers is undisputed. But the warning of both gcc and clang is wrong: Apparently, function argument evaluation, since C++17, is trivially never "unsequenced", which is what both gcc and clang claim to be a possibility.Attrition
In fact, rereading clang's warning: clang is plainly making a wrong claim ("warning: multiple unsequenced modifications to 'a'"). gcc washes its hands by inserting in a careful "may" which "may or may not" improve the message (it is less wrong but also less helpful).Attrition
@Peter-ReinstateMonica: "Mere confusing code is a matter of taste" If you have to be a language lawyer to know if the code is correct, then it's confusing. I'd say that's a pretty universal definition of "confusing". Just write it properly where possible.Leger
You know, with C++ you have to be a language lawyer unless you restrict yourself to the C subset. It's one of the main problems of the language, and it's not getting better, in spite of Herb's best intentions.Attrition
@Peter-ReinstateMonica: "You know, with C++ you have to be a language lawyer unless you restrict yourself to the C subset." Nonsense. You can do plenty of regular old C++ stuff without being a language lawyer. Language lawyerness only really matters when you're trying to do dubious stuff at the edges of the object model (constructing/destroying objects in unformed storage, trivial copyability, and code of dubious merit like what you're talking about here). If you avoid writing complex single expressions and spell it out more, you won't run into this issue.Leger
@Nicl Nonsense? I'm just reading https://mcmap.net/q/742321/-c-vector-member-initialization/3150802. Curly braces just change their semantics at will, depending on the element type. (I'll keep adding examples every time I come across one, if you don't mind.)Attrition
@Peter-ReinstateMonica: There's a difference between being a language lawyer and having esoteric rules. List initialization is an esoteric rule with a bunch of gotchas. Whether a parameter's initialization includes the evaluation of the expression that's doing the initializing so as to determine whether the ordering of those evaluations vs. other evaluations in a function call, all so that one can interrogate whether nonsense code that should never be written happens to be legitimate? That's language lawyering.Leger
@NicolBolas: In the C language Dennis Ritchie invented, every region of storage simultaneously held objects of every kind that would fit; writing to any of them would write the underlying storage, and changes to the underlying storage would affect all the objects whose value depended upon it. Such a model would be consistent with the behavior of all trivial types in C++, and would be simpler than the model specified by the Standard. If that model were tweaked to facilitate optimizations, it could allow more useful optimizations than the present one while remaining simpler.Eteocles
@NicolBolas Here is another one. Have you heard of pseudo destructors? Where you can call operator->() for non-classes!? Did you know that the language only allows calls to them for scalars? Did you know that MSVC V.19.30 (and probably all other versions) gets that wrong, even with /permissive-? This actually concerns our software development because such calls typically happen in generic containers.Attrition
T
0

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.

Tauromachy answered 3/12, 2022 at 21:4 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.