重新加载未定义的行为和序列点
Posted
技术标签:
【中文标题】重新加载未定义的行为和序列点【英文标题】:Undefined behavior and sequence points reloaded 【发布时间】:2011-06-06 00:27:44 【问题描述】:将此主题视为以下主题的续集:
上一期 Undefined behavior and sequence points
让我们重温一下这个 funny 和 convoluted 表达式(斜体词组取自上述主题 *smile* ):
i += ++i;
我们说这调用了未定义的行为。我假设当这样说时,我们隐含地假设 i
的 type 是内置类型之一。
如果i
的type 是用户定义的类型怎么办?假设它的类型是Index
,它在本文后面定义(见下文)。它还会调用 undefined-behavior 吗?
如果是,为什么?不等于写i.operator+=(i.operator++());
,甚至语法上更简单的i.add(i.inc());
?或者,它们是否也调用未定义行为?
如果没有,为什么不呢?毕竟,对象i
在连续序列点之间被修改两次。请记住经验法则:an expression can modify an object's value only once between consecutive "sequence points。如果i += ++i
是一个表达式,那么它必须调用未定义行为。如果是这样,那么它的等价物 i.operator+=(i.operator++());
和 i.add(i.inc());
也必须调用 undefined-behavior 这似乎是不真实的! (据我所知)
或者,i += ++i
一开始就不是一个表达式?如果是,那么它是什么,表达式的定义是什么?
如果它是一个表达式,同时它的行为也是明确定义的,那么它意味着与一个表达式关联的序列点的数量在某种程度上取决于类型 表达式中涉及的操作数。我是否正确(即使部分正确)?
对了,这个表达式怎么样?
//Consider two cases:
//1. If a is an array of a built-in type
//2. If a is user-defined type which overloads the subscript operator!
a[++i] = i; //Taken from the previous topic. But here type of `i` is Index.
您也必须在回复中考虑到这一点(如果您确定知道它的行为)。 :-)
是
++++++i;
在 C++03 中定义良好?毕竟是这个,
((i.operator++()).operator++()).operator++();
class Index
int state;
public:
Index(int s) : state(s)
Index& operator++()
state++;
return *this;
Index& operator+=(const Index & index)
state+= index.state;
return *this;
operator int()
return state;
Index & add(const Index & index)
state += index.state;
return *this;
Index & inc()
state++;
return *this;
;
【问题讨论】:
+1 个很好的问题,激发了很好的答案。我觉得我应该说它仍然是可怕的代码,应该重构以提高可读性,但无论如何你可能都知道 :) @什么是问题:谁说它是一样的?或者谁说不一样?它不取决于您如何实现它们吗? (注意:我假设s
的类型是用户定义的类型!)
我没有看到任何 scalar 对象在两个序列点之间被修改两次...
@Johannes :那么它是关于 scalar 对象的。它是什么?我想知道为什么我以前从未听说过它。也许是因为教程/C++-faq 没有提到它,或者没有强调它?和内置类型的对象有区别吗?
@Phillip :显然,我不会在现实生活中编写这样的代码。事实上,没有理智的程序员会编写它。通常设计这些问题是为了让我们更好地理解未定义行为和序列点的整个业务! :-)
【参考方案1】:
看起来像代码
i.operator+=(i.operator ++());
在序列点方面工作得很好。 C++ ISO 标准的第 1.9.17 节对序列点和函数求值进行了说明:
当调用一个函数时(无论该函数是否是内联的),在函数体中的任何表达式或语句执行之前,在所有函数参数(如果有的话)的求值之后都会有一个序列点。在复制返回值之后和函数外的任何表达式执行之前,还有一个序列点。
这将表明,例如,i.operator ++()
作为operator +=
的参数在其评估后有一个序列点。简而言之,由于重载运算符是函数,因此适用正常的排序规则。
顺便说一句,好问题!我真的很喜欢你强迫我理解一种我已经认为我知道(并且认为我认为我知道)的语言的所有细微差别。 :-)
【讨论】:
【参考方案2】:http://www.eelis.net/C++/analogliterals.xhtml 我想到了模拟文字
unsigned int c = ( o-----o
| !
! !
! !
o-----o ).area;
assert( c == (I-----I) * (I-------I) );
assert( ( o-----o
| !
! !
! !
! !
o-----o ).area == ( o---------o
| !
! !
o---------o ).area );
【讨论】:
有一个问题,是++++++i;在 C++03 中定义良好?【参考方案3】:正如其他人所说,您的 i += ++i
示例适用于用户定义类型,因为您正在调用函数,并且函数包含序列点。
另一方面,假设a
是您的基本数组类型,甚至是用户定义的数组类型,a[++i] = i
就没有那么幸运了。您在这里遇到的问题是我们不知道首先评估包含i
的表达式的哪一部分。可能是 ++i
被评估,传递给 operator[]
(或原始版本)以便在那里检索对象,然后 i
的值被传递给它(这是在 i
之后递增)。另一方面,可能是先评估后者,存储以供以后分配,然后评估 ++i
部分。
【讨论】:
所以...结果是未指定的而不是UB,因为表达式的评估顺序未指定? @Philip: unspecified 意味着我们期望编译器指定行为,而 undefined 没有这样的义务。我认为这里是未定义的,让编译器有更多的优化空间。 @Noah :我也发布了回复。请检查一下,让我知道你的想法。 :-) @Philip:结果是 UB,因为 5/4 中的规则:“对于完整表达式的子表达式的每个允许排序,应满足本段的要求;否则行为是不明确的。”。如果所有允许的排序在修改++i
和分配的RHS 上i
的读数之间都有序列点,那么顺序将是未指定的。因为允许的顺序之一在没有中间序列点的情况下完成这两项操作,所以行为是未定义的。
@Philip:它不只是将未指定的行为定义为未定义的行为。同样,如果未指定行为的范围包括一些未定义的行为,则整体行为未定义。 如果在所有可能性中都定义了未指定行为的范围,那么整体行为是未指定的。但是您在第二点上是对的,我在考虑用户定义的a
和内置的i
。【参考方案4】:
我认为它定义明确:
来自 C++ 草案标准 (n1905) §1.9/16:
"后面还有一个序列点 返回值的复制和 在执行任何 函数外的表达式13) 。 C++ 中的几个上下文导致 函数调用的评估,甚至 虽然没有对应的函数调用 语法出现在翻译中 单元。 [例子:评估一个新的 表达式调用一个或多个 分配和构造函数; 见 5.3.4。再举一个例子, 调用转换函数 (12.3.2) 可能出现在 没有出现函数调用语法。 — end example ] 序列指向 函数入口和函数出口(如 上述)是的特点 评估后的函数调用,随便 表达式的语法 调用函数可能是。 "
注意我加粗的部分。这意味着在增量函数调用(i.operator ++()
)之后但在复合赋值调用(i.operator+=
)之前确实有一个序列点。
【讨论】:
【参考方案5】:好的。在浏览了之前的回复之后,我重新思考了我自己的问题,特别是只有诺亚尝试answer但我并不完全相信他的部分。
a[++i] = i;
案例一:
如果a
是一个内置类型的数组。那么诺亚说的是对的。也就是说,
a[++i] = i 不是那么幸运,假设 a 是您的基本数组类型,
或 甚至是用户定义的一个。问题 你在这里是我们不知道的 表达式的哪一部分 包含 i 的首先被评估。
所以a[++i]=i
调用未定义行为,或者结果未指定。不管它是什么,它都没有明确定义!
PS:在上面的引用中,删除线当然是我的了。
案例2:
如果a
是重载operator[]
的用户定义类型的对象,那么又有两种情况。
-
如果重载的
operator[]
函数的返回类型是内置类型,那么a[++i]=i
再次调用undefined-behavior或者结果未指定。
但是如果重载operator[]
函数的返回类型是用户定义类型,那么a[++i] = i
的行为是明确定义的(据我所知),因为在这种情况下a[++i]=i
相当于写a.operator[](++i).operator=(i);
与 a[++i].operator=(i);
相同。也就是说,赋值operator=
在a[++i]
的returned 对象上被调用,这似乎非常明确,因为在a[++i]
返回时,++i
已经被评估,然后 returned 对象调用operator=
函数,将i
的更新值作为参数传递给它。 请注意,这两个调用之间有一个序列点。并且语法确保这两个调用之间没有竞争,operator[]
将首先被调用,随后,传递给它的参数 ++i
也将首先被评估。
将其视为someInstance.Fun(++k).Gun(10).Sun(k).Tun();
,其中每个连续的函数调用都返回某个用户定义类型的对象。在我看来,这种情况更像是:eat(++k);drink(10);sleep(k)
,因为在这两种情况下,每个函数调用之后都存在序列点。
如果我错了,请纠正我。 :-)
【讨论】:
@Nawazk++
和 k
不由序列点分隔。它们都可以在评估 Sun
或 Fun
之前进行评估。语言only 要求在Sun
之前评估Fun
,而不是在Sun
的参数之前评估Fun
的参数。我有点想再次解释同样的事情,但无法提供参考,所以我们不会从这里开始。
@Nawaz:因为没有定义将它们分开的序列点。在Sun
执行之前和之后有序列点,但Fun
的参数++k
可以在此之前或之后评估。在Fun
执行之前和之后有序列点,但Sun
的参数k
可以在此之前或之后评估。因此,一种可能的情况是k
和++k
在Sun
或Fun
被评估之前被评估,因此两者都在函数调用序列点之前,因此没有分隔@ 的序列点987654362@ 和 ++k
.
@Philip:我再说一遍:这种情况与eat(i++);drink(10);sleep(i);
有何不同? ...即使是现在,您也可以说 i++
可能会在此之前或之后进行评估?
@Nawaz:我怎样才能让自己更清楚?在 Fun/Sun 示例中,k
和 ++k
之间没有no 序列点。在吃/喝示例中,在i
和i++
之间有 作为序列点。
@Philip:这根本没有意义。在 Fun() 和 Sun() 之间存在一个序列点,但在它们的参数之间不存在序列点。就像在说,eat()
和sleep()
之间存在序列点,但在它们之间甚至没有一个参数。由序列点分隔的两个函数调用的参数如何属于相同序列点?以上是关于重新加载未定义的行为和序列点的主要内容,如果未能解决你的问题,请参考以下文章
入口点未定义 = index.html 使用 HtmlWebpackPlugin
刨根究底字符编码之十四——UTF-16究竟是怎么编码的(“代理区(Surrogate Zone)”,范围为0xD800~0xDFFF(十进制55296~57343),共2048个码点未定义。UTF8和