++克里斯托!
C++ 标准 1.9.16 对于如何为类实现 operator++(postfix) 非常有意义。当调用该 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++,甚至可能不知道在调用bar()之前i是否会增加一次或两次。 结果是不确定的! (给定i=0,这可能是bar(0,0)或bar(1,0)或bar(0,1)或一些非常奇怪的东西!)
鉴于:
foo(i++);
我们现在知道i将在foo()被调用之前递增。正如Kristo从C++ 标准第 1.9.16 节指出的那样:
调用函数时(无论该函数是否内联),与任何参数表达式或与指定被调用函数的后缀表达式相关的每个值计算和副作用都在执行主体中的每个表达式或语句之前进行排序称为函数。[注意:与不同参数表达式相关的值计算和副作用是无序的。——尾注]
虽然我认为第 5.2.6 节说得更好:
后缀 ++ 表达式的值是其操作数的值。[注:获得的值是原始值的副本——结束注]操作数应为可修改的左值。操作数的类型应为算术类型或指向完整有效对象类型的指针。操作数对象的值通过加 1 来修改,除非该对象是 bool 类型,在这种情况下它被设置为 true。[注意:不推荐使用此用法,请参阅附件 D。 -- 结束注释] ++ 表达式的值计算在操作数对象的修改之前排序。对于不确定顺序的函数调用,后缀 ++ 的操作是单次求值。[ 注意:因此,函数调用不应干预左值到右值的转换以及与任何单个后缀 ++ 运算符相关的副作用。-- 尾注]结果是一个右值。结果的类型是操作数类型的 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);
现在我们只有一个表达式。根据标准(第 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);
在这种情况下,必须在foo(j)之前评估i+=1。 theI是 1。而R是 0^0^1=1。