C++ execution order in method chaining

13,615

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

...

  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.

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:

Share:
13,615

Related videos on Youtube

Moises Viñas
Author by

Moises Viñas

Updated on June 03, 2022

Comments

  • Moises Viñas
    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 when meth2() starts?

    • Lightness Races in Orbit
      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
      Jan Hudec almost 8 years
      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.
    • Shafik Yaghmour
      Shafik Yaghmour almost 8 years
    • Pharap
      Pharap almost 8 years
      @JanHudec This is precisely why functional programming puts such great emphasis on function purity.
    • Neil
      Neil almost 8 years
      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.
    • Amir Kirsh
      Amir Kirsh almost 3 years
      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.
  • Revolver_Ocelot
    Revolver_Ocelot almost 8 years
    There 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
    Martin Bonner supports Monica almost 8 years
    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.
  • Roddy
    Roddy almost 8 years
    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...?
  • Martin Bonner supports Monica
    Martin Bonner supports Monica almost 8 years
    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.
  • Jan Hudec
    Jan Hudec almost 8 years
    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).
  • Joshua Taylor
    Joshua Taylor almost 8 years
    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".
  • Lightness Races in Orbit
    Lightness Races in Orbit almost 8 years
    @JoshuaTaylor: Yes, if the program were different, you'd have to write different code.
  • Jules
    Jules almost 8 years
    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.
  • BartekChom
    BartekChom almost 8 years
    It is logical, when you think about it. It works like meth2(meth1(c, &nu), nu)
  • T.C.
    T.C. almost 8 years
    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.
  • Smeeheey
    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 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.
  • T.C.
    T.C. almost 8 years
    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.
  • Smeeheey
    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.
    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
    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
    Lightness Races in Orbit almost 8 years
    @Buksy: Then it's a good thing your C++ textbook explains this phenomenon.
  • Buksy
    Buksy almost 8 years
    @LightnessRacesinOrbit not sure I understand, what textbook do you mean?
  • Smeeheey
    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.
    T.C. almost 8 years
    You took something wrong, and made it worse. There is absolutely no undefined behavior in here. Keep reading [intro.execution]/15, past the example.
  • Smeeheey
    Smeeheey almost 8 years
    I have read past it, what specifically are you referring to? Are you happy to private-chat about this?
  • Smeeheey
    Smeeheey almost 8 years
    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?
  • T.C.
    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 and c.meth1(&nu) are indeterminately sequenced, not unsequenced, and so the behavior is unspecified, not undefined. These two are a world apart.
  • Smeeheey
    Smeeheey almost 8 years
    "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.
  • T.C.
    T.C. almost 8 years
    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.)