为什么赋值运算符不是序列点?

24

operator=在C和C++中不是序列点,这个设计有什么好的理由吗?

我很难想出一个反例。


8
通常,事物需要有一个理由才能成为序列点。它们不需要有一个理由来不成为序列点;那是默认的。 - Karl Knechtel
7
&&是一个序列点,因为它允许像ptr && ptr->data这样的习惯用法。然而,&&之所以被标准要求具有短路行为,是因为标准规定如果左侧为假,则右侧不能被评估。因此,在左侧为假的情况下不允许先评估右侧。 :) - Karl Knechtel
@Karl,也许我不理解序列点,但你所说的比序列点要弱得多。基本上,你所描述的对于=同样适用,只是方向相反,你需要评估右侧才能将其分配到右侧。但由于=不是序列点,所以i++ = i++是未定义的,而i++ && i++则是已定义的。 - Šimon Tóth
8
看这里。使用=时,您必须评估左侧和右侧,然后进行赋值(只是对于每个侧面的“评估”有略微不同的规则-lvalues vs rvalues等)。但是,在实际分配之前,没有理由必须先评估左侧还是右侧-只要在实际分配之前两者都要评估即可。而使用&&时,必须先评估左侧,然后再评估右侧,因为可能根本不需要评估右侧。 - Karl Knechtel
1
@Karl 哦,好的。好的,你能把它写成一个答案吗? - Šimon Tóth
显示剩余2条评论
4个回答

25

按照要求翻译如下:

一般来说,对于一个序列点需要一个理由才能使其成为序列点。而它们不需要理由就可以不成为序列点;这是默认的。

例如,&& 必须是序列点,因为存在短路行为:如果左侧为 false,则右侧不能被评估。(这不仅仅涉及优化问题;右侧可能具有副作用,并且/或者依赖于左侧为 true,例如 ptr && ptr->data)。因此,必须先评估左侧,再评估右侧,以确定是否应该评估右侧。

对于 =,不存在这样的理由,因为虽然双方都需要进行“评估”(虽然对双方的出现有不同的限制:左侧必须是左值 - 顺便说一下,“l”不代表“left”,而代表“location”,即在内存中的位置 - 我们不能将其赋值给临时变量或文字),但无论哪一方先进行评估都没有关系——只要在实际分配之前评估两个方面就可以了。


7
+1,但“=”不是序列点曾经让我吃过亏:我曾经写了myArray[i++] = <包含i的表达式>;,然后两个编译器中的一个在计算RHS之前计算了LHS,导致得出了错误的答案。虽然最终是我的错,但是这确实很棘手。 - j_random_hacker
如果我没记错的话,Java 在这里提供了更多的保证 :) - Karl Knechtel
短路行为本身不需要序列点。例如,表达式(((ch = *x++) != 0) && (*y++ = ch))可以在没有&&强制要求序列点的情况下延迟增加x的值,直到执行表达式的第二部分之后,因为编译器可以在增加x的值之前确定&&的左操作数是否为零。实际上,序列点禁止的任何代码重排在实践中都不太可能有用,但在某些理论情况下可能会有用。 - supercat
4
x = y[x] = 0 这行代码会首先对 y[x] 进行求值(使用旧的 x 值),然后再把 x 设为 0。 - Ariel
@Ariel 我认为标准没有任何保证排序的内容,因为这里没有序列点。编译器也很容易在确定要写入的地址之前预先确定赋值表达式 y[x] = 0 的值,因为该值只是右侧表达式(或其隐式转换为 y[x] 类型)。只要编译器允许自己在解析实际要写入的地址之前确定该类型,就没有什么可以阻止编译器执行该优化并创建未定义行为。 - Theodore Murdock

0

不需要要求任何一侧在另一侧之前进行评估的原因有很多。更有趣的问题是,赋值运算符本身在执行任何操作之前是否需要计算双方的评估结果,包括副作用。我建议这种要求将会减轻某些别名限制,但在某些情况下需要编译器完成更多的工作。例如,假设“foo”和“bar”都是指向大型结构体的指针,其地址会重叠。语句“*foo = *bar;”在当前标准下代表未定义行为。如果在操作数评估和赋值之间存在一个序列点,则可以保证这种语句可以“正常工作”。这种保证将需要更复杂的赋值运算符,即使在实践中指针永远不会重叠,也需要更大更慢的代码。

例子:

unsigned char foo[100];
typedef struct {int x, int y;} POINT;
POINT *p1 = (POINT*)foo;
POINT *p2 = (POINT*)(&(p1->y));

根据上述声明,我认为以下语句具有严格定义的行为,并且不涉及任何未定义行为。

p1->y = somevalue;  // 将 p2->x 设置为 somevalue
p2->x = somevalue;  // 将 p1->y 设置为 somevalue
*p1 = mystruct;     // 将 p2->x 设置为 mystruct.y
*p2 = mystruct;     // 将 p1->x 设置为 mystruct.x
然而,以下两个语句将涉及到未定义的行为: *p1 = *p2; *p2 = *p1;
如果等号处有一个序列点,编译器必须比较 p1 和 p2,或者将源操作数复制到临时位置,然后将其复制到目标位置。但是,标准明确指出上述两个语句都被视为未定义的行为。标准要求编译器在将一个结构体复制到非重叠的结构体时生成正确工作的代码,但对编译器在结构体重叠时可以做什么没有任何限制。如果编译器将处理器置于循环状态,向每个打开的 TCP 套接字发送“Frink Rules!”消息,这样做并不违反标准。

"具有地址重叠的大型数据结构。" 你能给一个例子吗? - curiousguy
你是从《辛普森一家》的3D集“霍默之旅”中得到了那句“弗林克规则”的吧? - ninjalj
@curiousguy:什么样的对齐问题?您是否暗示结构需要比其中任何元素更大的对齐? - supercat
“什么样的对齐问题?”你认为unsigned char [100]对于任何需要对齐的内容都是正确对齐的吗?“你是在暗示结构体需要比其中任何元素更大的对齐方式吗?”也许是的。 - curiousguy
@curiousguy:说得好。假设foo是通过calloc()获得的char*。那么会有任何问题吗? - supercat

0

这有点类似。operator=(可以由工程师定义(也称用户定义的类类型operator=))只是函数调用的语法糖。因此,它具有与函数调用相同的“序列点”语义。

如果我们谈论内置类型,那么我认为这是一件好事。
您不希望引入太多的序列点,因为这会妨碍优化。


2
这只是C++中用户自定义类的函数调用的语法糖。虽然你有点暗示了这一点,但从你措辞的方式来看,这一点并不清楚。 - Adam Rosenfield
6
“序列点”语义并不特别有用,因为operator=的参数将是=符号左右两侧的操作数,而函数调用不会对其参数的评估顺序施加限制——只有函数本身的评估是一个序列点。 - Karl Knechtel

0

这是自从c++17版本开始的。 详情请参见this


我明白你的意思,但是自从C++11以来,C++中已经没有序列点了。 - HolyBlackCat

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