为什么是f(i =
我正在阅读关于评估违规的顺序,他们举了一个令我困惑的例子。
1)如果标量对象的副作用相对于同一标量对象上的另一副作用未被排序,则行为是未定义的。
// snip
f(i = -1, i = -1); // undefined behavior
在这种情况下, i
是一个标量对象,这显然意味着
算术类型(3.9.1),枚举类型,指针类型,成员类型指针(3.9.2),std :: nullptr_t和这些类型的cv限定版本(3.9.3)统称为标量类型。
我不明白这种说法在这种情况下是如何含糊不清的。 在我看来,无论第一个还是第二个参数先被评估, i
以-1
结束,并且两个参数也都是-1
。
有人可以澄清?
UPDATE
我非常感谢所有的讨论。 到目前为止,我很喜欢@ harmic的答案,因为它暴露了定义这个陈述的陷阱和复杂性,尽管它乍看之下有多简单。 @ acheong87指出了使用引用时出现的一些问题,但我认为这与这个问题的不确定的副作用方面是正交的。
概要
由于这个问题引起了很大的关注,我将总结主要观点和答案。 首先,请允许我进行一次小小的离题指出,“为什么”可以有密切相关但意义不同的含义,即“为什么”,“出于何种原因”和“为了什么目的”。 我将按照哪些“为什么”的含义给出答案。
为什么导致
这里的主要答案来自Paul Draper,Martin J提供了一个类似但不是广泛的答案。 Paul Draper的答案归结为
这是未定义的行为,因为它没有定义什么行为。
就解释C ++标准所说的问题而言,答案总的来说非常好。 它还解决了UB的一些相关案例,如f(++i, ++i);
和f(i=1, i=-1);
。 在第一个相关案例中,不清楚第一个参数应该是i+1
还是第二个i+2
,反之亦然; 在第二个函数调用后, i
不清楚i
应该是1还是-1。 这两种情况都属于UB,因为它们属于以下规则:
如果标量对象上的副作用相对于同一标量对象上的另一副作用而不被序列化,则行为是不确定的。
因此,即使程序员的意图是(恕我直言)明显而明确, f(i=-1, i=-1)
也是UB,因为它属于同一规则。
Paul Draper在他的结论中也明确指出
它可以被定义为行为吗? 是。 是否定义了? 没有。
这使我们想到“ f(i=-1, i=-1)
是什么原因/目的还是未定义的行为?”。
出于何种原因/目的
尽管在C ++标准中存在一些疏漏(可能不小心),但许多遗漏都是合理的,并有特定的目的。 虽然我知道目的通常是“让编译器 - 作者的工作更容易”或“更快的代码”,但我主要想知道是否有一个好的理由leave f(i=-1, i=-1)
作为UB。
harmic和supercat提供了为UB提供理由的主要答案。 Harmic指出,优化编译器可能会将表面上原子分配操作拆分为多个机器指令,并且可能会进一步交织这些指令以获得最佳速度。 这可能会导致一些非常令人惊讶的结果: i
在他的场景中以-2结束! 因此,如果操作是不确定的,harmic说明了如何将多个相同的值分配给一个变量可能会产生不良影响。
supercat提供了试图让f(i=-1, i=-1)
做它看起来应该做的事情的陷阱的相关论述。 他指出,在某些体系结构中,存在对多个同时写入同一内存地址的硬限制。 如果我们处理的是比f(i=-1, i=-1)
更少的东西,编译器可能很难捕捉到这个问题。
davidf也提供了一个交叉指令的例子,与harmic的指令非常相似。
虽然每个harmic,supercat和davidf的例子都是有些人为的,但它们仍然可以为f(i=-1, i=-1)
应该是未定义的行为提供一个有形的理由。
我接受了哈利米的回答,因为尽管保罗德雷珀的回答更好地解决了“为什么”的部分,但它在解决所有为什么的问题上做得最好。
其他答案
JohnB指出,如果我们考虑重载赋值操作符(而不是简单的标量),那么我们也会遇到麻烦。
由于操作是无序的,所以没有什么可以说执行任务的指令不能交错。 根据CPU架构,这可能是最佳选择。 引用页面指出:
如果A在B和B未在A之前排序之前未被测序,则存在两种可能性:
A和B的评估是不确定的:它们可以以任何顺序执行并且可以重叠(在单个执行线程内,编译器可以交织包括A和B的CPU指令)
A和B的评估是不确定的:它们可以以任何顺序执行,但可能不重叠:A在B之前完成,或B在A之前完成。顺序可能与下次相同表达式相反被评估。
这本身似乎并不会造成问题 - 假设正在执行的操作将值-1存储到内存位置。 但也没有什么可说的,编译器无法将其优化为一组单独的具有相同效果的指令,但如果该操作与另一个操作在同一内存位置交错,则可能会失败。
例如,想象一下,将内存归零,然后将其减少,与加载值-1相比,效率更高。然后,
f(i=-1, i=-1)
可能会变成:
clear i
clear i
decr i
decr i
现在我是-2。
这可能是一个虚假的例子,但它是可能的。
首先,“标量对象”意味着像int
, float
或指针这样的类型(请参阅什么是C ++中的标量对象?)。
其次,这似乎更明显
f(++i, ++i);
会有未定义的行为。 但
f(i = -1, i = -1);
不太明显。
一个稍微不同的例子:
int i;
f(i = 1, i = -1);
std::cout << i << "n";
什么任务发生在“最后”, i = 1
或i = -1
? 它没有在标准中定义。 真的,那意味着i
可能是5
(参见harmic的答案,对于这种情况应该如何完全合理的解释)。 或者你的程序可以segfault。 或者重新格式化您的硬盘。
但是现在你问:“我的例子呢?我对这两个任务使用了相同的值( -1
),可能有什么不清楚的地方?”
你是对的......除了C ++标准委员会描述这一点的方式。
如果标量对象上的副作用相对于同一标量对象上的另一副作用而不被序列化,则行为是不确定的。
他们本可以为你的特例做一个特例,但他们没有。 (为什么他们呢?会有什么用处?)所以, i
仍然可以是5
。 或者你的硬盘可能是空的。 因此,你的问题的答案是:
这是未定义的行为,因为它没有定义什么行为。
(这是值得强调的,因为很多程序员认为“未定义”意味着“随机”或“不可预测”,它不;这意味着没有被标准定义,行为可能是100%一致的,仍然是未定义的。
它可以被定义为行为吗? 是。 是否定义了? 不是。因此,它是“未定义的”。
也就是说,“未定义”并不意味着编译器会格式化您的硬盘驱动器......这意味着它可以并且它仍然是符合标准的编译器。 实际上,我确信g ++,Clang和MSVC都会按照您的预期进行操作。 他们不会“必须”。
另一个问题可能是为什么C ++标准委员会选择使这种副作用不被引用? 答案将涉及委员会的历史和意见。 或者,这种副作用在C ++中没有被确定,这有什么好处?它允许任何理由,不管它是否是标准委员会的实际推理。 你可以在这里提出这些问题,或者在程序员.stackexchange.com上提问。
一个切实可行的理由是,不要因为这两个值是相同而从规则中排除例外:
// config.h
#define VALUEA 1
// defaults.h
#define VALUEB 1
// prog.cpp
f(i = VALUEA, i = VALUEB);
考虑这是允许的情况。
现在,几个月后,需要改变
#define VALUEB 2
看似无害,不是吗? 但突然prog.cpp不会编译了。 但是,我们认为汇编不应该依赖文字的价值。
底线:规则没有例外,因为它会使编译成功取决于常量的值(而不是类型)。
编辑
@HeartWare指出,当B
为0时,某些语言不允许表达式A DIV B
常量表达式,并且导致编译失败。 因此,改变常量可能会导致其他地方的编译错误。 恕我直言,这是不幸的。 但把这些事情限制到不可避免的地步当然是好事。
上一篇: Why is f(i =