27

我隐约记得在某处读过,如果复合表达式中的多个操作数修改同一个对象,则这是未定义的行为。

我相信下面的代码中显示了这个 UB 的一个示例,但是我已经在 g++、clang++ 和 Visual Studio 上编译,它们都打印出相同的值,并且似乎无法在不同的编译器中产生不可预测的值。

#include <iostream>

int a( int& lhs ) { lhs -= 4; return lhs; }
int b( int& lhs ) { lhs *= 7; return lhs; }
int c( int& lhs ) { lhs += 1; return lhs; }
int d( int& lhs ) { lhs += 2; return lhs; }
int e( int& lhs ) { lhs *= 3; return lhs; }

int main( int argc, char **argv )
{
    int i = 100;
    int j = ( b( i ) + c( i ) ) * e( i ) / a( i ) * d( i );

    std::cout << i << ", " << j << std::endl;

    return 0;
}

这种行为是未定义的,还是我以某种方式想出了一个实际上未定义的假定 UB 的描述?

如果有人可以发布此 UB 的示例,甚至可以指出我在 C++ 标准中说它是 UB 的位置,我将不胜感激。

4

3 回答 3

34

不它不是。未定义的行为在这里是没有问题的(假设int算术没有溢出):所有修改i都由序列点隔离(使用 C++03 术语)。每个函数的入口处都有一个序列点,出口处有一个序列点。

此处未指定行为。

您的代码实际上遵循与通常用于说明未定义未指定行为之间区别的经典示例相同的模式。考虑这个

int i = 1;
int j = ++i * ++i;

人们经常会声称在这个例子中“结果不依赖于评估的顺序,因此j必须始终为 6”。这是一个无效的声明,因为行为是未定义的。

然而在这个例子中

int inc(int &i) { return ++i; }

int i = 1;
int j = inc(i) * inc(i);

该行为在形式上只是未指定的。即,未指定评估顺序。但是,由于表达式的结果根本不依赖于求值顺序,j所以保证总是以6. 这是一个示例,说明通常危险的未指定行为组合如何导致完美定义的结果。

在您的情况下,表达式的结果在很大程度上取决于评估顺序,这意味着结果将是不可预测的。然而,这里没有未定义的行为,即不允许该程序格式化您的硬盘驱动器。只允许在 中产生不可预知的结果j

PS 同样,您的表达式的某些评估场景可能会导致有符号整数溢出(我没有全部分析过),这本身会触发未定义的行为。因此,仍有可能导致表达式中出现未定义行为的未指定行为。但这可能不是你的问题。

于 2012-12-22T17:52:19.903 回答
12

不,它不是未定义的行为。

但它确实调用了未指定的行为

这是因为未指定评估子表达式的顺序。

int j = ( b( i ) + c( i ) ) * e( i ) / a( i ) * d( i );

在上面的表达式中,子表达式:

b(i)
c(i)
e(i)
a(i)
d(i)

可以按任何顺序进行评估。因为它们都有副作用,所以结果将取决于此顺序。

如果将表达式划分为所有子表达式(这是伪代码)
,那么您可以看到所需的任何排序。上述表达式不仅可以按任何顺序完成,而且可能与更高级别的子表达式交错(只有几个约束)。

tmp_1 = b(i)           // A
tmp_2 = c(i)           // B
tmp_3 = e(i)           // C
tmp_4 = a(i)           // D
tmp_5 = d(i)           // E

tmp_6 = tmp_1 + tmp_2  // F   (Happens after A and B)
tmp_7 = tmp_6 * tmp_3  // G   (Happens after C and F)
tmp_8 = tmp_7 / tmp_4  // H   (Happens after D and G)
tmp_9 = tmp_8 * tmp_5  // I   (Happens after E and H)

int j = tmp_9;         // J   (Happens after I)
于 2012-12-22T17:58:35.603 回答
8

它不是未定义的行为,但它具有未指定的结果:唯一修改的对象是i通过传递给函数的引用。但是,对函数的调用引入了序列点(我没有 C++ 2011:它们在那里被称为不同的东西),即表达式中的多次更改不会导致未定义的行为的问题。

但是,未指定计算表达式的顺序。因此,如果评估顺序发生变化,您可能会得到不同的结果。这不是未定义的行为:结果是所有可能的评估顺序之一。未定义的行为意味着程序可以按照它想要的任何方式运行,包括为所讨论的表达式生成“预期”(程序员预期的)结果,同时破坏所有其他数据。

于 2012-12-22T17:59:21.650 回答