C++ execution order in method chaining
Asked Answered
F

5

121

The output of this program:

#include <iostream> 
class c1
{   
  public:
    c1& meth1(int* ar) {
      std::cout << "method 1" << std::endl;
      *ar = 1;
      return *this;
    }
    void meth2(int ar)
    {
      std::cout << "method 2:"<< ar << std::endl;
    }
};

int main()
{
  c1 c;
  int nu = 0;
  c.meth1(&nu).meth2(nu);
}

Is:

method 1
method 2:0

Why is nu not 1 when meth2() starts?

Fulvia answered 16/5, 2016 at 11:0 Comment(7)
@MartinBonner: Although I know the answer, I wouldn't call it "obvious" in any sense of the word and, even if it were, that would not be a decent reason to drive-by downvote. Disappointing!Floater
This is what you get when you modify your arguments. Functions modifying their arguments are harder to read, their effects are unexpected for the next programmer to work on the code and they lead to surprises like this. I strongly suggest to avoid modifying any parameters except the invocant. Modifying the invocant wouldn't be a problem here, because the second method is called on the result of the first, so the effects are ordered on it. There are still some cases where they wouldn't be though.Bugeye
This is relevant question as wellSchock
@JanHudec This is precisely why functional programming puts such great emphasis on function purity.Smog
As an example, a stack-based calling convention would probably prefer to push nu, &nu, and c on to the stack in that order, then invoke meth1, push the result on to the stack, then invoke meth2, while a register-based calling convention would want to load c and &nu into registers, invoke meth1, load nu into a register, then invoke meth2.Lelia
The answer to this question depends on the C++ standard. It changed since C++17 with P0145 accepted into the spec. See also: this SO post, cppref on the subject and this point at a presentation from CoreCpp 2019.Asco
@AmirKirsh is right, after C++17 the result of the above code is always 1. Before C++17, the result can be either 0(like the OP's result) or 1.Quoth
F
70

Because evaluation order is unspecified.

You are seeing nu in main being evaluated to 0 before even meth1 is called. This is the problem with chaining. I advise not doing it.

Just make a nice, simple, clear, easy-to-read, easy-to-understand program:

int main()
{
  c1 c;
  int nu = 0;
  c.meth1(&nu);
  c.meth2(nu);
}
Floater answered 16/5, 2016 at 11:2 Comment(10)
There is a possibility, that a proposal to clarify evaluation order in some cases, which fixes this problem, will come through for C++17Georgiageorgian
I like method chaining (eg << for output, and "object builders" for complex objects with too many arguments to the constructors - but it mixes really badly with output arguments.Aldis
Do I understand this right? evaluation order of meth1 and meth2 is defined, but evaluation of parameter for meth2 may happen before meth1 is called...?Ensnare
Method chaining is fine as long as the methods are sensible and only modify the invocant (for which the effects are well ordered, because the second method is called on the result of the first).Bugeye
Now, to be strictly equivalent, this would need to be tmp = c.meth1(&nu); tmp.meth2(nu);, right? Since meth1 returns the *this, they'll be equivalent in this case, but if c.meth returned something unusual, these wouldn't be the same. That's really just elaborating on Jan Hudec's comment that "the second method is called on the result of the first".Jeffcott
@JoshuaTaylor: Yes, if the program were different, you'd have to write different code.Floater
Note that while meth2's parameter may or may not be evaluated before meth1 is called per the standard, in this specific case and with an optimising compiler, it's very likely that it will be evaluated first, simply because the compiler knows what its value is at the start of the statement, and can therefore save a memory load instruction by evaluating it first.Morly
It is logical, when you think about it. It works like meth2(meth1(c, &nu), nu)Lachesis
@Buksy: Then it's a good thing your C++ textbook explains this phenomenon.Floater
@LightnessRacesinOrbit not sure I understand, what textbook do you mean?Andry
D
32

I think this part of the draft standard regarding order of evaluation is relevant:

1.9 Program Execution

...

  1. Except where noted, evaluations of operands of individual operators and of subexpressions of individual expressions are unsequenced. The value computations of the operands of an operator are sequenced before the value computation of the result of the operator. If a side effect on a scalar object is unsequenced relative to either another side effect on the same scalar object or a value computation using the value of the same scalar object, and they are not potentially concurrent, the behavior is undefined

and also:

5.2.2 Function call

...

  1. [ Note: The evaluations of the postfix expression and of the arguments are all unsequenced relative to one another. All side effects of argument evaluations are sequenced before the function is entered — end note ]

So for your line c.meth1(&nu).meth2(nu);, consider what is happening in operator in terms of the function call operator for the final call to meth2, so we clearly see the breakdown into the postfix expression and argument nu:

operator()(c.meth1(&nu).meth2, nu);

The evaluations of the postfix expression and argument for the final function call (i.e. the postfix expression c.meth1(&nu).meth2 and nu) are unsequenced relative to one another as per the function call rule above. Therefore, the side-effect of the computation of the postfix expression on the scalar object ar is unsequenced relative to the argument evaluation of nu prior to the meth2 function call. By the program execution rule above, this is undefined behaviour.

In other words, there is no requirement for the compiler to evaluate the nu argument to the meth2 call after the meth1 call - it is free to assume no side-effects of meth1 affect the nu evaluation.

The assembly code produced by the above contains the following sequence in the main function:

  1. Variable nu is allocated on the stack and initialised with 0.
  2. A register (ebx in my case) receives a copy of the value of nu
  3. The addresses of nu and c are loaded into parameter registers
  4. meth1 is called
  5. The return value register and the previously cached value of nu in the ebx register are loaded into parameter registers
  6. meth2 is called

Critically, in step 5 above the compiler allows the cached value of nu from step 2 to be re-used in the function call to meth2. Here it disregards the possibility that nu may have been changed by the call to meth1 - 'undefined behaviour' in action.

NOTE: This answer has changed in substance from its original form. My initial explanation in terms of side-effects of operand computation not being sequenced before the final function call were incorrect, because they are. The problem is the fact that computation of the operands themselves is indeterminately sequenced.

Densitometer answered 16/5, 2016 at 11:17 Comment(14)
In practise, what is happening is that the evaluation of the argument to meth2 is happening before the call to meth1. Note: Your analysis of the sequencing is correct - but my formulation may be easier for the OP to follow.Aldis
This is wrong. Function calls are indeterminately sequenced w/r/t other evaluations in the calling function (unless a sequenced-before constraint is otherwise imposed); they do not interleave.Unilingual
@Unilingual - I never said anything about the function calls being interleaved. I only referred to side-effects of operators. If you look at the assembly code produced by the above, you will see that meth1 is executed before meth2, but the parameter for meth2 is a value of nu cached into a register before the call to meth1 - i.e. the compiler has ignored the potential side-effects, which is consistent with my answer.Densitometer
You are exactly claiming that - "its side-effect (i.e. setting the value of ar) is not guaranteed to be sequenced before the call". The evaluation of the postfix-expression in a function call (which is c.meth1(&nu).meth2) and the evaluation of the argument to that call (nu) are generally unsequenced, but 1) their side effects are all sequenced before entry into meth2and 2) since c.meth1(&nu) is a function call, it is indeterminately sequenced with the evaluation of nu. Inside meth2, if it somehow obtained a pointer to the variable in main, it would always see 1.Unilingual
@Unilingual - I don't think I claimed anything which contradicts what you're saying. Anyway I've edited the answer now to be somewhat more elaborate. Hopefully you will agree with it now.Densitometer
"However, the side-effect of the computation of the operands (i.e. setting the value of ar) is not guaranteed to be sequenced before anything at all (as per 2) above)." It is absolutely guaranteed to be sequenced before the call to meth2, as noted in item 3 of the cppreference page you are quoting (which you also neglected to properly cite).Unilingual
@Unilingual - you are hard to please :) . Fine, I made 2 more minor edits. I get what you're saying but it somehow seems quite pedantic now. Yes, OK "anything at all" was strictly speaking incorrect, but I think it is clear from the rest of the context that the "all" being referred to is all the operand evaluation.Densitometer
@Unilingual - I have further heavily edited a large part of the answer, including scrapping the cppreference link which doesn't explain what is going on here. Thanks for your input, I think the answer is much more accurate now because of it.Densitometer
You took something wrong, and made it worse. There is absolutely no undefined behavior in here. Keep reading [intro.execution]/15, past the example.Unilingual
I have read past it, what specifically are you referring to? Are you happy to private-chat about this?Densitometer
Look at the assembly code produced above. The compiler was fully entitled to load nu from the stack into a parameter register after the call to meth1, which would have produced different output. How can that be well-defined behaviour?Densitometer
"Every evaluation in the calling function (including other function calls) that is not otherwise specifically sequenced before or after the execution of the body of the called function is indeterminately sequenced with respect to the execution of the called function." (The wording is changed in the current working draft, but the gist of it didn't change.) The evaluation of nu-the-argument-to-meth2 and c.meth1(&nu) are indeterminately sequenced, not unsequenced, and so the behavior is unspecified, not undefined. These two are a world apart.Unilingual
"The evaluation of nu-the-argument-to-meth2 and c.meth1(&nu) are indeterminately sequenced" - this conclusion is not supported by the quote you provide. In fact the quote shows that both of them are indeterminately sequenced with respect to the execution of the called function (i.e. meth2). It doesn't say anything about them being indeterminately sequenced with respect to each other.Densitometer
You have two "called function"s here. meth1 is every bit a function as meth2. (The evaluations of &nu and nu-the-argument-to-meth2 are unsequenced, but the call to meth1 is indeterminately sequenced w/r/t every other evaluation in main, including the evaluation of nu-the-argument-to-meth2.)Unilingual
C
9

In the 1998 C++ standard, Section 5, para 4

Except where noted, the order of evaluation of operands of individual operators and subexpressions of individual expressions, and the order in which side effects take place, is unspecified. Between the previous and next sequence point a scalar object shall have its stored value modified at most once by the evaluation of an expression. Furthermore, the prior value shall be accessed only to determine the value to be stored. The requirements of this paragraph shall be met for each allowable ordering of the subexpressions of a full expression; otherwise the behavior is undefined.

(I've omitted a reference to footnote #53 which is not relevant to this question).

Essentially, &nu must be evaluated before calling c1::meth1(), and nu must be evaluated before calling c1::meth2(). There is, however, no requirement that nu be evaluated before &nu (e.g. it is permitted that nu be evaluated first, then &nu, and then c1::meth1() is called - which might be what your compiler is doing). The expression *ar = 1 in c1::meth1() is therefore not guaranteed to be evaluated before nu in main() is evaluated, in order to be passed to c1::meth2().

Later C++ standards (which I don't currently have on the PC I'm using tonight) have essentially the same clause.

Carollcarolle answered 16/5, 2016 at 11:41 Comment(0)
C
8

I think when compiling ,before the funtions meth1 and meth2 are really called, the paramaters have been passed to them. I mean when you use "c.meth1(&nu).meth2(nu);" the value nu = 0 have been passed to meth2, so it doesn't matter wether "nu" is changed latter.

you can try this:

#include <iostream> 
class c1
{
public:
    c1& meth1(int* ar) {
        std::cout << "method 1" << std::endl;
        *ar = 1;
        return *this;
    }
    void meth2(int* ar)
    {
        std::cout << "method 2:" << *ar << std::endl;
    }
};

int main()
{
    c1 c;
    int nu = 0;
    c.meth1(&nu).meth2(&nu);
    getchar();
}

it will get the answer you want

Cayuga answered 16/5, 2016 at 11:20 Comment(1)
The real reason for the OP's result (before C++17) is that the order of evaluation is unspecified. It is like meth2(meth1(c, &nu), nu), as @Lachesis commented in the accepted answer. But it seems that the standard was changed in C++17 regarding method chaining. See @AmirKirsh's answer.Quoth
A
3

The answer to this question depends on the C++ standard.

The rules have changed since C++17 with P0145 accepted into the spec. Since C++17 the order of evaluation is defined and parameter evaluation would be performed according to the order of the function calls. Note that parameter evaluation order inside a single function call is still not specified.

So order of evaluation in chaining expressions is guaranteed, since C++17, to work in the actual order of the chain: the code in question is guaranteed since C++17 to print:

method 1
method 2:1

Before C++17 it could print the above, but could also print:

method 1
method 2:0

See also:

Asco answered 7/6, 2021 at 20:9 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.