为什么' - ++ a- - ++ + b - '按此顺序评估?

时间:2017-04-06 18:56:42

标签: c++ operator-overloading compiler-optimization evaluation operator-precedence

为什么以下打印bD aD aB aA aC aU而不是aD aB aA aC bD aU?换句话说,为什么在b--之前评估--++a--++

#include <iostream>
using namespace std;

class A {
    char c_;
public:
    A(char c) : c_(c) {}
    A& operator++() {
        cout << c_ << "A ";
        return *this;
    }
    A& operator++(int) {
        cout << c_ << "B ";
        return *this;
    }
    A& operator--() {
        cout << c_ << "C ";
        return *this;
    }
    A& operator--(int) {
        cout << c_ << "D ";
        return *this;
    }
    void operator+(A& b) {
        cout << c_ << "U ";
    }
};

int main()
{
    A a('a'), b('b');
    --++a-- ++ +b--;  // the culprit
}

从我收集的内容,以下是编译器如何解析表达式:

  • 预处理程序标记化:-- ++ a -- ++ + b --;
  • 运算符优先级 1 (--(++((a--)++))) + (b--);
  • +是从左到右关联的,但编译器可能会先选择评估右侧的表达式(b--)。

我假设编译器选择这样做,因为它会带来更好的优化代码(更少的指令)。但是,值得注意的是,在使用/Od(MSVC)和-O0(GCC)进行编译时,我得到了相同的结果。这让我想到了我的问题:

由于我在一个测试中被问到这个原则上是实现/编译器无关的,是否有C ++标准中的某些内容规定了上述行为,或者它是否真的未指定?有人可以引用标准的摘录来确认吗?在测试中提出这样的问题是不对的?

1 我意识到编译器并不真正了解运算符优先级或关联性,而只关心语言语法,但这应该是重点无论哪种方式。

5 个答案:

答案 0 :(得分:18)

表达式声明

--++a-- ++ +b--;  // the culprit

可以用以下方式表示

最初喜欢

( --++a-- ++ )  + ( b-- );

然后喜欢

( -- ( ++ ( ( a-- ) ++ ) ) )  + ( b-- );

最后喜欢

a.operator --( 0 ).operator ++( 0 ).operator ++().operator --().operator  + ( b.operator --( 0 ) );

这是一个示范程序。

#include <iostream>
using namespace std;

#include <iostream>
using namespace std;

class A {
    char c_;
public:
    A(char c) : c_(c) {}
    A& operator++() {
        cout << c_ << "A ";
        return *this;
    }
    A& operator++(int) {
        cout << c_ << "B ";
        return *this;
    }
    A& operator--() {
        cout << c_ << "C ";
        return *this;
    }
    A& operator--(int) {
        cout << c_ << "D ";
        return *this;
    }
    void operator+(A& b) {
        cout << c_ << "U ";
    }
};

int main()
{
    A a('a'), b('b');
    --++a-- ++ +b--;  // the culprit

    std::cout << std::endl;

    a.operator --( 0 ).operator ++( 0 ).operator ++().operator --().operator  + ( b.operator --( 0 ) );

    return 0;
}

它的输出是

bD aD aB aA aC aU 
bD aD aB aA aC aU 

您可以想象以函数形式编写的最后一个表达式,如表单

的后缀表达式
postfix-expression ( expression-list ) 

后缀表达式为

a.operator --( 0 ).operator ++( 0 ).operator ++().operator --().operator  +

,表达式列表是

b.operator --( 0 )

在C ++标准(5.2.2函数调用)中有说

  

8 [注:post fi x表达式和参数的评估   都没有相对于彼此的顺序。所有方面的影响   在输入函数之前对参数评估进行排序(参见   1.9)。 - 后注]

因此,它是实现定义的,首先是要评估参数还是后缀表达式。根据显示的输出,编译器首先评估参数,然后仅计算后缀表达式。

答案 1 :(得分:14)

我想说包含这样一个问题是错误的。

除非另有说明,否则以下摘录均来自N4618的§[intro.execution](我不认为这些内容在最近的草稿中有所改变)。

第16段的基本定义是sequenced beforeindeterminately sequenced等。

第18段说:

  

除非另有说明,否则对单个操作符的操作数和单个表达式的子表达式的评估是不合理的。

在这种情况下,您(间接)调用某些函数。那里的规则也相当简单:

  

当调用函数时(无论函数是否为内联函数),在执行每个表达式或语句之前,对与任何参数表达式或指定被调用函数的后缀表达式相关联的每个值计算和副作用进行排序。被调用函数的主体。对于每个函数调用F,对于在F内发生的每个评估A以及在F中不发生但在同一线程上作为相同信号处理程序(如果有)的一部分进行评估的每个评估B,A是   在A或B之前对B或B进行测序。

将其放入项目符号中可以更直接地表明顺序:

  1. 首先评估函数参数,以及任何指定被调用函数的内容。
  2. 评估函数本身。
  3. 评估另一个(子)表达式。
  4. 除非有东西启动一个线程以允许其他东西并行执行,否则不允许交错。

    那么,在我们通过运算符重载而不是直接调用函数之前,是否有任何变化?第19段说“不”:

      

    对被调用函数的执行的排序约束(如上所述)是被评估的函数调用的特征,无论调用函数的表达式的语法如何。

    §[expr] / 2也说:

      

    如上所述,重载运算符的使用被转换为函数调用   在13.5。重载运算符遵循第5章中指定的语法和求值顺序规则,但操作数类型和值类别的要求被函数调用的规则所取代。

    个体经营者

    您使用过的唯一一个对排序有一些不寻常要求的运算符是后递增和后递减。这些说(§[expr.post.incr] / 1:

      

    在修改操作数对象之前,对++表达式的值计算进行排序。对于不确定顺序的函数调用,后缀++的操作是单个评估。 [注意:因此,函数调用不应介入左值到右值的转换和与任何单个后缀++运算符相关的副作用。 - 后注]

    然而,最后,这几乎就是您可能期望的:如果您将x++作为参数传递给函数,该函数将接收先前的x值,但是如果x也在函数内的范围内,x将在函数体开始执行时具有递增的值。

    但是,+运算符未指定对其操作数的求值顺序。

    摘要

    使用重载运算符不会对表达式中的子表达式的求值执行任何排序,除了评估单个运算符是函数调用这一事实,并且具有任何其他函数调用的排序要求。

    更具体地说,在这种情况下,b--是函数调用的操作数,--++a-- ++是指定被调用函数的表达式(或者至少是函数所在的对象)调用 - --指定该对象内的函数。如上所述,未指定这两者之间的排序(operator +也未指定评估其左右操作数的顺序)。

答案 2 :(得分:7)

C ++标准中没有一些内容表明需要以这种方式评估事物。 C ++具有sequenced-before的概念,其中一些操作保证在其他操作之前发生。这是一个部分有序的集合;也就是说,sosome操作先于其他操作排序,两个操作在eath之前无法排序,如果a在b之前排序,b在c之前排序,则a在c之前排序。但是,有许多类型的操作没有按顺序排序的保证。在C ++ 11之前,有一个序列点的概念,它不完全相同但相似。

很少有运营商(我认为只有,&&?:||)保证其参数之间的序列点(即便如此,直到C ++ 17,当运算符过载时,此保证不存在)。特别是,添加不保证任何此类事物。编译器可以先评估左侧,先评估右侧,或者(我认为)甚至同时评估它们。

有时,更改优化选项可能会更改结果或更改编译器。显然你没有看到;这里没有任何保证。

答案 3 :(得分:4)

运算符优先级和关联性规则仅用于将表达式从原始的“表达式中的运算符”表示法转换为等效的“函数调用”格式。在转换之后,您最终会得到一堆嵌套函数调用,这些调用以通常的方式处理。特别是,参数评估的顺序是未指定的,这意味着无法首先评估“binary +”调用的哪个操作数。

另外,请注意,在您的情况下,二进制+被实现为成员函数,这会在其参数之间产生某些表面不对称:一个参数是“常规”参数,另一个参数是this。也许某些编译器“首选”首先评估“常规”参数,这导致b--在测试中首先被评估(如果实现二进制文件,可能会从同一编译器中获得不同的排序{{1作为一个独立的功能)。或者根本没关系。

例如,Clang从评估第一个操作数开始,稍后留下+

答案 4 :(得分:-1)

考虑c ++中运算符的优先级:

  1. a ++ a--后缀/后缀增量和减量。从左到右
  2. ++ a --a前缀增量和减量。从右到左
  3. a + b a-b加法和减法。左到右
  4. 即使没有括号,您也可以轻松阅读表达式:

    --++a--+++b--;//will follow with
    --++a+++b--;//and so on
    --++a+b--;
    --++a+b;
    --a+b;
    a+b;
    

    并且在变量和表达式的顺序评估方面不要忘记基本差异前缀和后缀运算符))