Перегрузка оператора присваивания в C++

Как я понимаю, при перегрузке operator= возвращаемое значение должно быть неконстантной ссылкой.


A& A::operator=( const A& )
{
    // check for self-assignment, do assignment

    return *this;
}

Это неконстантно, чтобы позволить неконстантные функции-члены вызываться в таких случаях, как:


( a = b ).f();

Но зачем возвращать ссылку? В каком случае это вызовет проблему, если возвращаемое значение не объявлено как ссылка, скажем, возвращаемое по значению?

Предполагается, что конструктор копирования реализован правильно.

10 ответов

Решение

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

a = b; // huh, why does this create an unnecessary copy?

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

int &a = (some_int = 0); // works

Хороший общий совет при перегрузке операторов: "делай так, как делают примитивные типы", и поведение приписывания примитивному типу по умолчанию таково.

Невозможно ничего вернуть, можно отключить присваивание внутри других выражений, если вы чувствуете необходимость, но возвращать копию вообще не имеет смысла: если вызывающий объект хочет сделать копию, он может сделать это из ссылки, если он не нужна копия, нет необходимости создавать временные данные, которые не нужны.

Причина f() может изменить. (мы возвращаем неконстантную ссылку)

Если мы вернем значение (копию), f() изменит копию, а не

Я не уверен, как часто вы хотели бы сделать это, но что-то вроде: (a=b)=c; требует ссылки на работу.

Изменить: Хорошо, это немного больше, чем это. Большая часть рассуждений является полуисторической. Есть больше причин, по которым вы не хотите возвращать значение, чем просто избегать ненужного копирования во временный объект. Используя (незначительную) вариацию на примере, первоначально опубликованном Эндрю Кенигом, рассмотрим что-то вроде этого:

struct Foo { 
    Foo const &assign(Foo const &other) { 
        return (*this = other);
    }
};

Теперь предположим, что вы используете старую версию C++, где присваивание вернуло значение. В таком случае, (*this=other); даст это временное. Затем вы привязываете ссылку к временному, уничтожаете временное и, наконец, возвращаете висячую ссылку на уничтоженное временное.

Правила, вступившие в силу с тех пор (продление срока службы временного хранилища, используемого для инициализации ссылки), по крайней мере, смягчают (и могут полностью излечить) эту проблему, но я сомневаюсь, что кто-нибудь повторно посещал эту конкретную ситуацию после написания этих правил. Это было немного похоже на уродливый драйвер устройства, который включает в себя ключи для работы с десятками ошибок в разных версиях и вариациях аппаратного обеспечения - его, вероятно, можно было бы реорганизовать и упростить, но никто не уверен, что некоторые, казалось бы, безобидные изменения сломают что-то, что в настоящее время работает, и в конечном счете никто не хочет даже смотреть на это, если они могут помочь этому.

Если ваш оператор присваивания не принимает константный ссылочный параметр:

A& A::operator=(A&); // unusual, but std::auto_ptr does this for example.

или если класс A имеет изменяемые члены (число ссылок?), тогда возможно, что оператор присваивания изменяет объект, от которого назначается, а также назначается. Тогда, если у вас был такой код:

a = b = c;

b = c назначение произойдет первым и вернет копию (назовите ее b') по значению вместо возврата ссылки на b, Когда a = b' назначение сделано, мутирующий оператор присваивания изменит b' скопировать вместо реального b,

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

Если вы собираетесь сделать что-то вроде (a = b).f() тогда вы захотите вернуть его по ссылке, чтобы, если f() мутирует объект, он не мутирует временно.

Возврат по ссылке сокращает время выполнения связанных операций. Например:

a = b = c = d;

Давайте посмотрим действия, которые будут вызваны, если operator= возвращает по значению.

  1. Копировать назначение оператора = для c марки c равно d а затем создает временный анонимный объект (вызывает копию ctor). Давайте назовем это tc,
  2. Тогда оператор = для b называется. Правосторонний объект tc. Оператор назначения перемещения называется. b становится равным tc, А потом функция копирует b временно анонимно, давайте назовем это tb,
  3. То же самое происходит снова, a.operator= возвращает временную копию a, После оператора ; все три временных объекта уничтожены

Всего: 3 оператора копирования, 2 оператора перемещения, 1 оператор копирования

Посмотрим, что изменится, если operator = вернет значение по ссылке:

  1. Оператор присваивания копии вызывается. c становится равным d, ссылка на объект lvalue возвращается
  2. Такой же. b становится равным c, ссылка на объект lvalue возвращается
  3. Такой же. a становится равным b, ссылка на объект lvalue возвращается

В целом: вызывается только три оператора копирования, а не ctors вообще!

Более того, я рекомендую вам возвращать значение по константной ссылке, это не позволит вам написать хитрый и неочевидный код. С более чистым кодом поиск ошибок будет намного проще:) ( a = b ).f(); лучше разбить на две строки a=b; a.f();,

PS: оператор копирования копирования: operator=(const Class& rhs),

Оператор назначения перемещения: operator=(Class&& rhs),

Если вы беспокоитесь, что возвращение неправильной вещи может молча вызвать непреднамеренные побочные эффекты, вы можете написать свой operator=() возвращать void, Я видел довольно много кода, который делает это (я полагаю, из-за лени или просто не зная, какой должен быть тип возвращаемого значения, а не для "безопасности"), и это вызывает немного проблем. Вид выражений, которые должны использовать ссылку, обычно возвращаемую operator=() довольно редко используются, и почти всегда легкий код альтернативы.

Я не уверен, что одобрил бы возвращение void (в обзоре кода я, вероятно, назвал бы это как то, что вы не должны делать), но я добавлю это в качестве опции, чтобы рассмотреть, хотите ли вы не беспокоиться о том, как странно использует оператор присваивания может быть обработано.


позднее редактирование:

Кроме того, я должен был изначально упомянуть, что вы можете разделить разницу, имея operator=() вернуть const& - это по-прежнему разрешает цепочку присвоений:

a = b = c;

Но запретит некоторые из более необычных применений:

(a = b) = c;

Обратите внимание, что это заставляет оператор присваивания иметь семантику, аналогичную той, которую он имеет в C, где значение, возвращаемое = оператор не является lvalue. В C++ стандарт изменил это так, чтобы = оператор возвращает тип левого операнда, так что это lvalue, но, как отметил Стив Джессоп в комментарии к другому ответу, компилятор примет это

(a = b) = c;

даже для встроенных функций результатом является неопределенное поведение встроенных a изменяется дважды без промежуточной точки последовательности. Этой проблемы избегают для не встроенных operator=() поскольку operator=() вызов функции является точкой последовательности.

Это 10-я статья превосходной книги Скотта Мейерса " Эффективный С ++". Возврат ссылки из operator= это всего лишь соглашение, но оно хорошее.

Это всего лишь соглашение; код, который не следует за ним, будет скомпилирован. Однако за соглашением следуют все встроенные типы, а также все типы в стандартной библиотеке. Если у вас нет веских оснований для того, чтобы поступать иначе, не делайте этого.

В реальном коде (т.е. не такие вещи, как (a=b)=c), возвращение значения вряд ли вызовет какие-либо ошибки компиляции, но неэффективно возвращать копию, потому что создание копии часто может быть дорогостоящим.

Очевидно, что вы можете столкнуться с ситуацией, когда требуется справка, но на практике это случается редко - если вообще когда-либо -.

Если он вернул копию, он потребовал бы, чтобы вы реализовали конструктор копирования почти для всех нетривиальных объектов.

Также это вызовет проблемы, если вы объявите конструктор копирования закрытым, но оставите оператор присваивания открытым... вы получите ошибку компиляции, если попытаетесь использовать оператор присваивания вне класса или его экземпляров.

Не говоря уже о более серьезных проблемах, уже упомянутых. Вы не хотите, чтобы это была копия объекта, вы действительно хотите, чтобы он ссылался на тот же объект. Изменения одного должны быть видны обоим, и это не сработает, если вы вернете копию.

Другие вопросы по тегам