Переместить оператор присваивания и `if (this! = & Rhs)`

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

Class& Class::operator=(const Class& rhs) { if (this != &rhs) { // do the assignment } return *this; } 

Вам нужно то же самое для оператора присваивания переадресации? Есть ли когда-нибудь ситуация, когда this == &rhs будет правдой?

 ? Class::operator=(Class&& rhs) { ? } 

Вау, здесь просто так много, чтобы убрать здесь …

Во-первых, Copy and Swap не всегда является правильным способом реализации назначения копирования. Почти наверняка в случае с dumb_array это неоптимальное решение.

Использование Copy and Swap для dumb_array – это classический пример размещения самой дорогой операции с самыми полными функциями на нижнем уровне. Он идеально подходит для клиентов, которым нужна самая полная функция, и готовы платить штраф за исполнение. Они получают именно то, что хотят.

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

Ключом к удовлетворению обоих клиентов является построение самых быстрых операций на самом низком уровне, а затем добавление API поверх этого для более полных функций за счет более высоких затрат. Т.е. вам нужна сильная гарантия исключения, хорошо, вы платите за нее. Вам это не нужно? Вот более быстрое решение.

Давайте получим конкретный: вот быстрый, основной dumb_array исключения экземпляров Copy Assignment для dumb_array :

 dumb_array& operator=(const dumb_array& other) { if (this != &other) { if (mSize != other.mSize) { delete [] mArray; mArray = nullptr; mArray = other.mSize ? new int[other.mSize] : nullptr; mSize = other.mSize; } std::copy(other.mArray, other.mArray + mSize, mArray); } return *this; } 

Объяснение:

Одна из самых дорогих вещей, которые вы можете сделать на современном оборудовании, – это поездка в кучу. Все, что вы можете сделать, чтобы избежать поездки в кучу, – это потраченное время и усилия. Клиенты dumb_array вполне могут часто назначать массивы одинакового размера. И когда они это делают, все, что вам нужно сделать, это memcpy (скрытый под std::copy ). Вы не хотите выделять новый массив того же размера и затем освобождать старый один размер!

Теперь для ваших клиентов, которые действительно хотят сильной безопасности исключений:

 template  C& strong_assign(C& lhs, C rhs) { swap(lhs, rhs); return lhs; } 

Или, может быть, если вы хотите воспользоваться назначением перемещения в C ++ 11, то это должно быть:

 template  C& strong_assign(C& lhs, C rhs) { lhs = std::move(rhs); return lhs; } 

Если dumb_array оценивают скорость, они должны вызвать operator= . Если им нужна сильная защита исключений, существуют общие алгоритмы, которые они могут назвать, которые будут работать на самых разных объектах и ​​должны быть реализованы только один раз.

Теперь вернемся к исходному вопросу (который имеет тип-o на данный момент времени):

 Class& Class::operator=(Class&& rhs) { if (this == &rhs) // is this check needed? { // ... } return *this; } 

Это действительно спорный вопрос. Некоторые скажут «да», абсолютно, некоторые скажут «нет».

Мое личное мнение – нет, вам не нужна эта проверка.

Обоснование:

Когда объект привязывается к ссылке rvalue, это одна из двух вещей:

  1. Временный.
  2. Объект, который хочет, чтобы вы считали, является временным.

Если у вас есть ссылка на объект, который является фактическим временным, то по определению у вас есть уникальная ссылка на этот объект. На него нельзя ссылаться нигде во всей вашей программе. Т.е. this == &temporary не представляется возможным .

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

 Class& Class::operator=(Class&& other) { assert(this != &other); // ... return *this; } 

Т.е. если вы передали собственную ссылку, это ошибка со стороны клиента, которая должна быть исправлена.

Для полноты здесь оператор присваивания перемещения для dumb_array :

 dumb_array& operator=(dumb_array&& other) { assert(this != &other); delete [] mArray; mSize = other.mSize; mArray = other.mArray; other.mSize = 0; other.mArray = nullptr; return *this; } 

В типичном случае использования delete [] mArray; *this будет перемещенный объект и поэтому delete [] mArray; должен быть не-op. Очень важно, чтобы реализации делали удаление на nullptr как можно быстрее.

Предостережение:

Некоторые утверждают, что swap(x, x) – хорошая идея или просто необходимое зло. И это, если своп переходит к обмену по умолчанию, может вызвать самоперемещение-присваивание.

Я не согласен с тем, что swap(x, x) – хорошая идея. Если он найден в моем собственном коде, я рассмотрю его как ошибку производительности и исправлю ее. Но в случае, если вы хотите разрешить это, поймите, что swap(x, x) выполняет только self-move-assignemnet по сдвинутому значению. И в нашем примере dumb_array это будет совершенно безвредным, если мы просто опустим утверждение или сведем его к перенесенному случаю:

 dumb_array& operator=(dumb_array&& other) { assert(this != &other || mSize == 0); delete [] mArray; mSize = other.mSize; mArray = other.mArray; other.mSize = 0; other.mArray = nullptr; return *this; } 

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

< Обновить >

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

Для назначения копии:

 x = y; 

следует иметь пост-условие, что значение y не должно изменяться. Когда &x == &y то это постусловие преобразуется в: само копирование не должно влиять на значение x .

Для назначения переадресации:

 x = std::move(y); 

нужно иметь пост-условие, что y имеет действительное, но неуказанное состояние. Когда &x == &y то это постусловие означает: x имеет действительное, но неуказанное состояние. Т.е. самоперемещение назначения не должно быть no-op. Но он не должен падать. Это пост-условие согласуется с тем, чтобы позволить swap(x, x) просто работать:

 template  void swap(T& x, T& y) { // assume &x == &y T tmp(std::move(x)); // x and y now have a valid but unspecified state x = std::move(y); // x and y still have a valid but unspecified state y = std::move(tmp); // x and y have the value of tmp, which is the value they had on entry } 

Вышеупомянутые работы, пока x = std::move(x) не сбой. Он может оставить x в любом действительном, но неуказанном состоянии.

Я вижу три способа программирования оператора присваивания перемещения для dumb_array для достижения этого:

 dumb_array& operator=(dumb_array&& other) { delete [] mArray; // set *this to a valid state before continuing mSize = 0; mArray = nullptr; // *this is now in a valid state, continue with move assignment mSize = other.mSize; mArray = other.mArray; other.mSize = 0; other.mArray = nullptr; return *this; } 

Вышеприведенная реализация допускает самостоятельное назначение, но *this и other конечном итоге представляют собой массив нулевого размера после назначения самоперевода, независимо от исходного значения *this . Это отлично.

 dumb_array& operator=(dumb_array&& other) { if (this != &other) { delete [] mArray; mSize = other.mSize; mArray = other.mArray; other.mSize = 0; other.mArray = nullptr; } return *this; } 

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

 dumb_array& operator=(dumb_array&& other) { swap(other); return *this; } 

Вышеприведенное dumb_array справедливо только в том случае, если dumb_array не содержит ресурсы, которые следует уничтожить «немедленно». Например, если единственным ресурсом является память, то это хорошо. Если dumb_array мог бы удерживать блокировку мьютексов или открытое состояние файлов, клиент мог бы разумно ожидать, что эти ресурсы на lhs присваивания перемещения будут немедленно выпущены, и поэтому эта реализация может быть проблематичной.

Стоимость первого - это два дополнительных магазина. Стоимость второго - это тест-ответ. Оба работают. Оба соответствуют всем требованиям Таблицы 22 Требования MoveAssignable в стандарте C ++ 11. Третий также работает по модулю проблемы, не связанной с памятью.

Все три реализации могут иметь разные затраты в зависимости от оборудования: насколько дорогой является отрасль? Много ли регистров или очень мало?

Вывод заключается в том, что самоперемещение-присваивание, в отличие от самокопирования-назначения, не должно сохранять текущее значение.

< / Update >

Одно окончательное (надеюсь) редактирование, вдохновленное комментарием Люка Дантона:

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

 Class& operator=(Class&&) = default; 

Это будет перемещать назначение каждой базы и каждого члена по очереди и не будет включать this != &other проверку. Это даст вам самую высокую производительность и базовую безопасность для исключений, если не нужно поддерживать инвариантов среди ваших баз и членов. Для ваших клиентов, требующих strong_assign защиты исключений, укажите их на strong_assign .

Во-первых, вы ошиблись в сигнатуре оператора присваивания. Поскольку перемещение приводит к краху ресурсов из исходного объекта, источник должен быть ссылкой на const r-значения.

 Class &Class::operator=( Class &&rhs ) { //... return *this; } 

Обратите внимание, что вы все равно возвращаетесь через ссылку (не const ) l -value.

Для любого типа прямого назначения стандарт не должен проверять самоназначение, но чтобы убедиться, что самозапуск не вызывает сбой и сжигание. Как правило, ни один из них явно не выполняет вызовы x = x или y = std::move(y) , но сглаживание, особенно с помощью нескольких функций, может привести к тому, что a = b или c = std::move(d) станут самоначислениями. Явная проверка для самоопределения, то есть this == &rhs , которая пропускает мясо функции, когда истина является одним из способов обеспечения безопасности самонаведения. Но это один из худших способов, так как он оптимизирует (надеюсь) редкий случай, в то время как это анти-оптимизация для более распространенного случая (из-за разветвления и, возможно, промахов кэша).

Теперь, когда (по крайней мере) один из операндов является непосредственно временным объектом, вы никогда не можете иметь сценарий самонаведения. Некоторые люди выступают за принятие этого случая и оптимизируют код для него настолько, что код становится сумасшедшим глупо, когда предположение неверно. Я говорю, что сброс проверки того же объекта на пользователей безответственен. Мы не делаем этого аргумента для копирования-назначения; зачем отменить позицию для назначения переноса?

Давайте сделаем пример, измененный от другого респондента:

 dumb_array& dumb_array::operator=(const dumb_array& other) { if (mSize != other.mSize) { delete [] mArray; mArray = nullptr; // clear this... mSize = 0u; // ...and this in case the next line throws mArray = other.mSize ? new int[other.mSize] : nullptr; mSize = other.mSize; } std::copy(other.mArray, other.mArray + mSize, mArray); return *this; } 

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

Как и исходный пример, это копирование-присвоение обеспечивает основную гарантию безопасности исключений. Если вам нужна сильная гарантия, используйте оператор унифицированного присваивания из исходного запроса Copy and Swap , который обрабатывает как назначение копирования , так и перемещение. Но точка этого примера заключается в том, чтобы снизить безопасность на один ранг, чтобы получить скорость. (BTW, мы предполагаем, что значения отдельных элементов независимы, что нет никакого инвариантного ограничения, ограничивающего некоторые значения по сравнению с другими).

Давайте посмотрим на присваивание move для этого же типа:

 class dumb_array { //... void swap(dumb_array& other) noexcept { // Just in case we add UDT members later using std::swap; // both members are built-in types -> never throw swap( this->mArray, other.mArray ); swap( this->mSize, other.mSize ); } dumb_array& operator=(dumb_array&& other) noexcept { this->swap( other ); return *this; } //... }; void swap( dumb_array &l, dumb_array &r ) noexcept { l.swap( r ); } 

Тип swap который нуждается в настройке, должен иметь свободную функцию с двумя аргументами, называемую swap в том же пространстве имен, что и тип. (Ограничение пространства имен позволяет использовать неквалифицированные вызовы для свопинга.) Тип контейнера также должен добавить функцию public swap для соответствия стандартным контейнерам. Если swap не предоставляется, то swap свободной функции, вероятно, должен быть помечен как друг типа с возможностью замены. Если вы настраиваете перемещения для использования swap , вам необходимо предоставить свой собственный код обмена; стандартный код вызывает код перемещения типа, что приведет к бесконечной взаимной рекурсии для перемещаемых типов.

Подобно деструкторам, функции подкачки и операции перемещения должны быть никогда не выбрасываться, если это вообще возможно, и, возможно, помечены как таковые (в C ++ 11). Стандартные типы и типы библиотек имеют оптимизацию для недвижущихся типов перемещения.

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

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

Эта проблема вызвала противоречивые рекомендации нынешнего гуру в отношении самонастройки при переадресации. Способ записи переадресации без лишних ресурсов – это что-то вроде:

 class dumb_array { //... dumb_array& operator=(dumb_array&& other) noexcept { delete [] this->mArray; // kill old resources this->mArray = other.mArray; this->mSize = other.mSize; other.mArray = nullptr; // reset source other.mSize = 0u; return *this; } //... }; 

Источник сбрасывается до состояния по умолчанию, а старые ресурсы назначения уничтожаются. В случае с самостоятельным назначением ваш текущий объект заканчивает самоубийство. Основной путь вокруг него состоит в том, чтобы окружить код действия блоком if(this != &other) , или ввернуть его, и позволить клиентам есть исходную строку assert(this != &other) (если вы чувствуете себя хорошо).

Альтернативой является изучение того, как сделать исключение копирования исключительным безопасным без унифицированного присвоения и применить его к назначению переноса:

 class dumb_array { //... dumb_array& operator=(dumb_array&& other) noexcept { dumb_array temp{ std::move(other) }; this->swap( temp ); return *this; } //... }; 

Когда other и this разные, other опорожняются переходом к temp и остаются таким образом. Затем this теряет свои старые ресурсы до temp , получая ресурсы, первоначально принадлежащие other . Тогда старые ресурсы this будут убиты, когда это произойдет.

Когда происходит самоназвание, опорожнение other на temp освобождает this . Затем целевой объект возвращает свои ресурсы при temp и this свопе. Смерть temp претендует на пустой объект, который должен быть практически не-op. Этот / other объект сохраняет свои ресурсы.

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

Я вхожу в лагерь тех, кто хочет безопасных операторов самонаведения, но не хочу писать проверки самоопределения в реализациях operator= . И на самом деле я даже не хочу реализовать operator= вообще, я хочу, чтобы поведение по умолчанию работало «прямо из коробки». Лучшие специальные участники – это те, которые поступают бесплатно.

При этом требования MoveAssignable, представленные в стандарте, описываются следующим образом (из 17.6.3.1 Требования к аргументам шаблона [utility.arg.requirements], n3290):

 Выражение Тип возврата Возвращаемое значение Послесловие
 t = rv T & tt эквивалентно значению rv перед назначением

где заполнители описываются как: « t [является] изменяемым значением типа T;» и ” rv является rзначением типа T;”. Обратите внимание, что это требования, предъявляемые к типам, используемым в качестве аргументов для шаблонов стандартной библиотеки, но, смотря в другом месте Стандарта, я замечаю, что каждое требование для назначения переноса аналогично этому.

Это означает, что a = std::move(a) должно быть «безопасным». Если вам нужен тест идентичности (например, this != &other ), то пойдите для него, иначе вы даже не сможете разместить свои объекты в std::vector ! (Если вы не используете те элементы / операции, которые требуют MoveAssignable, но не обращайте на них внимания.) Обратите внимание, что в предыдущем примере a = std::move(a) , тогда this == &other действительно будут выполняться.

Поскольку ваша текущая функция operator= написана, поскольку вы создали аргумент rvalue-reference const , вы не можете «украсть» указатели и изменить значения входящей ссылки rvalue … вы просто не можете изменить это, вы могли только читать. Я видел бы только проблему, если бы вы начали вызывать delete в указателях и т. Д. В вашем объекте, как в обычном режиме lvaue-reference operator= , но этот вид побеждает точку rvalue-версии … т. е. представляется излишним использовать версию rvalue, чтобы в основном выполнять те же операции, которые обычно выполняются для operator= const -lvalue operator= .

Теперь, если вы определили свой operator= взять ссылку на константу rvalue, тогда единственный способ, которым я мог видеть проверку, был, если вы передали this объект функции, которая намеренно вернула ссылку rvalue, а не временную.

Например, предположим, что кто-то попытался написать operator+ функцию и использовать комбинацию ссылок rvalue и ссылок на lvalue для того, чтобы «предотвратить» создание дополнительных временных рядов во время некоторой операции суммирования сложения в тип объекта:

 struct A; //defines operator=(A&& rhs) where it will "steal" the pointers //of rhs and set the original pointers of rhs to NULL A&& operator+(A& rhs, A&& lhs) { //...code return std::move(rhs); } A&& operator+(A&& rhs, A&&lhs) { //...code return std::move(rhs); } int main() { A a; a = (a + A()) + A(); //calls operator=(A&&) with reference bound to a //...rest of code } 

Теперь, из того, что я понимаю в ссылках на rvalue, выполнение вышеуказанного не рекомендуется (т. Е. Вы должны просто вернуть временную ссылку, а не ссылку на rvalue), но, если кто-то все еще это сделал, тогда вы хотите проверить, чтобы сделать убедитесь, что входящая ссылка rvalue не ссылалась на тот же объект, что и this указатель.

Мой ответ по-прежнему заключается в том, что назначение переноса не должно быть спасено от самоопределения, но оно имеет другое объяснение. Рассмотрим std :: unique_ptr. Если бы я был реализован, я бы сделал что-то вроде этого:

 unique_ptr& operator=(unique_ptr&& x) { delete ptr_; ptr_ = x.ptr_; x.ptr_ = nullptr; return *this; } 

Если вы посмотрите на Скотта Мейерса, объясняющего это, он делает что-то подобное. (Если вы блуждаете, почему бы не сделать swap – у него есть одна дополнительная запись). И это не безопасно для самостоятельного назначения.

Иногда это несчастливо. Рассмотрим перемещение из вектора всех четных чисел:

 src.erase( std::partition_copy(src.begin(), src.end(), src.begin(), std::back_inserter(even), [](int num) { return num % 2; } ).first, src.end()); 

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

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

Небольшое обновление.

  1. Я не согласен с Говардом, это плохая идея, но все же – я думаю, что самоперемещение назначения «вывезенных» объектов должно работать, потому что swap(x, x) должен работать. Алгоритмы любят это! Всегда приятно, когда угловой корпус работает. (И я еще не видел случая, когда это не бесплатно. Не означает, что он не существует).
  2. Так как назначение unique_ptrs реализовано в libc ++: unique_ptr& operator=(unique_ptr&& u) noexcept { reset(u.release()); ...} unique_ptr& operator=(unique_ptr&& u) noexcept { reset(u.release()); ...} Это безопасно для самоперевода.
  3. Основные принципы считают, что должно быть хорошо, чтобы самостоятельно переместить назначение.

Существует ситуация, о которой я могу думать (это == rhs). Для этого утверждения: Myclass obj; std :: move (obj) = std :: move (obj)

  • Каковы правила автоматической генерации операций перемещения?
  • Почему std :: move предотвращает RVO?
  • Должны ли все / большинство функций setter в C ++ 11 записываться как шаблоны функций, принимающие универсальные ссылки?
  • В чем преимущество использования универсальных ссылок в диапазонах для циклов?
  • Что представляет собой допустимое состояние для объекта «перемещено из» в C ++ 11?
  • Оставляет ли перемещение объект в пригодном для использования состоянии?
  • Как использовать пользовательский удаленный элемент с элементом std :: unique_ptr?
  • Как std :: move () переносит значения в RValues?
  • push_back vs emplace_back
  • initializer_list и семантика перемещения
  • Могу я обычно / всегда использовать std :: forward вместо std :: move?
  • Давайте будем гением компьютера.