++Kristo!
C++标准1.9.16对于如何实现类的operator++(后缀)方法有很多合理之处。当调用该方法时,它会自增并返回原始值的一个副本。正如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() 之前或之后被调用。这是“未指定”的。
但我们知道,在 baz() 之前,同时会调用 g() 和 h()。
给定:
bar(i++,i++);
再次强调,我们不知道哪个 i++ 会先被计算,甚至不确定在调用 bar() 之前 i 是否会被增加一次或两次。 结果是未定义的!(假设 i=0,这可能是 bar(0,0) 或 bar(1,0) 或 bar(0,1) 或者一些非常奇怪的东西!)
给定:
foo(i++);
我们现在知道,在调用
foo()之前,
i将会被递增。正如
Kristo从
C++标准第1.9.16节中指出的:
在调用函数(无论函数是否为内联函数)时,与任何参数表达式相关的值计算和副作用,或者与指定所调用函数的后缀表达式相关的值计算和副作用,在执行所调用函数体中的每个表达式或语句之前进行排序。[注意:与不同参数表达式相关的值计算和副作用是未排序的。--注释]
虽然我认为第5.2.6节阐述得更好:
后缀++表达式的值是其操作数的值。[注意:获得的值是原始值的副本--注释]操作数必须是可修改的左值。操作数的类型必须是算术类型或完整有效对象类型的指针。将1添加到操作数对象会修改操作数对象的值,除非对象的类型为bool,在这种情况下,它将设置为true。[注意:此用法已弃用,请参见附录D.--注释] ++表达式的值计算在修改操作数对象之前进行排序。关于一个不确定排序的函数调用,后缀++运算符的操作是单个评估。[注意:因此,函数调用不得介入从左值转换为右值的过程和任何单个后缀++运算符相关的副作用。--注释]结果是rvalue。结果的类型是操作数类型的cv-unqualified版本。另请参见5.7和5.17。
标准在第1.9.16节中还列出了(作为其示例的一部分):
i = 7, i++, i++
f(i = -1, i = -1)
我们可以轻松地用以下方法证明这一点:
#define SHOW(X) cout << # X " = " << (X) << endl
int i = 0;
void foo(int theI) { SHOW(theI); SHOW(i); }
int main() { foo(i++); }
所以,是的,在调用
foo()之前,
i会被递增。
从以下角度来看,所有这些都有很多道理:
class Foo
{
public:
Foo operator++(int) {...}
}
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 调用的),因此在调用 bar("C",i) 和 bar("D",i) 之前,i 已经自增了。
回到 j_random_hacker 的评论:
j_random_hacker 写道:
+1,但我必须仔细阅读标准文件才能确信这是正确的。如果 bar() 不是一个对象方法而是一个返回 int 的全局函数,f 是一个 int,那么这些调用如果使用 "^" 相连,是否 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);
现在我们只有
一个表达式。根据标准(第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的值。(在评估任何后续表达式之前。)
附:
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。