未排序值计算(也称为序列点)

19

很抱歉再次开启此话题,但是思考此话题本身已经开始让我陷入未定义的行为。想要进入明确定义行为的领域。

给定:

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- "如果标量对象的副作用与另一个标量对象相对无序, 或者与使用同一标量对象的值计算有关,则行为未定义。"

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

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

  • 在Expr1中,表达式i(第一个参数)的评估与具有副作用的表达式operator++(i)(后置递增)的评估是无序的。

    因此,Expr1的行为未定义。

  • 在Expr2中,表达式i(第一个参数)的评估与具有副作用的表达式operator++(i, 0)(前置递增,第二个参数为零)的评估是无序的。

    因此,Expr2的行为未定义。

  • 在Expr3中,在调用外部operator++之前必须完成单参数operator++(i)的评估。

    因此,Expr3的行为已定义。

  • 在Expr4中,表达式i(第一个参数)的评估与具有副作用的operator[](operator++(i, 0))(后置递增,第二个参数为零)的评估是无序的。

    因此,Expr4的行为未定义。

这种理解正确吗?


P.S.像OP所述分析表达式的方法是不正确的。正如@Potatoswatter所指出的那样:“第13.6条不适用。请参见13.6/1中的免责声明,“这些候选函数参与操作符重载过程,如13.3.1.2中所述,并且仅用于其他目的。”它们只是虚拟声明;内置运算符没有函数调用语义。”


1
好问题。我会关注答案。 - Arun
@Chubsdad:我同意@James McNellis在他的回答中所说的话(之后他删除了)。在C++0x中,所有这4个表达式都会引发UB [依我看]。我认为你应该在csc++(comp.std.c++)上提出这个问题。:) - Prasoon Saurav
这是因为与 ++ [inner] 和 ++ [outer] 相关的副作用在彼此之间没有排序(尽管值计算是有序的)。 :) - Prasoon Saurav
Johannes Schaub - litb:在此期间,请告诉我们这是否是正确的可视化表达方式,或者我是否在这种思考中错过了任何情况(即使在实际情况下不存在本机类型的运算符函数调用)除了$13.6/18。 - Chubsdad
例如,这种思考方式也解释了++i = 0(operator=(operator++(i), 0))作为良好定义的行为。 - Chubsdad
显示剩余4条评论
2个回答

16
Native operator expressions and overloaded operator expressions are not equivalent. When values are bound to function arguments, there is a sequence point which makes the operator++() versions well-defined. However, this does not exist for the native-type case.
In all four cases, the variable i changes twice within the full-expression. Since there are no ,, ||, or && operators in the expressions, this results in undefined behavior (UB).
According to §5/4 of the C++ standard: "Between the previous and next sequence point a scalar object shall have its stored value modified at most once by the evaluation of an expression."
For C++0x, §1.9/15 states: "The value computations of the operands of an operator are sequenced before the value computation of the result of the operator. If a side effect on a scalar object is unsequenced relative to either another side effect on the same scalar object or a value computation using the value of the same scalar object, the behavior is undefined."
Value computations and side effects are distinct. If ++i is equal to i = i+1, then + is the value computation and = is the side effect. As per 1.9/12, "evaluation of an expression (or a sub-expression) in general includes both value computations ... and initiation of side effects." Therefore, although value computations are more strongly sequenced in C++0x than in C++03, two side effects in the same expression, unless otherwise sequenced, produce UB.
§5.17/1 defining the assignment operators says, "In all cases, the assignment is sequenced after the value computation of the right and left operands, and before the value computation of the assignment expression." Also, §5.3.2/1 on the preincrement operator says: "If x is not of type bool, the expression ++x is equivalent to x+=1 [Note: see … addition (5.7) and assignment operators (5.17) …]."
Hence ++ ++ x is shorthand for (x += 1) += 1, and this is well-defined for steps 1 and 3, but undefined for steps 2 and 4.
The glvalue (address) of a named object is constant and cannot have dependencies, so in i == ++i;, the glvalue of i is not intended to be included as an ambiguous dependency by 1.9/15.
( i % 2? i : j ) = ++ i; // certainly undefined

这里,赋值运算符左侧的glvalue取决于对prvalue i 的副作用。 i的地址并不成问题;而是会受到?:表达式的影响。

也许一个好的反例是

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

这里的j具有与i不同但相同的glvalue。这是明确定义的,但是i = ++i却没有定义。这代表了编译器可以应用于任何情况的微不足道的转换。

1.9/15 应该说:

如果标量对象上的副作用在与同一标量对象的另一个副作用或使用同一标量对象的prvalue进行值计算时无序关联,则行为未定义。


2
@Potatoswatter:在C++0x中调用UB的四个表达式 - Prasoon Saurav
1
@Potatoswatter:我不是很确定,但仍然认为++ ++ii = ++i在C++0x中都是未定义行为。请阅读James Kanze的帖子[在那个讨论的结尾处]。;) - Prasoon Saurav
这也是我最初的感觉。但是,Usenet讨论论坛似乎得出结论,expr1的行为是非法的。@litb也为此辩护。 - Chubsdad
1
我已找到支持此答案的两个DR:http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_defects.html#637 和 http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_defects.html#222。 - Johannes Schaub - litb
@PrasoonSaurav 在C++0x中,这四个表达式都会引发未定义行为 你错了,因为 i = ++i 在c++0x中是完全被定义的。https://dev59.com/6WQm5IYBdhLWcg3wtAw_ - Kolyunya
显示剩余36条评论

0
在考虑像上述这样的表达式时,我发现想象一个内存具有互锁功能的机器是很有用的。这样,在读取-修改-写入序列的过程中,如果尝试进行除序列结束写入之外的任何读取或写入操作,都会被阻塞,直到序列完成。这样的机器并不是一个荒谬的概念;事实上,这种设计可以简化许多多线程代码场景。另一方面,如果'x'和'y'是对同一变量的引用,并且编译器生成的代码执行类似于读取并锁定reg1=y; reg2=reg1+1; 写入x=reg1; 写入并解锁y=reg2的操作,那么像"x=y++;"这样的表达式在这样的机器上可能会失败。如果写入新计算出的值会导致流水线延迟,那么这将是处理器上非常合理的代码序列,但如果y与相同的变量别名,则对x的写入将锁定处理器。

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