方法链中的C++执行顺序

Moi*_*ñas 105 c++ operator-precedence chaining

该程序的输出:

#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);
}
Run Code Online (Sandbox Code Playgroud)

方法是:

method 1
method 2:0
Run Code Online (Sandbox Code Playgroud)

开始nu时为什么不是1 meth2()

Lig*_*ica 64

因为评估订单未指定.

你看到numain被评估到0之前甚至meth1被调用.这是链接的问题.我建议不要这样做.

只需制作一个简单,清晰,易读,易于理解的程序:

int main()
{
  c1 c;
  int nu = 0;
  c.meth1(&nu);
  c.meth2(nu);
}
Run Code Online (Sandbox Code Playgroud)

  • 我理解这个吗?定义了`meth1`和`meth2`的评估顺序,但是`meth2`的参数评估可能在调用`meth1`之前发生...? (34认同)
  • 有可能提出[在某些情况下澄清评估顺序](http://open-std.org/JTC1/SC22/WG21/docs/papers/2016/p0145r1.pdf),以解决这个问题,将来C++ 17 (14认同)
  • 我喜欢方法链接(例如`<<`表示输出,"对象构建器"表示构造函数参数太多的复杂对象 - 但它与输出参数混合得非常糟糕. (7认同)
  • 方法链接很好,只要这些方法是合理的并且只修改了调用者(效果很好,因为第二种方法是在第一种方法的结果上调用的). (7认同)
  • 当你想到它时,这是合乎逻辑的.它的作用类似于`meth2(meth1(c,&nu),nu)` (4认同)
  • 请注意,虽然“meth2”的参数*可能会或可能不会*在根据标准调用“meth1”之前评估,但在这种特定情况下并且使用优化编译器,很可能会首先评估它,因为编译器在语句开始时就知道它的值是什么,因此可以通过首先评估它来保存内存加载指令。 (2认同)

Sme*_*eey 28

我认为关于评估顺序的标准草案的这一部分是相关的:

1.9程序执行

...

  1. 除非另有说明,否则对单个运算符的操作数和单个表达式的子表达式的评估是不确定的.在运算符的结果的值计算之前,对运算符的操作数的值计算进行排序.如果对标量对象的副作用相对于同一标量对象的另一个副作用或使用相同标量对象的值进行的值计算未被排序,并且它们不可能是并发的,则行为未定义

并且:

5.2.2函数调用

...

  1. [注意:后缀表达式和参数的评估都是相对于彼此的.在输入函数之前,参数评估的所有副作用都会被排序 - 结束注释]

因此,对于您的行c.meth1(&nu).meth2(nu);,请考虑最终调用的函数调用运算符在运算符中发生的情况meth2,因此我们清楚地看到后缀表达式和参数的细分nu:

operator()(c.meth1(&nu).meth2, nu);
Run Code Online (Sandbox Code Playgroud)

根据上面的函数调用规则,对最终函数调用的后缀表达式和参数(即后缀表达式c.meth1(&nu).meth2nu)的评估相对于彼此无序的.因此,相对于函数调用之前的参数评估,对标量对象上的后缀表达式的计算的副作用是未序的.通过上面的程序执行规则,这是未定义的行为.arnumeth2

换句话说,编译器不需要numeth2调用后评估调用的参数meth1- 可以自由地假设没有meth1影响nu评估的副作用.

上面生成的汇编代码在main函数中包含以下序列:

  1. 变量nu在堆栈上分配并初始化为0.
  2. 寄存器(ebx在我的情况下)收到值的副本nu
  3. 地址nuc加载到参数寄存器中
  4. meth1 叫做
  5. 返回值寄存器和先前高速缓存的值nuebx寄存器被加载到参数寄存器
  6. meth2 叫做

关键的是,在上面的步骤5中,编译器允许nu在函数调用中重用步骤2 的缓存值meth2.在这里,它忽略了可能nu通过调用meth1"未定义的行为" 而改变的可能性.

注意:此答案的实质内容已从原始形式发生变化.我最初的解释是在最终函数调用之前未对操作数计算进行排序的副作用是不正确的,因为它们是.问题在于操作数本身的计算是不确定的.

  • 这是错的.函数调用是在调用函数中的其他评估中不确定地排序的(除非以其他方式强制执行排序前约束); 他们不交错. (2认同)
  • "但是,操作数计算的副作用(即设置ar的值)不能保证在上面的任何事情之前(按照上述2)排序." 绝对保证在调用`meth2`之前对其进行排序,如您引用的cppreference页面的第3项中所述(您也忽略了正确引用). (2认同)

Pet*_*ter 9

在1998 C++标准中,第5节,第4段

除非另有说明,否则单个运算符的操作数和单个表达式的子表达式的评估顺序以及副作用发生的顺序是未指定的.在前一个和下一个序列点之间,标量对象应通过表达式的计算最多修改其存储值一次.此外,只能访问先前值以确定要存储的值.对于完整表达式的子表达式的每个允许排序,应满足本段的要求; 否则行为未定义.

(我省略了对脚注#53的引用,这与此问题无关).

基本上,&nu必须在调用之前进行评估c1::meth1(),并且nu必须在调用之前进行评估c1::meth2().然而,有没有这个要求nu之前评估&nu(例如,它是允许的nu可以先进行计算,然后&nu,再c1::meth1()被称为-这可能是你的编译器做的).表达*ar = 1c1::meth1()因此不能保证之前评估numain()进行评价时,以被传递给c1::meth2().

后来的C++标准(我目前在PC上没有使用的标准)具有基本相同的条款.


小智 6

我认为在编译时,在真正调用函数meth1和meth2之前,参数已经传递给它们.我的意思是当你使用"c.meth1(&nu).meth2(nu);" 值nu = 0已经传递给meth2,所以后者改变"nu"无关紧要.

你可以试试这个:

#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();
}
Run Code Online (Sandbox Code Playgroud)

它会得到你想要的答案