为什么 '--++a- - ++ +b--' 按这个顺序计算?

Why is '--++a-​- ++ +b--' evaluated in this order?

为什么下面打印 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
}

据我所知,编译器解析表达式的方式如下:

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

因为我在一个测试中被问到这个原则上应该是implementation/compiler-agnostic,C++标准中有什么规定了以上内容行为,还是真的未指定? 有人可以引用标准的摘录来证实这两者吗?考试出这样的题是不是错了?

1 我意识到编译器并不真正了解运算符优先级或结合性,它只关心语言语法,但这应该能说明问题。

C++ 标准中没有任何内容说需要以这种方式求值。 C++ 有 sequenced-before 的概念,保证某些操作先于其他操作发生。这是一个偏序集;也就是说,有些操作排在其他操作之前,两个操作不能排在另一个操作之前,如果 a 排在 b 之前,b 排在 c 之前,那么 a 排在 c 之前。但是,有许多类型的操作没有先序保证。在C++11之前,有一个sequence point的概念,虽然不完全一样,但是很相似。

很少有运算符(我相信只有 ,&&?:||)保证它们的参数之间有一个序列点(即便如此,在 C++17 之前,这种保证在运算符重载时不存在)。特别是,添加不保证任何此类事情。编译器可以自由地先评估左侧,先评估右侧,或者(我认为)甚至同时评估它们。

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

运算符优先级和结合性规则仅用于将表达式从原始 "operators in expression" 表示法转换为等效的 "function call" 格式。转换后,您将得到一堆嵌套的函数调用,它们以通常的方式处理。特别是,参数评估的顺序是未指定的,这意味着没有办法说出 "binary +" 调用的哪个操作数将首先被评估。

此外,请注意,在您的情况下,二进制 + 是作为成员函数实现的,这会在其参数之间造成某些表面上的不对称:一个参数是 "regular" 参数,另一个是 this.也许一些编译器 "prefer" 首先评估 "regular" 参数,这就是导致 b-- 在你的测试中首先被评估的原因(如果你实现你的,你可能最终会从同一个编译器得到不同的顺序二进制 + 作为独立函数)。或者也许根本不重要。

例如,Clang 从计算第一个操作数开始,将 b-- 留到后面。

表达式语句

--++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 [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 (see 1.9). —end note]

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

我会说他们提出这样的问题是错误的。

除特别注明外,以下摘录均来自 N4618 的 §[intro.execution](我认为这些内容在最近的草稿中没有任何变化)。

第16段有sequenced beforeindeterminately sequenced等基本定义

第 18 段说:

Except where noted, evaluations of operands of individual operators and of subexpressions of individual expressions are unsequenced.

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

When calling a function (whether or not the function is inline), every value computation and side effect associated with any argument expression, or with the postfix expression designating the called function, is sequenced before execution of every expression or statement in the body of the called function. For each function invocation F, for every evaluation A that occurs within F and every evaluation B that does not occur within F but is evaluated on the same thread and as part of the same signal handler (if any), either A is sequenced before B or B is sequenced before A.

将其放入要点以更直接地指示顺序:

  1. 首先评估函数参数,以及指定被调用函数的任何内容。
  2. 评估函数本身的主体。
  3. 计算另一个(子)表达式。

不允许交错,除非某些东西启动一个线程以允许其他东西并行执行。

那么,在我们通过运算符重载而不是直接调用函数之前,这有什么变化吗?第 19 段说 "No":

The sequencing constraints on the execution of the called function (as described above) are features of the function calls as evaluated, whatever the syntax of the expression that calls the function might be.

§[expr]/2 还说:

Uses of overloaded operators are transformed into function calls as described in 13.5. Overloaded operators obey the rules for syntax and evaluation order specified in Clause 5, but the requirements of operand type and value category are replaced by the rules for function call.

个体经营者

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

The value computation of the ++ expression is sequenced before the modification of the operand object. With respect to an indeterminately-sequenced function call, the operation of postfix ++ is a single evaluation. [ Note: Therefore, a function call shall not intervene between the lvalue-to-rvalue conversion and the side effect associated with any single postfix ++ operator. —end note ]

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

但是,+ 运算符没有指定其操作数的计算顺序。

总结

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

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

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

  1. a++ a-- Suffix/postfix 自增自减。从左到右
  2. ++a --a 前缀自增自减。从右到左
  3. a+b a-b 加减法。从左到右

记住这个列表,即使没有括号,您也可以轻松阅读表达式:

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

不要忘记前缀和后缀运算符在变量和表达式的顺序评估方面的本质区别))