++克里斯托!
C++ 标准 1.9.16 对如何为类实现 operator++(后缀)非常有意义。当调用该 operator++(int) 方法时,它会自增并返回原始值的副本。正如 C++ 规范所说的那样。
很高兴看到标准在提高!
但是,我清楚地记得使用旧的(ANSI 之前的)C 编译器,其中:
foo -> bar(i++) -> charlie(i++);
没有按照你的想法去做!相反,它编译为:
foo -> bar(i) -> charlie(i); ++i; ++i;
而且这种行为依赖于编译器实现。 (让移植变得有趣。)
现在可以很容易地测试和验证现代编译器的行为是否正确:
#define SHOW(S,X) cout << S << ": " # X " = " << (X) << endl
struct Foo
{
Foo & bar(const char * theString, int theI)
{ SHOW(theString, theI); return *this; }
};
int
main()
{
Foo f;
int i = 0;
f . bar("A",i) . bar("B",i++) . bar("C",i) . bar("D",i);
SHOW("END ",i);
}
正在回复帖子中的评论...
...在几乎每个人的答案的基础上...(谢谢大家!)
我认为我们需要更好地说明这一点:
给定:
baz(g(),h());
那么我们不知道 g() 会在之前还是之后 h() 被调用。它是“未指定”。
但我们确实知道 g() 和 h() 都会在 baz()之前被调用>.
给定:
bar(i++,i++);
同样,我们不知道哪个 i++ 将首先被评估,甚至可能不知道 i 是否会在 bar() 之前递增一次或两次 被调用。 结果未定义!(给定 i=0,这可能是 bar(0,0) 或 bar(1,0 ) 或 bar(0,1) 或一些非常奇怪的东西!)
给定:
foo(i++);
我们现在知道 i 将在 foo() 被调用之前递增。正如Kristo从the C++ standard section 1.9.16:指出的那样
调用函数时(无论函数是否内联),与任何参数表达式或指定被调用函数的后缀表达式相关的每个值计算和副作用,都在执行每个表达式或语句之前排序被调用函数的主体。 [注意:与不同参数表达式相关的值计算和副作用是无序的。 -- 尾注]
虽然我认为第 5.2.6 节说得更好:
后缀 ++ 表达式的值是其操作数的值。 [注:得到的值是原始值的副本——尾注] 操作数应为可修改的左值。操作数的类型应为算术类型或指向完整有效对象类型的指针。操作数对象的值通过加 1 来修改,除非该对象是 bool 类型,在这种情况下它被设置为 true。 [注意:不推荐使用此用法,请参阅附件 D。 -- 结束注释] ++ 表达式的值计算在操作数对象的修改之前排序。 对于一个不定序的函数调用,后缀++的操作是单次求值。 [ 注意:因此,函数调用不应干预左值到右值的转换以及与任何单个后缀 ++ 运算符相关的副作用。 -- end note ] 结果是一个右值。结果的类型是操作数类型的 cv 非限定版本。另见 5.7 和 5.17。
该标准在 1.9.16 节中还列出(作为其示例的一部分):
i = 7, i++, i++; // i becomes 9 (valid)
f(i = -1, i = -1); // the behavior is undefined
我们可以简单地证明这一点:
#define SHOW(X) cout << # X " = " << (X) << endl
int i = 0; /* Yes, it's global! */
void foo(int theI) { SHOW(theI); SHOW(i); }
int main() { foo(i++); }
所以,是的,i 在 foo() 被调用之前递增。
从以下角度来看,所有这些都很有意义:
class Foo
{
public:
Foo operator++(int) {...} /* Postfix variant */
}
int main() { Foo f; delta( f++ ); }
这里 Foo::operator++(int) 必须在 delta() 之前调用。并且增量操作必须在该调用期间完成。
在我(也许过于复杂)的例子中:
f . bar("A",i) . bar("B",i++) . bar("C",i) . bar("D",i);
f.bar("A",i) 必须执行才能获得用于object.bar("B",i++)的对象,以此类推对于 "C" 和 "D"。
所以我们知道 i++ 在调用 bar("B",i++) 之前会增加 i(即使 bar( "B",...) 使用 i) 的旧值调用,因此 i 在 bar("C ",i) 和 bar("D",i).
回到 j_random_hacker 的评论:
j_random_hacker 写道:
+1,但我必须仔细阅读标准以说服自己这没问题。我是否正确地认为,如果 bar() 是一个返回say int 的全局函数,f 是一个 int,并且这些调用是通过say "^" 而不是 "." 连接的,那么 A、C 和 D 中的任何一个都可以报告“0”?
这个问题比你想象的要复杂得多......
将您的问题重写为代码...
int bar(const char * theString, int theI) { SHOW(...); return i; }
bar("A",i) ^ bar("B",i++) ^ bar("C",i) ^ bar("D",i);
现在我们只有 ONE 表达式。根据标准(第 1.9 节,第 8 页,pdf 第 20 页):
注意:只有在运算符真正具有关联性或交换性的情况下,运算符才能根据通常的数学规则重新组合。(7) 例如,在以下片段中:a=a+32760+b+5;表达式语句的行为完全相同: a=(((a+32760)+b)+5);由于这些运算符的关联性和优先级。因此,和的结果 (a+32760) 接下来被添加到 b,然后将该结果添加到 5,这导致分配给 a 的值。在溢出产生异常并且 int 可表示的值范围为 [-32768,+32767] 的机器上,实现不能将此表达式重写为 a=((a+b)+32765);因为如果 a 和 b 的值分别为 -32754 和 -15,则 a+b 之和会产生异常,而原始表达式不会;也不能将表达式重写为 a=((a+32765)+b);或 a=(a+(b+32765));因为 a 和 b 的值可能分别是 4 和 -8 或 -17 和 12。但是在溢出不会产生异常并且溢出结果是可逆的机器上,上述表达式语句可以由实现以上述任何方式重写,因为将出现相同的结果。 -- 尾注]
所以我们可能会认为,由于优先级,我们的表达式将与以下内容相同:
(
(
( bar("A",i) ^ bar("B",i++)
)
^ bar("C",i)
)
^ bar("D",i)
);
但是,因为 (a^b)^c==a^(b^c) 没有任何可能的溢出情况,所以可以按任意顺序重写...
但是,因为 bar() 正在被调用,并且可能涉及副作用,所以这个表达式不能以任何顺序重写。优先规则仍然适用。
这很好地确定了 bar() 的评估顺序。
现在,i+=1 什么时候发生?那么它仍然必须在 bar("B",...) 被调用之前发生。 (即使 bar("B",....) 使用旧值调用。)
所以它确定性地发生在 bar(C) 和 bar(D) 之前,以及 bar(A) 之后。
回答:否。我们将始终拥有“A=0, B=0, C=1, D=1”,如果编译器符合标准。
但考虑另一个问题:
i = 0;
int & j = i;
R = i ^ i++ ^ j;
R的值是多少?
如果 i+=1 发生在 j 之前,我们将有 0^0^1=1。但是如果 i+=1 出现在整个表达式之后,我们就会有 0^0^0=0。
事实上,R 为零。 i+=1 直到表达式被计算之后才会出现。
我认为这是为什么:
i = 7, i++, i++; // i 变为 9(有效)
是合法的……它有三种表达方式:
在每种情况下,i 的值都会在每个表达式的结尾发生变化。 (在计算任何后续表达式之前。)
PS:考虑一下:
int foo(int theI) { SHOW(theI); SHOW(i); return theI; }
i = 0;
int & j = i;
R = i ^ i++ ^ foo(j);
在这种情况下,i+=1 必须在 foo(j) 之前计算。 theI 是 1。而 R 是 0^0^1=1。