Подтвердить что ты не робот

Понимание примера перегруженного оператора []

Я смущен вопросом, который я видел в тесте С++. Код здесь:

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

4b9b3361

Ответ 1

Эта программа имеет неопределенное поведение: компилятору не требуется оценивать i[0] и i[2] в порядке слева направо (язык С++ дает эту свободу компиляторам, чтобы разрешить оптимизацию).

Например, Clang делает это в этом экземпляре, а GCC делает не.

Порядок оценки не указан, поэтому вы не можете ожидать согласованного вывода, даже если вы повторно выполняете свою программу на определенной машине.

Если вы хотите получить согласованный вывод, вы можете перефразировать вышеуказанное (или каким-то эквивалентным образом):

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

Как вы можете видеть, GCC больше не выводит 44 в этом случае.

EDIT:

Если по какой-либо причине вы действительно хотите, чтобы выражение cout << i[0] << i[2] печатало 24, вам придется значительно изменить определение перегруженных операторов (operator [] и operator <<), поскольку язык преднамеренно не позволяет определить, какое подвыражение (i[0] или [i2]) оценивается первым.

Единственная гарантия, которую вы получаете, заключается в том, что результат оценки i[0] будет вставлен в cout до результата оценки i[2], поэтому лучше всего позволить operator << выполнить модификацию члена Int's данных v, а не operator [].

Однако дельта, которая должна применяться к v, передается как аргумент 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 <<, чтобы он принял объект Decorator, изменяет ссылочный объект Int, применяя сохраненную дельта к элементу данных v, а затем печатает свое значение:

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

Вот живой пример.

Отказ от ответственности: как отмечали другие, имейте в виду, что operator [] и operator << обычно являются неперемещающими операциями (точнее, они не мутируют состояние объекта, который индексируется и вставляется потоком, соответственно), поэтому вам очень не рекомендуется писать такой код, если вы просто не пытаетесь решить некоторые пустяки С++.

Ответ 2

Чтобы объяснить, что происходит здесь, сделайте вещи намного проще: cout<<2*2+1*1;. Что происходит сначала, 2 * 2 или 1 * 1? Один из возможных ответов - 2 * 2, должен произойти первым, так как это самая левая вещь. Но стандарт С++ говорит: кто заботится?! В конце концов, результат 5 в любом случае. Но иногда это имеет значение. Например, если f и g - две функции, и мы выполняем f()+g(), тогда нет гарантии, которая сначала вызывается. Если f печатает сообщение, но g завершает работу программы, сообщение никогда не будет напечатано. В вашем случае i[2] получил вызов до i[0], потому что С++ считает, что это не имеет значения. У вас есть два варианта:

Один из вариантов - изменить код, чтобы он не имел значения. Перепишите свой оператор [], чтобы он не менял Int, и вместо этого возвращает новый Int. Это, вероятно, хорошая идея, так как это будет соответствовать 99% всех других операторов [] на планете. Он также требует меньше кода:

Int &operator[](int x) { return this->v + x;}.

Другой вариант - сохранить [] то же самое и разделить печать на два оператора:

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

Некоторые языки действительно гарантируют, что в 2*2+1*1 сначала выполняется 2 * 2. Но не С++.

Редактировать: Я не был таким ясным, как я надеялся. Попробуйте более медленно. Для С++ можно оценить два параметра 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]. Согласно С++, это оба действительных ответа. Теперь мы готовы атаковать вашу оригинальную проблему. Как вы скоро увидите, это почти не имеет отношения к оператору <<.

Как работает С++ с 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, как вы ожидали. Но в соответствии с С++ метод 2 одинаково хорош, и он будет печатать 44, как вы видели. Обратите внимание, что проблема возникает до вызова <<. Невозможно перегрузить <<, чтобы предотвратить это, потому что к моменту выполнения << "повреждение" уже выполнено.