理解一个重载运算符[]的例子

13

我在一次C++测试中看到了一个问题,让我很困惑。 下面是代码:

#include <iostream>
using namespace std;

class Int {
public:
    int v;
    Int(int a) { v = a; }
    Int &operator[](int x) {
        v+=x;
        return *this;
    }
};
ostream &operator<< (ostream &o, Int &a) {
    return o << a.v;
}

int main() {
    Int i = 2;
    cout << i[0] << i[2]; //why does it print 44 ?
    return 0;
}

我本以为这会打印出24,但它却打印出44。我希望有人能澄清一下。这是累积评估吗? 另外,<<二元中缀运算符吗?

提前感谢。

编辑如果操作符重载定义不清楚,在此能否给出更好的实现方式,使其打印出24?


1
cout << i[0] << i[2]; 也可以写成这样:operator<<(operator<<(cout, i[0]), i[2]); - Xaqq
1
似乎两个“<<”的求值顺序没有定义好。调试你的代码,你会发现i[2]首先被调用。 - Tim3880
我真的建议不要让operator[]具有副作用。我认为这种行为在某些时候会再次咬你一口,而不仅仅是在这里。想象一下它如何与多线程交互,例如。 - user4842163
2个回答

16

这个程序的行为是不确定的:编译器无需按从左到右的顺序评估i[0]i[2] (C++语言允许编译器自由优化)。

例如,Clang在此示例中执行了这样做,而GCC则没有

评估的顺序是未指定的,因此您不能期望有一致的输出,即使在同一台计算机上反复运行您的程序也是如此。

如果您想要获得一致的输出,则可以将上述重新表述为以下内容(或以某种等效方式):

cout << i[0];
cout << i[2];

正如您所看到的,GCC在这种情况下不再输出44

编辑:

如果您真的非常想让表达式cout << i[0] << i[2]打印24,那么您将不得不大幅修改重载运算符(operator []operator <<)的定义,因为语言故意使无法确定哪个子表达式(i[0]还是[i2])先被评估。

您在这里得到的唯一保证就是评估i[0]的结果会被插入到cout中,然后才是评估i[2]的结果,因此您最好让operator <<执行对Int的数据成员v的修改,而不是operator []

然而,应该应用于v的增量作为参数传递给operator [],您需要某种方式将其与原始的Int对象一起转发给operator <<。一种可能性是让operator []返回一个包含增量以及对原始对象的引用的数据结构:

class Int;

struct Decorator {
    Decorator(Int& i, int v) : i{i}, v{v} { }
    Int& i;
    int v;
};

class Int {
public:
    int v;
    Int(int a) { v = a; }
    Decorator operator[](int x) {
        return {*this, x}; // This is C++11, equivalent to Decorator(*this, x)
    }
};

现在你只需要重写operator <<,使其接受一个Decorator对象,通过将存储的增量应用于v数据成员来修改引用的Int对象,然后打印出它的值:

ostream &operator<< (ostream &o, Decorator const& d) {
    d.i.v += d.v;
    o << d.i.v;
    return o;
}

这里有一个实时示例

免责声明:正如其他人所提到的,需要注意的是,operator []operator << 通常是非变异操作(更准确地说,它们不会改变被索引和流插入的对象的状态),因此您强烈不建议编写像这样的代码,除非您只是想解决一些C++小问题。


4
在这种情况下,行为并不是未定义的,仅仅是不确定的。对于重载的 operator [] 的调用只是普通的函数调用,它们不能交错执行(见第1.9/15段)。 - Andy Prowl
1
哦,但是段落结尾是这样的:如果标量对象上的副作用相对于同一标量对象上的另一个副作用或使用相同标量对象的值计算是未排序的,并且它们不可能并发(1.10),则行为是未定义的。我有点困惑,operator[]不是“副作用”吗?哦,好的好的,我错过了示例后面的部分。谢谢! - vsoftco
1
@vsoftco:没错,脚注9也有些澄清:“换句话说,函数执行不会相互交错。” - Andy Prowl
1
@AndyProwl 谢谢你的回答。我已经尝试了你提出的使用两个 cout 的方法。我们如何重载 << 以便它可以打印出 24?再次感谢,我也在原帖中进行了编辑。 - BugShotGG
2
@vsoftco 也许将表达式重写为 operator<<(operator<<(cout, i.operator[](0)), i.operator[](2)); 会更容易理解。对 operator[] 的两次调用可以以任何顺序进行,但它们不会交错,这使得对 v 的修改是不确定顺序的而非无序的。 - Praetorian
显示剩余3条评论

5
为了解释这里发生了什么,让我们简单地理解一下:cout<<2*2+1*1;。2*2和1*1哪个先计算?一个可能的答案是应该先计算2*2,因为它是最左边的东西。但是C++标准说:谁在乎呢?不管怎样,结果都是5。但有时候这很重要。例如,如果fg是两个函数,并且我们做f()+g(),那么不能保证哪个会先被调用。如果f打印一条消息,但g退出程序,则该消息可能永远不会被打印出来。在您的情况下,i[2]i[0]之前被调用,因为C++认为这没关系。你有两个选择:
一种选择是改变代码,使其不重要。重写你的[]操作符,使它不改变Int,而是返回一个新的Int。这可能是个好主意,因为这将使它与行星上99%的其他[]操作符一致。它也需要更少的代码:Int &operator[](int x) { return this->v + x;}
另一种选择是保持你的[]不变,将你的打印拆成两个语句:cout<<i[0]; cout<<i[2]; 有些语言确实保证在2*2+1*1中,2*2先进行。但不是C++。
编辑:我不像我希望的那样清楚。让我们再慢慢来。C++计算2*2+1*1有两种方法。
方法1:2*2+1*1 ---> 4+1*1 ---> 4+1 --->5
方法2:2*2+1*1 ---> 2*2+1 ---> 4+1 --->5
在两种情况下,我们得到相同的答案。
让我们用一个不同的表达式再试一次:i[0]+i[2]
方法1:i[0]+i[2] ---> 2+i[2] ---> 2+4 ---> 6
方法2:i[0]+i[2] ---> i[0]+4 ---> 4+4 ---> 8
我们得到了不同的答案,因为“[]”具有副作用,所以做i [0]i [2]的先后顺序很重要。根据C ++,这两个答案都是有效的。现在,我们准备攻击您原来的问题。正如您很快就会看到的那样,它与“<<”运算符几乎没有任何关系。
C ++如何处理cout << i [0] << i [2]?与之前一样,有两种选择。
方法1:cout << i [0] << i [2] ---> cout << 2 << i [2] ---> cout << 2 << 4。
方法2:cout << i[0] << i[2] ---> cout << i[0] << 4 ---> cout << 4 << 4。
第一个方法将打印24,就像您预期的那样。但是根据C ++,方法2同样好,并且将打印44,就像您看到的那样。请注意,麻烦发生在调用“<<”之前。无法重载“<<”以防止此问题,因为在运行“<<”时,“损坏”已经发生了。

+1 是为了提到这里的 [] 方法是多么愚蠢(以及其他提到的东西!)因为我以前从未见过那种用法! - Alec Teal
@MarkVY 你好,Mark。你在哪里看到 C++ 没有运算符优先级?* 运算符的优先级高于 +。无论如何,这段代码示例中的问题不在下标操作符 [ ] 的实现中,而是在 << 中。 - BugShotGG
@GeoPapas 我认为你误读了这个答案。C++确实有运算符优先级,*的优先级高于+,这个答案假设你已经知道了这一点。也许你把优先级和评估顺序混淆了。 - M.M
2*2 + 1*1 这个例子并不是很清晰,因为在现实中编译器只会替换为 5。我建议将其改为仅使用 f() + g() 的示例。 - M.M

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