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



为什么下面的打印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我意识到编译器并不真正了解运算符优先级或关联性,而是只关心语言语法,但这应该以任何一种方式理解这一点。

表达式语句

--++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 [注:后缀表达式和参数的计算 都是相对于彼此未排序的。所有副作用 参数计算在输入函数之前排序(请参阅 1.9). —尾注]

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

我会说他们包含这样的问题是错误的。

除说明外,以下摘录均来自N4618的§[intro.execution](我认为这些东西在最近的草案中没有任何变化)。

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

第18段说:

除非另有说明,否则对单个运算符的操作数和单个表达式的子表达式的计算是无序的。

在本例中,您(间接)调用了一些函数。那里的规则也相当简单:

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

将其放入项目符号中以更直接地指示顺序:

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

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

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

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

§[expr]/2 还说:

重载运算符的使用转换为函数调用,如前所述 在 13.5 中。重载运算符遵守条款 5 中指定的语法和求值顺序规则,但操作数类型和值类别的要求将替换为函数调用规则。

个人操作员

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

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

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

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

总结

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

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

C++标准中没有说事物需要以这种方式进行评估。C++具有先于排序的概念,其中某些操作保证在其他操作之前发生。这是一个偏序集合;也就是说,Sosome操作先于其他操作进行排序,两个操作不能先于其他操作进行排序,如果A在B之前排序,B在C之前排序,则A在C之前排序。但是,有许多类型的操作没有先序保证。在C++11之前,有一个序列点的概念,它不完全相同,但相似。

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

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

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

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

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

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

  1. a++ a-- 后缀/后缀递增和递减。从左到右
  2. ++a --前缀递增和递减。从右到左
  3. a+b a-b 加法和减法。从左到右

将列表牢记在心,即使没有括号,您也可以轻松阅读表达式:

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

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

相关内容

  • 没有找到相关文章

最新更新