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

发布于 2024-09-25 19:10:12 字数 1758 浏览 9 评论 0原文

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

鉴于

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++ 之前完成。 code> 被调用。

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

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

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

这种理解正确吗?


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

Sorry for opening this topic again, but thinking about this topic itself has started giving me an Undefined Behavior. Want to move into the zone of well-defined behavior.

Given

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

I think of the above expressions (in that order) as

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

Now coming to behaviors here are the important quotes from C++ 0x.

$1.9/12- "Evaluation of an expression
(or a sub-expression) in general
includes both value computations
(including determining the identity of
an object for lvalue evaluation and
fetchinga value previously assigned to
an object for rvalue evaluation) and
initiation of side effects."

$1.9/15- "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."

[ Note: Value computations and side
effects associated with different
argument expressions are unsequenced.
—end note ]

$3.9/9- "Arithmetic types (3.9.1),
enumeration types, pointer types,
pointer to member types (3.9.2),
std::nullptr_t, and cv-qualified
versions of these types (3.9.3) are
collectively called scalar types."

  • In Expr1, the evaluation of the expression i (first argument), is unsequenced with respect to the evaluation of the expession operator++(i) (which has a side effect).

    Hence Expr1 has undefined behavior.

  • In Expr2, the evaluation of the expression i (first argument), is unsequenced with respect to the evaluation of the expession operator++(i, 0) (which has a side effect)'.

    Hence Expr2 has undefined behavior.

  • In Expr3, the evaluation of the lone argument operator++(i) is required to be complete before the outer operator++ is called.

    Hence Expr3 has well defined behavior.

  • In Expr4, the evaluation of the expression i (first argument) is unsequenced with respect to the evaluation of the operator[](operator++(i, 0) (which has a side effect).

    Hence Expr4 has undefined behavior.

Is this understanding correct?


P.S. The method of analyzing the expressions as in OP is not correct. This is because, as @Potatoswatter, notes - "clause 13.6 does not apply. See the disclaimer in 13.6/1, "These candidate functions participate in the operator overload resolution process as described in 13.3.1.2 and are used for no other purpose." They are just dummy declarations; no function-call semantics exist with respect to built-in operators."

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

计㈡愣 2024-10-02 19:10:12

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

在所有四种情况下,i 在完整表达式中更改了两次。由于表达式中没有出现 ||&&,因此即为即时 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 的简写。那么,让我们解释一下。

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

所以, 1 和 3 是明确定义的,而 2 和 4 是未定义的行为,正如您所期望的那样。

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

编辑#4:(哦,我们编织了多么混乱的网络)

Johannes 再次指出,在 i == ++i; 中,i 的泛左值(又名地址)是模糊地依赖于++i。泛左值肯定是 ia值,但我不认为 1.9/15 打算包含它,原因很简单,命名对象的泛左值是常量,并且实际上不能具有依赖关系。

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

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

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

也许一个很好的反例是

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

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

1.9/15 应该说

如果标量对象上的副作用相对于同一标量对象上的另一个副作用或使用同一标量对象的纯右值的值计算而言是无序的,则行为未定义。< /p>

Native operator expressions are not equivalent to overloaded operator expressions. There is a sequence point at the binding of values to function arguments, which makes the operator++() versions well-defined. But that doesn't exist for the native-type case.

In all four cases, i changes twice within the full-expression. Since no ,, ||, or && appear in the expressions, that's instant UB.

§5/4:

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.

Edit for C++0x (updated)

§1.9/15:

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.

Note however that a value computation and a side effect are two distinct things. If ++i is equivalent to i = i+1, then + is the value computation and = is the side effect. From 1.9/12:

Evaluation of an expression (or a sub-expression) in general includes both value computations (including determining the identity of an object for glvalue evaluation and fetching a value previously assigned to an object for prvalue evaluation) and initiation of side effects.

So although the value computations are more strongly sequenced in C++0x than C++03, the side effects are not. Two side effects in the same expression, unless otherwise sequenced, produce UB.

Value computations are ordered by their data dependencies anyway and, side effects absent, their order of evaluation is unobservable, so I'm not sure why C++0x goes to the trouble of saying anything, but that just means I need to read more of the papers by Boehm and friends wrote.

Edit #3:

Thanks Johannes for coping with my laziness to type "sequenced" into my PDF reader search bar. I was going to bed and getting up on the last two edits anyway… right ;v) .

§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) …].

By this identity, ++ ++ x is shorthand for (x +=1) +=1. So, let's interpret that.

  • Evaluate the 1 on the far RHS and descend into the parens.
  • Evaluate the inner 1 and the value (prvalue) and address (glvalue) of x.
  • Now we need the value of the += subexpression.
    • We're done with the value computations for that subexpression.
    • The assignment side effect must be sequenced before the value of assignment is available!
  • Assign the new value to x, which is identical to the glvalue and prvalue result of the subexpression.
  • We're out of the woods now. The whole expression has now been reduced to x +=1.

So, then 1 and 3 are well-defined and 2 and 4 are undefined behavior, which you would expect.

The only other surprise I found by searching for "sequenced" in N3126 was 5.3.4/16, where the implementation is allowed to call operator new before evaluating constructor arguments. That's cool.

Edit #4: (Oh, what a tangled web we weave)

Johannes notes again that in i == ++i; the glvalue (a.k.a. the address) of i is ambiguously dependent on ++i. The glvalue is certainly a value of i, but I don't think 1.9/15 is intended to include it for the simple reason that the glvalue of a named object is constant, and cannot actually have dependencies.

For an informative strawman, consider

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

Here, the glvalue of the LHS of = is dependent on a side-effect on the prvalue of i. The address of i is not in question; the outcome of the ?: is.

Perhaps a good counterexample is

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

Here j has a glvalue distinct from (but identical to) i. Is this well-defined, yet i = ++i is not? This represents a trivial transformation that a compiler could apply to any case.

1.9/15 should say

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 prvalue of the same scalar object, the behavior is undefined.

鹤舞 2024-10-02 19:10:12

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

In thinking about expressions like those mentioned, I find it useful to imagine a machine where memory has interlocks so that reading a memory location as part of a read-modify-write sequence will cause any attempted read or write, other than the concluding write of the sequence, to be stalled until the sequence completes. Such a machine would hardly be an absurd concept; indeed, such a design could simplify many multi-threaded code scenarios. On the other hand, an expression like "x=y++;" could fail on such a machine if 'x' and 'y' were references to the same variable, and the compiler's generated code did something like read-and-lock reg1=y; reg2=reg1+1; write x=reg1; write-and-unlock y=reg2. That would be a very reasonable code sequence on processors where writing a newly-computed value would impose a pipeline delay, but the write to x would lock up the processor if y were aliased to the same variable.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文