17

很抱歉再次打开这个话题,但是考虑这个话题本身已经开始给我一个未定义的行为。想要进入定义明确的行为区域。

给定

int i = 0;
int v[10];
i = ++i;     //Expr1
i = i++;     //Expr2
++ ++i;      //Expr3
i = v[i++];  //Expr4

我认为上述表达式(按此顺序)为

operator=(i, operator++(i))    ; //Expr1 equivalent
operator=(i, operator++(i, 0)) ; //Expr2 equivalent
operator++(operator++(i))      ; //Expr3 equivalent
operator=(i, operator[](operator++(i, 0)); //Expr4 equivalent

现在谈到这里的行为是来自C++ 0x的重要引述。

$1.9/12-“表达式(或子表达式)的评估通常包括值计算(包括确定对象的身份以进行左值评估和获取先前分配给对象以进行右值评估的值)和副作用的启动。”

$1.9/15-“如果标量对象的副作用相对于同一标量对象的另一个副作用使用同一标量对象的值的值计算是未排序的,则行为未定义。”

[注意:与不同参数表达式相关的值计算和副作用是无序的。——尾注]

$3.9/9-“算术类型 (3.9.1)、枚举类型、指针类型、指向成员类型的指针 (3.9.2)、std::nullptr_t 和这些类型的 cv 限定版本 (3.9.3) 统称为标量类型。”

  • 在 Expr1 中,表达式i(第一个参数)的评估相对于表达式的评估operator++(i)(具有副作用)是无序的。

    因此 Expr1 具有未定义的行为。

  • 在 Expr2 中,表达式i(第一个参数)的评估相对于表达式operator++(i, 0)(具有副作用)的评估是无序的。

    因此 Expr2 具有未定义的行为。

  • 在 Expr3 中,operator++(i)需要在调用外部参数之前完成对单独参数的评估operator++

    因此,Expr3 具有明确定义的行为。

  • 在 Expr4 中,表达式i(第一个参数)的计算相对于operator[](operator++(i, 0)(有副作用)的计算是无序的。

    因此 Expr4 具有未定义的行为。

这种理解正确吗?


PS OP中分析表达式的方法不正确。这是因为,正如@Potatoswatter 所指出的那样 - “第 13.6 条不适用。请参阅 13.6/1 中的免责声明,“这些候选函数参与 13.3.1.2 中所述的运算符重载解决过程,并且不用于其他目的。 “它们只是虚拟声明;不存在与内置运算符相关的函数调用语义。”

4

2 回答 2

15

本机运算符表达式不等同于重载运算符表达式。值与函数参数的绑定有一个序列点,这使得operator++()版本定义良好。但这对于本机类型的情况不存在。

在所有四种情况下,i在完整表达式中变化两次。由于 no ,, ||, or&&出现在表达式中,那是即时 UB。

§5/4:

在前一个和下一个序列点之间,一个标量对象的存储值最多只能通过表达式的评估修改一次。

为 C++0x 编辑(更新)

§1.9/15:

运算符的操作数的值计算在运算符结果的值计算之前排序。如果标量对象上的副作用相对于同一标量对象上的另一个副作用或使用同一标量对象的值的值计算是未排序的,则行为未定义。

但是请注意,值计算和副作用是两个不同的东西。如果++i等价于i = i+1+则为值计算,=为副作用。从 1.9/12 开始:

表达式(或子表达式)的评估通常包括值计算(包括确定对象的身份以进行左值评估和获取先前分配给对象以进行纯右值评估的值)和副作用的启动。

因此,尽管 C++0x 中的值计算比 C++03 中的排序更强,但副作用不是。除非另有排序,否则同一表达式中的两个副作用会产生 UB。

无论如何,值计算是按它们的数据依赖关系排序的,并且没有副作用,它们的评估顺序是不可观察的,所以我不确定为什么 C++0x 会麻烦说什么,但这只是意味着我需要阅读更多Boehm 和朋友们写的论文。

编辑#3:

感谢 Johannes 解决了我在 PDF 阅读器搜索栏中输入“sequenced”的懒惰问题。无论如何,我要去睡觉并起床进行最后两个编辑……对;v)。

§5.17/1 定义赋值运算符说

在所有情况下,赋值都在左右操作数的值计算之后和赋值表达式的值计算之前进行排序。

前增量运算符的§5.3.2/1 还说

如果 x 不是 bool 类型,则表达式 ++x 等价于 x+=1 [注意:参见……加法 (5.7) 和赋值运算符 (5.17)……]。

通过这个身份,++ ++ x是的简写(x +=1) +=1。所以,让我们解释一下。

  • 评估1远端 RHS 并下降到 parens。
  • 计算 的内部1和值 (prvalue) 和地址 (glvalue) x
  • 现在我们需要 += 子表达式的值。
    • 我们已经完成了该子表达式的值计算。
    • 赋值副作用必须在赋值的值可用之前排序!
  • 将新值分配给x,这与子表达式的 glvalue 和 prvalue 结果相同。
  • 我们现在已经走出困境了。整个表达式现在已简化为x +=1

因此, 1 和 3 是明确定义的,而 2 和 4 是未定义的行为,这是您所期望的。

通过在 N3126 中搜索“sequenced”,我发现的唯一另一个惊喜是 5.3.4/16,其中允许operator new在评估构造函数参数之前调用实现。这很酷。

编辑#4:(哦,我们编织了多么纠结的网)

Johannes 再次指出,i == ++i;在 glvalue (也就是地址)中,i是模棱两可地依赖于++i. glvalue 肯定是 的i,但我不认为 1.9/15 打算包含它,原因很简单,命名对象的 glvalue 是常量,实际上不能有依赖关系。

对于一个信息丰富的稻草人,请考虑

( i % 2? i : j ) = ++ i; // certainly undefined

这里,LHS 的左=值取决于对 prvalue 的副作用i。的地址i没有问题;的结果?:是。

也许一个很好的反例是

int i = 3, &j = i;
j = ++ i;

这里j有一个不同于(但相同于)的左值i。这是明确定义的,但i = ++i不是吗?这表示编译器可以应用于任何情况的微不足道的转换。

1.9/15 应该说

如果标量对象的副作用相对于同一标量对象的另一个副作用或使用同一标量对象的纯右值的值计算是未排序的,则行为未定义。

于 2010-10-04T04:09:27.580 回答
0

在考虑像上面提到的那些表达式时,我发现想象一台内存有互锁的机器很有用,这样读取内存位置作为读-修改-写序列的一部分将导致任何尝试的读取或写入,而不是结束写入序列,直到序列完成。这样的机器绝不是一个荒谬的概念。事实上,这样的设计可以简化许多多线程代码场景。另一方面,像“x=y++;”这样的表达式 如果 'x' 和 'y' 是对同一个变量的引用,并且编译器生成的代码执行了诸如 read-and-lock reg1=y; 之类的操作,那么在这种机器上可能会失败。reg2=reg1+1;写 x=reg1; 写入和解锁 y=reg2。

于 2011-11-01T14:55:31.007 回答