语言标准中对赋值运算符的描述有几点我不清楚,我想澄清一下。
n4659,8.18/1:
[...] 在所有情况下,赋值都在左右操作数的值计算之后和赋值表达式的值计算之前进行排序。右操作数在左操作数之前排序。对于不确定顺序的函数调用,复合赋值的操作是单次求值。[注意:因此,函数调用不应干预左值到右值的转换以及与任何单个复合赋值运算符相关的副作用。——尾注]
问题 1. 什么是值计算,它是按照以下代码中左右操作数的标准执行的:
a = 1;
问题 2. 函数调用的不确定顺序是什么意思?我想看一个在同一个表达式中涉及赋值和函数调用的示例,其中函数调用相对于某物是无限排序的。
n4659, 8.18/8:
如果存储在一个对象中的值是通过另一个对象读取的,该对象以任何方式与第一个对象的存储重叠,那么重叠应该是精确的,并且两个对象应该具有相同的类型,否则行为是未定义的。[注:此限制适用于赋值操作的左右关系;它不是关于分配的目标通常如何别名的声明。见 6.10。——尾注]
问题 3. 我想看一个符合上述引用描述的例子。
除了问题 1。
也许将问题如下提出更有意义:从语言标准的角度来看,值计算是否总是在赋值运算符的操作数上执行?如果没有,什么时候完成,什么时候没有?
重点是这个。这个问题的第一句话说赋值发生在两个操作数的值计算发生之后。它还说右操作数在左操作数之前排序(即,与右操作数相关的值计算 + 副作用将发生在与左操作数相关的值计算 + 副作用之前)。
因此,如果在赋值运算符中,左操作数的值计算总是发生在实际赋值之前,那么右操作数总是在赋值之前排序。
“值计算”是计算表达式值的过程,即 得到它的结果。例如,假设我们有
int i = 0,那么计算的表达式i++将返回0。这里重要的是它i++包含副作用。因此,在计算了上述表达式后,我们不保证所需的更改(增量)同时应用于它。为什么这很重要?让我们以这个标准长达 17 年。其中,表达式:i = i++具有未定义的行为,因为i相对于赋值,for 的增量没有以任何方式排序:它可以发生在之前或之后。另一方面,如果我们采用一个表达式
++i,那么在计算表达式之前会在其中应用副作用,这使得这样的表达式i = ++i有效并且不包含未定义的行为。现在到第二个问题。假设我们有一个全局变量:
long long global = 0;和一个函数auto foo() {return global += 1;}。例如,我们还需要这样的函数void ignore(long long, long long)和 C++17(仅是为了保证函数参数的不确定顺序评估)。最后,考虑下面的代码:
ignore(global += BIG_NUMBER, foo()),所以,理论上,如果你删除这个规则,那么可能会导致以下情况(在某些平台上记录long long将在几个指令中):我们开始从global外部写入我们的,我们写向下一部分,但这里有一个代码切换foo(根据编译器的决定)并重写我们写下的内容。之后,我们继续记录,我们的变量global包含垃圾。那些。该规则禁止交错(interleaving,overlapping)表达式的求值,编译器无权做我在上句中描述的事情。但这都是理论,因为标准明确禁止 (C++14[intro.object]p13) 交错所有表达式,除了未排序的表达式,所以这样的澄清对我来说似乎是多余的。
第三个引用禁止这样做:
那些。左边和右边不应该有两个对象指向同一个内存区域,但具有不同的类型。
关于添加:这里值得引用 C++17[intro.object]p15:
这样就足够了。根据 C++17 中引入的规则,赋值表达式的右侧排序在前(sequenced before):
这是理解的关键:在我们分配一个新值之前,或者甚至开始计算我们在那里分配的内容之前,右边的一切都必须解决。此规则使代码
i = i++正确,尽管在 C++17 之前这是未定义的行为。在赋值运算符的左侧和右侧,都可以有比您的示例中指示的更复杂的实体。例如,左边可能有一个返回引用的函数,右边可能有一个返回值的函数。在这里,调用这些函数来获取左右部分的值就是值计算。对于您的情况,我们可以说变量的地址是在左侧计算的
a,而文字的值是在右侧计算的1。关于“不确定顺序的函数调用”的短语表示复合运算符,例如
+=,*=等。当用作函数参数时,应将其视为单个(不可分割的)评估。例如,当调用以下函数时:在对 和的调用
c()之间不能发生调用。a()b()对于第三个问题 - 假设您有一个字节数组。你有两个指针,比字节重,指向该数组中的数据。同时,解引用这些指针会用到原始数组的一些公共部分,即会有一个交集。这些是取消引用的指针,不能在赋值中使用。