如果复合表达式中的多个操作数修改同一个对象,它是否是未定义的行为?

cto*_*tor 27 c++ undefined-behavior language-lawyer unspecified-behavior c++11

我依稀记得在复合表达式中的多个操作数修改同一个对象时,在某处读取它是未定义的行为.

我相信下面的代码中显示了这个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;
}
Run Code Online (Sandbox Code Playgroud)

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

如果有人可以发布这个UB的一个例子,我甚至可能会指出我在C++标准中说它是UB的地方.

AnT*_*AnT 34

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

这里的行为未指定.

您的代码实际上遵循与经常用于说明未定义未指定行为之间差异的经典示例相同的模式.考虑一下

int i = 1;
int j = ++i * ++i;
Run Code Online (Sandbox Code Playgroud)

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

但是在这个例子中

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

int i = 1;
int j = inc(i) * inc(i);
Run Code Online (Sandbox Code Playgroud)

这种行为在形式上只是未指明.即,评估顺序未指定.但是,由于表达式的结果完全不依赖于评估的顺序,j因此保证始终最终为6.这是一个通常危险的未指明行为组合如何导致完美定义结果的示例.

在您的情况下,表达式的结果严重依赖于评估的顺序,这意味着结果将是不可预测的.然而,这里没有未定义的行为,即程序不允许格式化您的硬盘.只允许产生不可预测的结果j.

PS再次,可能会发现表达式的某些评估方案导致有符号整数溢出(我没有全部分析),这本身就会触发未定义的行为.因此,仍然存在未指定行为导致表达式中未定义行为的可能性.但这可能不是你的问题所在.

  • @GManNickG但是函数调用之间没有序列点.如果你调用多个函数来修改同一个变量,在函数调用之间没有一个序列点,那还不是UB吗?或者至少是未指定的,因为无法保证首先调用哪个函数? (2认同)
  • @David:'b(i)+ c(i)`中不需要序列点,因为它本身不会对`i`进行修改.存在的序列点位于每个函数的入口和存在处,这确保了"i"的每个实际修改都被序列点包围并因此彼此隔离. (2认同)

Mar*_*ork 12

没有它没有未定义的行为.

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

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

int j = ( b( i ) + c( i ) ) * e( i ) / a( i ) * d( i );
Run Code Online (Sandbox Code Playgroud)

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

b(i)
c(i)
e(i)
a(i)
d(i)
Run Code Online (Sandbox Code Playgroud)

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

如果将表达式划分为所有子表达式(这是伪代码),
那么您可以看到所需的任何顺序.上述表达式不仅可以以任何顺序完成,而且可以与更高级别的子表达式(仅具有少量约束)交织.

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)
Run Code Online (Sandbox Code Playgroud)


Die*_*ühl 8

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

但是,未指定计算表达式的顺序.因此,如果评估顺序发生变化,您可能会得到不同的结果.这不是未定义的行为:结果是所有可能的评估顺序之一.未定义的行为意味着程序可以以其想要的任何方式运行,包括在压缩所有其他数据时生成所需表达式的"预期"(由程序员预期)结果.