“读取”一个 POD 类型的前置自增结果不会产生未定义行为。为什么?

7

这是一个愚蠢的问题。 :)

[编辑:无论愚蠢与否,这实际上是一个关于C++的奇特问题,请参见UPDATE_2]

假设我们有:

int a = 0; // line 1
int b = ++a; // line 2

第二行发生的是(注意,数字只是标记,不指定确切的顺序):

                      = [1: write result of (3) to result of (2)]
                     /\
[2: take "b" l-value] [3: convert result of (4) to an r-value ]
                      |
                      [4: take "a" l-value, "increment" and return it]

在(4)中的“write”在(3)中的“read”之前是“有序的”,由于它们之间没有序列点,因此副作用不能保证在(3)之前发生(在(4)本身还有一个“read”,但是在“write”之前有序,因此不会导致未定义行为)。
那么,以上哪里出了问题?
换句话说,问题在哪里?
  1. 似乎存在一种“竞争”,即l值到r值转换(“读取”)或增量(“写入”)副作用先发生。

  2. 在C语言中,这将导致UB,根据JTC1/SC22/WG14 N926 "Sequence Point Analysis"*(例如,EXAMPLE 5:int x,y; (x=y) + x; // UB)。

  3. 请注意,如果使用后自增,则不会出现这种情况,因为(3)和(4)将构成单个 [(3):获取“a”的l值,将其转换为r值并返回该r值] ,“写入”副作用延迟到下一个序列点之前的某个时间才会发生

_

(*) 这似乎是C99标准委员会成员提供的最清晰系统的理论基础。

[更新_2]

  1. 教训:永远不要用C的规则来评判C++ :))。我曾经这样做,想知道为什么N926(清楚地描述了C99的事情)在前置增量产生l-value的主题上“不够清楚”。

  2. 问题是如何为C++构建类似的理论基础,因为并没有一个,即使在C的情况下,仅仅解释标准也非常困难,而C ++语言标准更加复杂和晦涩。

[更新_3]

有一个讨论涉及一些相关主题(至少在较新的一半中)"关于常见表达式的未定义性问题。", 此外,委员会成员在这里讨论了该问题(请参见“222.序列点和左值返回运算符”)。


3
有趣。这个问题只发生在C++中,而不是C语言(在C语言中++a不是左值)。 - Johannes Schaub - litb
1
感谢更新,这使问题更加有趣明显。看来我还不够老练,无法从你的“C”标签中猜出你只是想考虑C++。 - John Marshall
@John 那是我的错(也是导致问题本身不一致的原因,请参见update_2)。 - mlvljr
顺便说一句,我真的很喜欢你分享的那篇论文。我正在仔细阅读它,觉得很有趣 :) - Johannes Schaub - litb
2个回答

2
我认为解决方案可能在“++i”的措辞中。它说:“该操作数的新值是其新值;它是一个左值。”而根据5/4,“此外,仅应访问先前的值以确定要存储的值。”行为未定义。

因此,我们没有访问之前的值,而是新值。然后我们可能会没问题。虽然未定义行为和已定义行为之间的界限似乎非常微弱。

实际上,“先前的值”听起来像“对象在前一个序列点上具有的值”。如果按照这种方式解释,那么这个结构看起来是未定义的。但是,如果我们直接将“++i”的措辞在5.3/2与5/4进行比较,我们就会遇到“新值”与“先前的值”,并且事情被“弯曲”为已定义的行为(“++i”将查看“i”的值在下一个序列点,并将该值作为“++i”的结果左值的内容)。


那么,你是否看到了一种将N926分析方法扩展到支持所讨论的C++情况的方法? - mlvljr
添加了一个 comp.lang.c 的讨论链接,如果你感兴趣的话。 - mlvljr

1

C++ 5/4中的主要句子是:

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

(Johannes引用的句子是下面的从句。)

你认为哪个标量对象在这里被修改了多次?ab在第2行各被修改一次,所以没有问题。

(C标准中的6.5/2也有类似的语言。)


编辑:

(4) 中的“write”在(3)中的“read”之前是“有序”的,由于两者之间没有序列点,因此副作用不能保证在(3)之前发生。

重新阅读您的问题后,我认为混淆的原因来自于对++a的混乱思考方式:表达式实际上并没有窥视a的未来值。相反,可以将++a视为“返回a+1并作为副作用在下一个序列点之前随意增加a”。

那么谁关心这个副作用是在(3)之前还是之后发生呢?表达式的值已经确定了,这就是传递给(3)的值。


2
@John,重点是在C++中,当你执行++a时,它并不会自动产生一个独立于更改其存储值的值a+1,因为这样它就不能成为一个左值。在C++中,你可以做一些像int *p = &++i;这样的事情,而这在C中是不允许的。对于++a + 1这样的东西,在C++中发生的是你修改了a的存储值,并且在同一序列点之间读取了它的值,不是为了确定要存储的值,而是为了进一步处理它。 - Johannes Schaub - litb
@litb:没错,我正在更新我的帖子,其中实际上是一个被你超越的评论:)) - mlvljr
章节和节?因为我一直在阅读5/4和5.3.2/2(“该值是操作数的新值;它是一个lvalue”),我就是看不出这意味着标量对象被重新读取(在as-if世界中)。 - John Marshall
1
措辞不太恰当(因为它使用“value”,在C术语中意味着“非lvalue”,但在C++中并不意味着这一点)。在C++0x中,它被修正为说“结果是更新的操作数”。但由于它是一个lvalue,没有关于第二次读取的疑问。请参见4.1,了解将lvalue转换为rvalue所采取的操作,其中包括访问其中存储的值。而5/8则负责触发该转换,如果操作数是lvalue但期望是rvalue。 - Johannes Schaub - litb
@JohannesSchaub-litb:如果r是映射到硬件寄存器的extern volatile(外部易失),而i是一个局部变量,那么i = ++r;是否需要读取r,将该值加一存储到r中,再次读取r,并将该值存储到i中?如果有东西使用了++r产生的值,则第二次读取会发生吗? - supercat

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