20

今天我遇到了一些在 clang++ (3.7-git)、g++ (4.9.2) 和 Visual Studio 2013 上表现出不同行为的代码。经过一些简化后,我想出了这个突出问题的片段:

#include <iostream>
using namespace std;

int len_ = -1;

char *buffer(int size_)
{
    cout << "len_: " << len_ << endl;
    return new char[size_];
}

int main(int argc, char *argv[])
{
    int len = 10;
    buffer(len+1)[len_ = len] = '\0';
    cout << "len_: " << len_ << endl;
}

g++ (4.9.2) 给出了这个输出:

len_: -1
len_: 10

因此,g++ 将参数评估为缓冲区,然后是 buffer(..) 本身,然后将索引参数评估为数组运算符。直觉上,这对我来说很有意义。

clang (3.7-git) 和 Visual Studio 2013 都给出:

len_: 10
len_: 10

我想clang和VS2013会在它下降到缓冲区(..)之前评估所有可能的事情。这对我来说不太直观。

我想我的问题的要点是这是否是未定义行为的明显案例。

编辑:感谢您清除此问题,未指明的行为是我应该使用的术语。

4

2 回答 2

18

这是未指定的行为len_ = len相对于 的主体的执行顺序不确定buffer(),这意味着一个将在另一个之前执行,但未指定哪个顺序但存在排序,因此评估不能重叠,因此没有未定义的行为。这意味着gccclang并且Visual Studio都是正确的。另一方面,未排序的评估允许重叠评估,这可能导致未定义的行为,如下所述。

草案 C++11 标准部分1.9 [intro.execution]

[...]调用函数(包括其他函数调用)中的每个求值,如果在被调用函数的主体执行之前或之后没有特别排序,则相对于被调用函数的执行而言,其排序是不确定的。 9 [ ...]

不确定的排序在此之前稍作介绍,并说:

[...]当 A 在 B 之前排序或 B 在 A 之前排序时,评估 A 和 B 的排序不确定,但未指定哪个。[注意:不确定顺序的评估不能重叠,但可以先执行。——尾注]

这与未排序的评估不同:

[...]如果 A 未在 B 之前排序且 B 未在 A 之前排序,则 A 和 B 未排序。[注意:未排序评估的执行可以重叠。——尾注][...]

这可能导致未定义的行为(强调我的):

除非另有说明,否则单个运算符的操作数和单个表达式的子表达式的求值是未排序的。[注意:在程序执行期间多次评估的表达式中,其子表达式的未排序和不确定排序的评估不需要在不同的评估中一致地执行。—尾注] 运算符的操作数的值计算在运算符结果的值计算之前排序。如果标量对象的副作用相对于同一标量对象的另一个副作用或使用同一标量对象的值的值计算是未排序的,则行为未定义[...]

预 C++11

在 C++11之前,子表达式的求值顺序也未指定,但它使用序列点而不是排序。在这种情况下,函数入口和函数出口处有一个序列点,可确保没有未定义的行为。从部分1.9

[...]函数入口和函数出口处的序列点(如上所述)是评估的函数调用的特征,无论调用函数的表达式的语法可能是什么。

确定评估顺序

根据您的观点和期望,每个编译器做出的不同选择可能看起来不直观。确定评估顺序的主题是EWG 第 158 期:N4228 Refining Expression Evaluation Order for Idiomatic C++的主题,该主题正在考虑用于 C++17,但基于对该主题的民意调查的反应似乎存在争议。该论文涵盖了“The C++ Programming Language”第 4 版中的一个更复杂的案例。这表明即使是那些在 C++ 方面有丰富经验的人也会被绊倒。

于 2015-04-08T11:52:55.950 回答
12

好吧,不,这不是未定义行为的情况。这是一个未指定行为的案例。

未指定表达式len_ = len将在之前还是之后进行评估buffer(len+1)。从您描述的输出中,g++buffer(len+1)先评估,然后 clanglen_ = len先评估。

这两种可能性都是正确的,因为这两个子表达式的求值顺序是未指定的。将评估两个表达式(因此行为不符合未定义的条件),但标准未指定顺序。

于 2015-04-08T11:54:35.777 回答