Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C++ execution order in method chaining

People also ask

How do you do chaining method?

Method Chaining is the practice of calling different methods in a single line instead of calling other methods with the same object reference separately. Under this procedure, we have to write the object reference once and then call the methods by separating them with a (dot.).

What is method chaining in C++?

Method chaining, also known as named parameter idiom, is a common syntax for invoking multiple method calls in object-oriented programming languages. Each method returns an object, allowing the calls to be chained together in a single statement without requiring variables to store the intermediate results.

What is C# chaining?

Method chaining is a technique in which methods are called on a sequence to form a chain and each of these methods return an instance of a class. These methods can then be chained together so that they form a single statement. A fluent interface is an object-oriented API that depends largely on method chaining.


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);
}

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.


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.


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


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:

  • What are the evaluation order guarantees introduced by C++17?
  • cppreference on the evaluation order
  • this presentation point from CoreCpp 2019