序列点歧义,未定义行为?

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的参数,然后计算buffer(..)本身,最后计算数组运算符的索引参数。直观地讲,这对我来说是有意义的。

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

len_: 10
len_: 10

我想clang和VS2013会在降低到buffer(...)之前评估所有可能的情况。对我来说,这意义不太直观。

我猜我的问题的要点是,这是否是未定义行为的明显情况。

编辑:感谢澄清,应该使用未指定行为这个术语。


3
如果有人写了 "buffer(len+1)[len_ = len] = '\0';",我会建议他们对其进行修改 :) - paulm
2个回答

18
这是未指定的行为, len_ = lenbuffer()体的执行顺序是不确定的序列,这意味着一个将在另一个之前执行,但没有指定哪个顺序,但有一种排序,因此评估不能重叠,因此没有未定义的行为。这意味着gccclangVisual 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 issue 158: N4228 Refining Expression Evaluation Order for Idiomatic C++,这正在考虑用于C++17,但似乎存在争议 根据对该主题进行投票的反应。该论文涵盖了来自《C++程序设计语言》第四版的一个更加复杂的案例。这表明即使是有深入C++经验的人也可能会被绊倒。


12

嗯,并不是未定义行为。这是一种未指定的行为。

未指定表达式len_ = len是在buffer(len+1)之前还是之后被评估。根据您描述的输出,g++先评估buffer(len+1),而clang先评估len_ = len

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接