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


108

该程序的输出:

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

是:

method 1
method 2:0

为什么启动nu时不是1 meth2()


41
@MartinBonner:尽管我知道答案,但无论从什么意义上讲,我都不会称其为“显而易见” ,即使那是正确的理由,也不是拒绝投票的正当理由。令人失望!
Lightness Races in Orbit

4
这就是修改参数时得到的。修改其参数的函数更难以阅读,它们的效果对于下一个程序员处理代码而言是意料之外的,并且会导致类似的意外。我强烈建议避免修改除发起者之外的任何参数。在这里修改主叫方将不是问题,因为第二种方法是在第一种方法的结果上调用的,因此其效果是有序的。在某些情况下,它们不会出现。
Jan Hudec


@JanHudec这就是为什么函数式编程如此强调函数纯净的原因。
法拉普

2
举个例子,一个基于堆栈的调用约定可能会更愿意推nu&nuc到堆栈的顺序,然后调用meth1,推结果到堆栈中,然后调用meth2,而基于寄存器的调用约定会想加载c并加载&nu到寄存器中,调用meth1,加载nu到寄存器中,然后调用meth2
尼尔

Answers:


66

因为未指定评估顺序。

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

只需编写一个漂亮,简单,清晰,易于阅读,易于理解的程序即可:

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

14
有可能 C ++ 17中通过一些提案来澄清某些情况下的评估顺序,从而解决了这个问题
Revolver_Ocelot

7
我喜欢的方法链接(如<<输出,和“对象建设者”的有太多的参数传递给构造复杂的对象-但它混合实在太差了与输出参数。
马丁邦纳支持莫妮卡

34
我理解这个权利吗?评价顺序meth1meth2定义,但参数的评估meth2之前,可能会发生meth1被称为......?
罗迪

7
只要方法明智,方法链接就可以,并且仅修改参与者(其效果是有序的,因为第二种方法是第一种方法的结果)。
Jan Hudec

4
当您考虑时,这是合乎逻辑的。它的工作方式就像meth2(meth1(c, &nu), nu)
BartekChom,2016年

29

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

1.9程序执行

...

  1. 除非另有说明,否则对单个运算符的操作数和单个表达式的子表达式的求值是无序列的。运算符的操作数的值计算在运算符结果的值计算之前进行排序。如果相对于同一标量对象上的另一副作用或使用同一标量对象的值进行的值计算,未对标量对象的副作用进行排序,并且它们没有潜在的并发性,则该行为未定义

并且:

5.2.2函数调用

...

  1. [注意:后缀表达式和参数的求值相对于彼此都是无序列的。在输入函数之前,对参数求值的所有副作用进行了排序-尾注]

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

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

根据上面的函数调用规则,最终函数调用的后缀表达式和参数(即后缀表达式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
错了 函数调用在调用函数中不确定地按顺序进行w / r / t其他评估(除非另外施加了先序约束);他们不交织。
TC

1
@TC-我从未对交错的函数调用说过任何话。我只提到运营商的副作用。如果看一下上面产生的汇编代码,您会看到它meth1是在之前执行的meth2,但是for的参数meth2nu在调用之前缓存到寄存器中的值meth1-即编译器忽略了潜在的副作用,即与我的答案一致。
Smeeheey

1
您确切地宣称-“不能保证在调用之前对其副作用(即设置ar的值)进行排序”。通常不对函数调用中的postfix-expression求值(即c.meth1(&nu).meth2)和对该调用参数的求值(nu)求值,但是1)它们的副作用都在进入之前被排序了meth2; 2)因为c.meth1(&nu)是函数调用,它与的评估不确定地排序nu。里面meth2,如果它在某种程度上得到一个指针变量main,它总是会看到1
TC

2
“但是,操作数计算的副作用(即,设置ar的值)不能保证完全按上面的顺序排列(按2)。” meth2如您所引用的cppreference页的第3项中所述,绝对保证在调用之前对它进行排序(您也忽略了正确引用的内容)。
TC

1
您把事情弄错了,并使情况变得更糟。这里绝对没有未定义的行为。继续阅读示例之后的[intro.execution] / 15。
TC

9

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

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

(我省略了对脚注#53的引用,该注解与该问题无关)。

本质上,&nu必须在调用之前进行评估c1::meth1(),并且nu必须在调用之前进行评估c1::meth2()。但是,没有任何需要nu先评估的要求&nu(例如,允许nu先评估,然后&nuc1::meth1()调用,然后再调用-这可能是编译器正在执行的操作)。表达*ar = 1c1::meth1()因此不能保证前要评估numain()进行评价时,以被传递给c1::meth2()

后来的C ++标准(今晚在我现在使用的PC上尚不具备)实质上具有相同的子句。


7

我认为在编译时,在真正调用函数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();
}

它会得到你想要的答案

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.