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吗?

4

2 回答 2

16

i[0]该程序具有不确定的行为:编译器不需要按i[2]从左到右的顺序进行评估(C++ 语言为编译器提供了这种自由以允许优化)。

例如,Clang 在这种情况下会这样做,而GCC 不会

评估的顺序是未指定的,因此您不能期望得到一致的输出,即使在特定机器上重复运行程序时也是如此。

如果您想获得一致的输出,可以将上述内容改写如下(或以某种等效方式):

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

如您所见,在这种情况下,GCC不再输出。44

编辑:

如果出于某种原因,您真的希望表达式cout << i[0] << i[2]print 24,则必须显着修改重载运算符 ( operator []and operator <<) 的定义,因为该语言故意使无法判断哪个子表达式 ( i[0]or [i2]) 首先被评估。

您在这里获得的唯一保证是评估结果将在评估结果之前i[0]插入,因此您最好的选择可能是让执行数据成员的修改,而不是.couti[2]operator <<Int'svoperator []

但是,应该应用的 deltav作为参数传递给operator [],并且您需要某种方式将其operator <<与原始Int对象一起转发。一种可能性是让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 <<以便它接受一个对象,通过将存储的 delta 应用于数据成员来Decorator修改引用的对象,然后打印其值:Intv

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

这是一个活生生的例子

免责声明:正如其他人所提到的,请记住,operator []并且operator <<通常是非变异操作(更准确地说,它们不会改变分别被索引和流插入的对象的状态),因此非常不鼓励您编写代码像这样,除非你只是想解决一些 C++ 琐事。

于 2015-05-22T23:09:28.373 回答
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];

某些语言实际上确实保证 in 2*2+1*1,首先完成 2*2 。但不是 C++。

编辑:我并不像我希望的那样清楚。让我们慢慢尝试。C++ 有两种评估2*2+1*1.

方法一:2*2+1*1 ---> 4+1*1 ---> 4+1 --->5

方法二:2*2+1*1 ---> 2*2+1 ---> 4+1 --->5

在这两种情况下,我们都会得到相同的答案。

让我们用不同的表达式再试一次:i[0]+i[2].

方法一:i[0]+i[2] ---> 2+i[2] ---> 2+4 ---> 6

方法二:i[0]+i[2] ---> i[0]+4 ---> 4+4 ---> 8

我们得到了不同的答案,因为它[]有副作用,所以不管是先做i[0]还是先做都很重要i[2]。根据 C++,这些都是有效的答案。现在,我们已准备好解决您最初的问题。您很快就会看到,它几乎与操作员无关<<

C++是如何处理的cout << i[0] << i[2]?和以前一样,有两种选择。

方法一:cout << i[0] << i[2] ---> cout << 2 << i[2] ---> cout << 2 << 4

方法二:cout << i[0] << i[2] ---> cout << i[0] << 4 ---> cout << 4 << 4

第一种方法将像您预期的那样打印 24 。但是根据 C++,方法 2 同样好,它会像你看到的那样打印 44。请注意,问题发生在<<被调用之前。没有办法超载<<来防止这种情况,因为到时间<<运行时,“损坏”已经造成。

于 2015-05-23T06:55:06.263 回答