未测序值计算(a.k.a序列点)

时间:2010-10-04 04:06:16

标签: c++ language-lawyer side-effects sequence-points

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

鉴于

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-“对表达的评价   (或子表达式)一般而言   包括价值计算   (包括确定身份   左值评估的对象和   获取先前分配给的值   rvalue评估的对象)和   引发副作用。“

     

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

     

[注意:价值计算和方   与不同相关的影响   参数表达式未被排序。    - 后注]

     

$ 3.9 / 9-“算术类型(3.9.1),   枚举类型,指针类型,   指向成员类型的指针(3.9.2),   std :: nullptr_t和cv-qualified   这些类型的版本(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具有未定义的行为。

这种理解是否正确?


P.S。在OP中分析表达式的方法不正确。这是因为,作为@Potatoswatter,注意 - “第13.6条不适用。参见13.6 / 1中的免责声明,”这些候选函数参与13.3.1.2中描述的运算符重载解析过程,并且不用于其他目的。 “它们只是虚拟声明;内置运算符不存在函数调用语义。”

2 个答案:

答案 0 :(得分:15)

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

在所有四种情况下,i在完整表达式中更改两次。由于表达式中不显示,||&&,因此即时UB。

§5/ 4:

  

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

编辑C ++ 0x(更新)

§1.9/ 15:

  

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

但请注意,值计算和副作用是两个截然不同的事情。如果++i等同于i = i+1,那么+是值计算,=是副作用。从1.9 / 12:

  

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

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

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

编辑#3:

感谢约翰内斯应对我的懒惰,在我的PDF阅读器搜索栏中输入“已排序”。无论如何,我上床睡觉并且最后两次编辑......对吧; v)。

§5.17/ 1定义赋值运算符

  

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

关于preincrement运算符的§5.3.2/ 1也说

  

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

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

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

所以,那么 1和3是明确定义的,2和4是未定义的行为,这是你期望的。

通过在N3126中搜索“已排序”而发现的唯一其他惊喜是5.3.4 / 16,其中允许实现在评估构造函数参数之前调用operator new。那很酷。

编辑#4 :(哦,我们编织的网络错综复杂)

约翰尼斯再次注意到i == ++i; i++i的glvalue(a.k.a.地址)模糊地依赖于i。 glvalue肯定是 a ( i % 2? i : j ) = ++ i; // certainly undefined 值,但我不认为1.9 / 15是为了包含它,原因很简单,因为命名对象的glvalue是常量,而不能实际上有依赖。

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

=

此处,i的LHS的glvalue取决于i的prvalue的副作用。 ?:的地址不存在问题; int i = 3, &j = i; j = ++ i; 的结果是。

也许一个好的反例是

j

此处i的glvalue与i = ++i不同(但相同)。这是明确定义的,但{{1}}不是吗?这代表了一个简单的转换,编译器可以应用于任何情况。

1.9 / 15应该说

  

如果相对于同一标量对象的另一个副作用或使用相同标量对象的 prvalue 的值计算,标量对象的副作用未被排序,则行为未定义。< / p>

答案 1 :(得分:0)

在考虑如上所述的表达式时,我发现想象一个内存具有互锁的机器是有用的,这样作为读 - 修改 - 写序列的一部分读取内存位置将导致任何尝试的读或写,除了结束对序列的写入,直到序列完成为止。这样的机器几乎不是一个荒谬的概念;实际上,这样的设计可以简化许多多线程代码方案。另一方面,表达式如“x = y ++;”如果'x'和'y'是对同一个变量的引用,并且编译器生成的代码执行类似read-and-lock reg1 = y的操作,则可能在这样的机器上失败; REG2 = REG1 + 1;写x = reg1;写和解锁y = reg2。这对处理器来说是一个非常合理的代码序列,其中写入新计算的值会产生流水线延迟,但如果y被别名化为同一个变量,写入x会锁定处理器。