C++ execution order in method chaining
Solution 1
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);
}
Solution 2
I think this part of the draft standard regarding order of evaluation is relevant:
1.9 Program Execution
...
- 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
...
- [ 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:
- Variable
nu
is allocated on the stack and initialised with 0. - A register (
ebx
in my case) receives a copy of the value ofnu
- The addresses of
nu
andc
are loaded into parameter registers -
meth1
is called - The return value register and the previously cached value of
nu
in theebx
register are loaded into parameter registers -
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.
Solution 3
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.
Solution 4
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
Solution 5
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:
Related videos on Youtube
Moises Viñas
Updated on June 03, 2022Comments
-
Moises Viñas almost 2 years
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 whenmeth2()
starts?-
Lightness Races in Orbit almost 8 years@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!
-
Jan Hudec almost 8 yearsThis 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.
-
Shafik Yaghmour almost 8 years
-
Pharap almost 8 years@JanHudec This is precisely why functional programming puts such great emphasis on function purity.
-
Neil almost 8 yearsAs an example, a stack-based calling convention would probably prefer to push
nu
,&nu
, andc
on to the stack in that order, then invokemeth1
, push the result on to the stack, then invokemeth2
, while a register-based calling convention would want to loadc
and&nu
into registers, invokemeth1
, loadnu
into a register, then invokemeth2
. -
Amir Kirsh almost 3 yearsThe 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.
-
-
Revolver_Ocelot almost 8 yearsThere is a possibility, that a proposal to clarify evaluation order in some cases, which fixes this problem, will come through for C++17
-
Martin Bonner supports Monica almost 8 yearsI 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. -
Roddy almost 8 yearsDo I understand this right? evaluation order of
meth1
andmeth2
is defined, but evaluation of parameter formeth2
may happen beforemeth1
is called...? -
Martin Bonner supports Monica almost 8 yearsIn practise, what is happening is that the evaluation of the argument to
meth2
is happening before the call tometh1
. Note: Your analysis of the sequencing is correct - but my formulation may be easier for the OP to follow. -
Jan Hudec almost 8 yearsMethod 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).
-
Joshua Taylor almost 8 yearsNow, 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". -
Lightness Races in Orbit almost 8 years@JoshuaTaylor: Yes, if the program were different, you'd have to write different code.
-
Jules almost 8 yearsNote that while
meth2
's parameter may or may not be evaluated beforemeth1
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. -
BartekChom almost 8 yearsIt is logical, when you think about it. It works like
meth2(meth1(c, &nu), nu)
-
T.C. almost 8 yearsThis 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.
-
Smeeheey almost 8 years@T.C. - 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 beforemeth2
, but the parameter formeth2
is a value ofnu
cached into a register before the call tometh1
- i.e. the compiler has ignored the potential side-effects, which is consistent with my answer. -
T.C. almost 8 yearsYou 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 intometh2
and 2) sincec.meth1(&nu)
is a function call, it is indeterminately sequenced with the evaluation ofnu
. Insidemeth2
, if it somehow obtained a pointer to the variable inmain
, it would always see 1. -
Smeeheey almost 8 years@T.C. - 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.
-
T.C. almost 8 years"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). -
Smeeheey almost 8 years@T.C. - 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.
-
Lightness Races in Orbit almost 8 years@Buksy: Then it's a good thing your C++ textbook explains this phenomenon.
-
Buksy almost 8 years@LightnessRacesinOrbit not sure I understand, what textbook do you mean?
-
Smeeheey almost 8 years@T.C. - 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.
-
T.C. almost 8 yearsYou took something wrong, and made it worse. There is absolutely no undefined behavior in here. Keep reading [intro.execution]/15, past the example.
-
Smeeheey almost 8 yearsI have read past it, what specifically are you referring to? Are you happy to private-chat about this?
-
Smeeheey almost 8 yearsLook at the assembly code produced above. The compiler was fully entitled to load
nu
from the stack into a parameter register after the call tometh1
, which would have produced different output. How can that be well-defined behaviour? -
T.C. almost 8 years"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
andc.meth1(&nu)
are indeterminately sequenced, not unsequenced, and so the behavior is unspecified, not undefined. These two are a world apart. -
Smeeheey almost 8 years"The evaluation of
nu
-the-argument-to-meth2
andc.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. -
T.C. almost 8 yearsYou have two "called function"s here.
meth1
is every bit a function asmeth2
. (The evaluations of&nu
andnu
-the-argument-to-meth2
are unsequenced, but the call tometh1
is indeterminately sequenced w/r/t every other evaluation inmain
, including the evaluation ofnu
-the-argument-to-meth2
.)