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

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


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

    return *this;
}

Разрешить вызов неконстантных функций-членов в таких случаях, как:


( a = b ).f();

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

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


person jasonline    schedule 15.03.2010    source источник
comment
Если вы хотите, чтобы люди воспринимали присваивание как выражение, а не как выражение, вы могли бы вернуть void. Это остановит (a=b)=c, a=(b=c) и любые другие подобные махинации, которые могли бы выявить разницу между значением и ссылкой.   -  person Steve Jessop    schedule 15.03.2010
comment
Я счел полезным возвращать void в операторе присваивания, когда мне нужно было предотвратить автоматическое уничтожение объектов по мере их выхода из стека. Для объектов с подсчетом ссылок вы не хотите, чтобы деструкторы вызывались, когда вы о них не знаете.   -  person cjcurrie    schedule 12.01.2013


Ответы (10)


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

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

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

int &a = (some_int = 0); // works
person Johannes Schaub - litb    schedule 15.03.2010
comment
Да, я понимаю, это бесполезная трата. Мне просто интересно, есть ли случай, когда возврат по значению или ссылке делает операцию присваивания неправильным / неправильным значением. - person jasonline; 15.03.2010
comment
@Johannes: Извини, я не понял твоего последнего предложения. Не хочешь объяснить? - person jasonline; 15.03.2010
comment
@jasonline: obj1 = obj2 возвращает временное значение. когда кто-то, использующий ваш класс, пытается создать ссылку на (obj1 = obj2), увидит, что: 1 - он не будет компилироваться, если это неконстантная ссылка, 2 - он создаст ссылку на временный объект (не на obj1 или obj2), что запутает их, поскольку примитивные типы так не работают (см. пример litb). - person tiftik; 15.03.2010
comment
@tiftik: Вы говорите, что что-то вроде A & z = (x = y) не будет компилироваться, поскольку то, что возвращается (x = y), является временным, а ваша ссылка не является константой? - person jasonline; 16.03.2010
comment
@jasonline: Да. Вы его скомпилировали? - person tiftik; 16.03.2010
comment
@tiftik: Да, я понимаю вашу точку зрения. Мне просто интересно, почему он успешно компилируется в VC ++ 2008, объявляя operator = для возврата по значению. - person jasonline; 16.03.2010
comment
@jasonline, VC ++ использует нестандартное расширение. Поднимите уровни предупреждений, и вы увидите, что оно предупреждает. Временное не будет привязываться к неконстантной ссылке в строгом стандартном режиме. - person Johannes Schaub - litb; 16.03.2010

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

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

person David Rodríguez - dribeas    schedule 15.03.2010

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

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

person mgautierfr    schedule 15.03.2010
comment
Почему бы не сделать так, чтобы он возвращал константную ссылку? Таким образом, вы не будете делать копию и не сможете изменить возвращаемый объект. - person Yngve Hammersland; 15.03.2010

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

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

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

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

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

person Jerry Coffin    schedule 15.03.2010
comment
Да, это действительно необходимо. Но ты прав, странно так кодить. - person jasonline; 15.03.2010
comment
Есть много программистов на C +, которые делают такие ужасно умные вещи. Я так часто вижу подобные ужасы, что чувствую, что живу в малобюджетном слэшере. - person James Schek; 15.03.2010
comment
@Tadeusz: Написание (a=b)=c вида наказуемо, поскольку это неопределенное поведение для встроенных типов. Только не тогда, когда operator= - это вызов функции. - person Steve Jessop; 15.03.2010
comment
В пункте 10 Библии, я имею в виду эффективный c ++, говорится, что причина возврата * this из operator = () состоит в том, чтобы разрешить цепочку присваиваний. - person Graphics Noob; 15.03.2010
comment
@Graphics Noob: Да, я читал это. Но это не совсем так. Даже если он реализован как возврат по значению, вы все равно можете сказать a = b = c; и это все еще работало. - person jasonline; 15.03.2010
comment
@GraphicsNoob, это неверное толкование того, что там написано. Автор утверждает, что это связано с поведением встроенных типов. - person C S; 01.11.2012

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

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() изменяет объект, он не изменяет временный.

person Dan    schedule 15.03.2010

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

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

person Peter Alexander    schedule 15.03.2010


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

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


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

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

a = b = c;

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

(a = b) = c;

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

(a = b) = c;

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

person Michael Burr    schedule 15.03.2010
comment
@Michael: Спасибо за дополнительное (и ясное) объяснение этой разницы в C и C ++ и точек последовательности. Никогда не думал, что есть разница. Во всяком случае, меня просто беспокоит, как это реализовать правильно (например, как это делают примитивы) и зачем реализовывать это таким образом. Я не собираюсь возвращать void, так как это отключит цепочку, что обычно должно быть разрешено. - person jasonline; 16.03.2010

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

a = b = c = d;

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

  1. Copy assignment opertor = for c делает c равным d, а затем создает временный анонимный объект (вызывает copy ctor). Назовем это tc.
  2. Затем вызывается operator = для b. Объект справа - tc. Вызывается оператор присваивания перемещения. b становится равным tc. Затем функция копирует b во временный аноним, назовем его tb.
  3. То же самое происходит снова, a.operator= возвращает временную копию a. После оператора ; все три временных объекта уничтожаются

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

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

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

Итого: вызывается всего три оператора копирования и никаких операторов!

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

P.S.: оператор присваивания копий: operator=(const Class& rhs).

Оператор присваивания перемещения: operator=(Class&& rhs).

person yanpas    schedule 27.04.2016

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

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

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

person patros    schedule 15.03.2010