В операторе присваивания класса вам обычно нужно проверить, является ли назначаемый объект вызывающим объектом, чтобы не облажаться:
Class& Class::operator=(const Class& rhs) {
if (this != &rhs) {
// do the assignment
}
return *this;
}
Вам нужно то же самое для оператора присваивания перемещения? Есть ли когда-нибудь ситуация, когда this == &rhs
было бы правдой?
? Class::operator=(Class&& rhs) {
?
}
c++
c++11
move-semantics
move-assignment-operator
Сет Карнеги
источник
источник
A a; a = std::move(a);
.std::move
нормально. Затем примите во внимание псевдонимы, и когда вы находитесь глубоко внутри стека вызовов, и у вас есть одна ссылкаT
, а другая ссылка наT
... собираетесь ли вы проверять личность прямо здесь? Вы хотите найти первый вызов (или вызовы), при котором документальное подтверждение того, что вы не можете передать один и тот же аргумент дважды, статически докажет, что эти две ссылки не будут псевдонимами? Или вы сделаете так, чтобы самостоятельное задание работало?std::sort
orstd::shuffle
- каждый раз, когда вы меняете местамиi
th иj
th элементы массива без предварительной проверкиi != j
. (std::swap
реализовано с точки зрения назначенияОтветы:
Вау, здесь столько всего нужно навести порядок ...
Во-первых, копирование и замена - не всегда правильный способ реализации назначения копирования. В случае с почти наверняка
dumb_array
это неоптимальное решение.Использование копирования и Обмена для
dumb_array
является классическим примером того , чтобы поместить наиболее дорогостоящую операцию с полнейшими функциями в нижнем слое. Он идеально подходит для клиентов, которым нужна максимально полная функциональность и которые готовы платить штраф за производительность. Они получают именно то, что хотят.Но это катастрофа для клиентов, которым не нужна полная функциональность, а вместо этого нужна максимальная производительность. Для них
dumb_array
это просто еще одна программа, которую они должны переписать, потому что она слишком медленная. Если бы онdumb_array
был разработан по-другому, он мог бы удовлетворить обоих клиентов без каких-либо компромиссов ни для одного из них.Ключом к удовлетворению обоих клиентов является создание максимально быстрых операций на самом низком уровне, а затем добавление API поверх этого для более полных функций с более высокими затратами. Т.е. вам нужна сильная гарантия исключения, ладно, вы за это платите. Вам это не нужно? Вот более быстрое решение.
Давайте конкретизируем: вот быстрый, простой оператор присваивания копий с гарантией исключений для
dumb_array
:Объяснение:
Одна из самых дорогих вещей, которые вы можете сделать на современном оборудовании, - это прогуляться до кучи. Все, что вы можете сделать, чтобы не попасть в кучу, - это потраченное время и усилия. Клиенты
dumb_array
вполне могут захотеть часто назначать массивы одинакового размера. И когда они это сделают, все, что вам нужно сделать, этоmemcpy
(скрыть подstd::copy
). Вы же не хотите выделять новый массив того же размера, а затем освобождать старый массив того же размера!Теперь для ваших клиентов, которым действительно нужна надежная безопасность исключений:
Или, может быть, если вы хотите воспользоваться назначением перемещения в C ++ 11, это должно быть:
Если
dumb_array
клиенты ценят скорость, они должны вызватьoperator=
. Если им нужна строгая безопасность исключений, они могут вызывать общие алгоритмы, которые будут работать с широким спектром объектов и должны быть реализованы только один раз.Теперь вернемся к исходному вопросу (который на данный момент имеет тип-o):
На самом деле это спорный вопрос. Некоторые скажут "да", безусловно, некоторые скажут "нет".
Мое личное мнение - нет, эта проверка вам не нужна.
Обоснование:
Когда объект привязывается к ссылке rvalue, это одно из двух:
Если у вас есть ссылка на объект, который фактически является временным, то по определению у вас есть уникальная ссылка на этот объект. На него нельзя ссылаться где-либо еще во всей вашей программе. Т.е.
this == &temporary
не возможно .Теперь, если ваш клиент солгал вам и пообещал вам, что вы получаете временное, когда это не так, тогда ответственность за то, чтобы вам не было наплевать, является обязанностью клиента. Если вы хотите быть действительно осторожными, я считаю, что это будет лучшая реализация:
Т.е. если вы будете передать ссылку уверенности, что это ошибка со стороны клиента , которая должна быть исправлена.
Для полноты, вот оператор присваивания перемещения для
dumb_array
:В типичном случае использования назначения перемещения,
*this
это будет объект, из которого был перемещен объект, поэтому он неdelete [] mArray;
должен работать. Очень важно, чтобы реализации выполняли удаление для nullptr как можно быстрее.Предостережение:
Некоторые будут утверждать, что
swap(x, x)
это хорошая идея или просто необходимое зло. И это, если своп переходит в своп по умолчанию, может вызвать назначение самостоятельного перемещения.Я не согласен , что
swap(x, x)
это когда - либо хорошая идея. Если обнаружится в моем собственном коде, я сочту это ошибкой производительности и исправлю. Но в случае, если вы хотите разрешить это,swap(x, x)
знайте , что self-move-assignemnet выполняет только перемещенное значение. И в нашемdumb_array
примере это будет совершенно безвредно, если мы просто опустим утверждение или ограничим его регистром перемещенного объекта:Если вы сами назначаете два перемещенных (пустых)
dumb_array
объекта, вы не делаете ничего неправильного, кроме вставки бесполезных инструкций в вашу программу. То же самое наблюдение можно сделать для подавляющего большинства объектов.<
Обновить>
Я еще раз подумал над этим вопросом и несколько изменил свою позицию. Теперь я считаю, что назначение должно допускать самостоятельное назначение, но что условия публикации при назначении копии и назначении перемещения отличаются:
Для передачи копий:
нужно иметь постусловие, значение которого
y
не должно изменяться. Когда&x == &y
это постусловие преобразуется в: назначение самокопирования не должно влиять на значениеx
.Для присвоения хода:
у каждого должно быть постусловие, которое
y
имеет действительное, но неуказанное состояние. Когда&x == &y
тогда это постусловие переводится в:x
имеет допустимое, но неуказанное состояние. Т.е. назначение самостоятельного перемещения не обязательно должно быть бездействующим. Но он не должен падать. Это пост-условие соответствует разрешениюswap(x, x)
просто работать:Вышеупомянутое работает, пока
x = std::move(x)
не вылетает. Он может уйтиx
в любом допустимом, но неуказанном состоянии.Я вижу три способа запрограммировать для этого оператор присваивания перемещения
dumb_array
:Вышеупомянутая реализация допускает самоназначение, но
*this
и вother
конечном итоге становится массивом нулевого размера после назначения самоперемещения, независимо от того, каково исходное значение*this
. Это отлично.Вышеупомянутая реализация допускает самоназначение так же, как и оператор присваивания копии, делая его запретным. Это тоже нормально.
Вышесказанное нормально, только если
dumb_array
не содержит ресурсов, которые должны быть уничтожены "немедленно". Например, если единственным ресурсом является память, все в порядке. Еслиdumb_array
бы возможно было удерживать мьютексные блокировки или открытое состояние файлов, клиент мог бы разумно ожидать, что эти ресурсы в левой части назначения перемещения будут немедленно освобождены, и, следовательно, эта реализация может быть проблематичной.Стоимость первого - два дополнительных магазина. Стоимость второго - пробная. Оба работают. Оба соответствуют всем требованиям Таблицы 22 Требования к MoveAssignable в стандарте C ++ 11. Третий также работает по модулю не связанных с памятью ресурсов.
Все три реализации могут иметь разную стоимость в зависимости от оборудования: Насколько дорого обходится филиал? Регистров много или очень мало?
Вывод заключается в том, что присваивание с самоперемещением, в отличие от присваивания с самокопированием, не должно сохранять текущее значение.
<
/Обновить>
Последнее (надеюсь) редактирование, вдохновленное комментарием Люка Дантона:
Если вы пишете высокоуровневый класс, который напрямую не управляет памятью (но может иметь базы или члены, которые это делают), то лучшей реализацией назначения перемещения часто является:
Это переместит назначение каждой базы и каждого члена по очереди и не будет включать
this != &other
проверку. Это даст вам высочайшую производительность и базовую безопасность исключений, если не нужно поддерживать инварианты среди ваших баз и членов. Укажите клиентам, которым требуется надежная защита от исключенийstrong_assign
.источник
std::swap(x,x)
, то почему я должен доверять ему в правильной обработке более сложных операций?std::swap(x,x)
он просто работает, даже еслиx = std::move(x)
дает неопределенный результат. Попытайся! Вы не должны мне верить.swap
работает до тех пор, покаx = move(x)
выходитx
в любом состоянии перехода в состояние. И алгоритмыstd::copy
/std::move
определены таким образом, чтобы уже производить неопределенное поведение на бездействующих копиях (ой; 20-летнийmemmove
парень правильно понимает тривиальный случай, ноstd::move
не делает этого!). Так что, наверное, я еще не придумал «данк» для самостоятельного задания. Но очевидно, что самоназначение - это то, что часто случается в реальном коде, независимо от того, благословил это Стандарт или нет.Во-первых, вы ошиблись подписью оператора присваивания перемещения. Поскольку перемещения крадут ресурсы у исходного объекта, источник должен быть
const
ссылкой, не имеющей значения r.Обратите внимание, что вы по-прежнему возвращаетесь через (не
const
) l -значную ссылку.Для любого типа прямого назначения стандарт не проверяет самоназначение, а проверяет, не вызывает ли самоназначение аварийного отказа. Как правило, никто явно не делает
x = x
илиy = std::move(y)
звонки, но сглаживание, особенно через несколько функций, может привестиa = b
илиc = std::move(d)
в бытие само-заданий. Явная проверка на самоназначение, т.this == &rhs
Е. Пропускающая суть функции при истинном значении, является одним из способов гарантировать безопасность самоназначения. Но это один из худших способов, поскольку он оптимизирует (надеюсь) редкий случай, в то время как он является антиоптимизацией для более распространенного случая (из-за ветвления и, возможно, промахов кеша).Теперь, когда (по крайней мере) один из операндов является непосредственно временным объектом, у вас никогда не может быть сценария самоназначения. Некоторые люди рекомендуют предположить этот случай и настолько оптимизировать код для него, что код становится самоубийственно глупым, когда предположение неверно. Я говорю, что безответственно перекладывать проверку одного и того же объекта на пользователей. Мы не приводим этот аргумент в пользу копирования-присваивания; зачем менять позицию для присвоения хода?
Приведем пример, измененный с другого респондента:
Это копирование-присваивание изящно обрабатывает самоназначение без явной проверки. Если исходный и целевой размеры различаются, то копированию предшествует освобождение и перераспределение. В противном случае будет выполнено просто копирование. Самостоятельное назначение не получает оптимизированного пути, оно сбрасывается в тот же путь, что и при одинаковом исходном и целевом размерах. Копирование технически не нужно, когда два объекта эквивалентны (в том числе, когда они являются одним и тем же объектом), но это цена, когда не выполняется проверка равенства (по значению или по адресу), поскольку указанная проверка сама по себе будет пустой тратой. времени. Обратите внимание, что здесь самоприсвоение объекта вызовет серию самоназначений на уровне элемента; для этого тип элемента должен быть безопасным.
Как и его исходный пример, это присваивание копии обеспечивает основную гарантию безопасности исключений. Если вам нужна строгая гарантия, используйте оператор унифицированного присваивания из исходного запроса копирования и обмена , который обрабатывает как копирование , так и перемещение. Но суть этого примера в том, чтобы снизить безопасность на один уровень, чтобы набрать скорость. (Кстати, мы предполагаем, что значения отдельных элементов независимы; что нет инвариантного ограничения, ограничивающего одни значения по сравнению с другими.)
Давайте посмотрим на назначение ходов для этого же типа:
Подключаемый тип, который требует настройки, должен иметь функцию без двух аргументов, вызываемую
swap
в том же пространстве имен, что и тип. (Ограничение пространства имен позволяет неквалифицированным вызовам обмениваться данными для работы.) Тип контейнера также должен добавлять общедоступнуюswap
функцию-член, чтобы соответствовать стандартным контейнерам. Если членswap
не указан, то,swap
вероятно, бесплатную функцию нужно пометить как друга заменяемого типа. Если вы настраиваете ходы для использованияswap
, вы должны предоставить свой собственный код подкачки; стандартный код вызывает код перемещения типа, что приводит к бесконечной взаимной рекурсии для типов, настроенных для перемещения.Как и деструкторы, функции подкачки и операции перемещения должны никогда не выбрасываться, если это вообще возможно, и, вероятно, помечены как таковые (в C ++ 11). Стандартные библиотечные типы и подпрограммы оптимизированы для неперебрасываемых движущихся типов.
Эта первая версия переезда выполняет основной контракт. Маркеры ресурсов источника передаются объекту назначения. Утечка старых ресурсов не произойдет, поскольку теперь ими управляет исходный объект. И исходный объект остается в рабочем состоянии, и к нему могут быть применены дальнейшие операции, включая присвоение и уничтожение.
Обратите внимание, что это назначение перемещения автоматически безопасно для самостоятельного назначения, так как
swap
вызов является. Это также строго безопасно. Проблема в ненужном удержании ресурсов. Старые ресурсы для места назначения концептуально больше не нужны, но здесь они все еще существуют только для того, чтобы исходный объект мог оставаться действительным. Если до запланированного уничтожения исходного объекта еще далеко, мы тратим пространство ресурсов впустую или, что еще хуже, если общее пространство ресурсов ограничено, и другие запросы на ресурсы произойдут до того, как (новый) исходный объект официально умрет.Эта проблема является причиной противоречивых советов нынешних гуру относительно самонацеливания во время назначения хода. Способ записи перемещения-назначения без лишних ресурсов выглядит примерно так:
Источник сбрасывается до условий по умолчанию, а старые ресурсы назначения уничтожаются. В случае самостоятельного назначения ваш текущий объект совершает самоубийство. Основной способ обойти это - окружить код действия
if(this != &other)
блоком или прикрутить его и позволить клиентам съестьassert(this != &other)
начальную строку (если вы чувствуете себя хорошо).Альтернативой является изучение того, как сделать копирующее присваивание строго безопасным, без унифицированного присваивания, и применить его к перемещению-присваиванию:
Когда
other
иthis
различны,other
опорожняется при перемещенииtemp
и остается в этом положении. Затемthis
теряет свои старые ресурсы, чтобыtemp
получить ресурсы, изначально удерживаемыеother
. Тогда старые ресурсыthis
погибнут, когда этоtemp
произойдет.Когда само присваивание происходит, опорожнение
other
вtemp
стеклотары ,this
а также. Затем целевой объект получает свои ресурсы обратно , когдаtemp
иthis
своп. Смертьtemp
требует пустой объект, который должен быть практически закрытым. Объектthis
/other
сохраняет свои ресурсы.Назначение ходов никогда не должно выполняться, пока также выполняются конструирование ходов и замена. Плата за безопасность во время самоназначения - это еще несколько инструкций по сравнению с низкоуровневыми типами, которые должны быть вытеснены вызовом освобождения.
источник
delete
второй блок кода?std::copy
вызывает неопределенное поведение, если диапазоны источника и назначения перекрываются (включая случай, когда они совпадают). См. C ++ 14 [alg.copy] / 3.Я нахожусь в лагере тех, кому нужны безопасные операторы с самоназначением, но я не хочу писать проверки самоназначения в реализациях
operator=
. И на самом деле я даже не хочу вообще реализовыватьoperator=
, я хочу, чтобы поведение по умолчанию работало «прямо из коробки». Лучшие специальные участники - это те, которые приходят бесплатно.При этом требования MoveAssignable, представленные в Стандарте, описаны следующим образом (из 17.6.3.1 Требования к аргументам шаблона [utility.arg.requirements], n3290):
где заполнители описываются как: «
t
[является] изменяемым lvalue типа T;» и "rv
является значением типа T;". Обратите внимание, что это требования, предъявляемые к типам, используемым в качестве аргументов для шаблонов стандартной библиотеки, но, глядя в другое место в стандарте, я замечаю, что все требования при назначении перемещения аналогичны этому.Это означает, что
a = std::move(a)
это должно быть «безопасно». Если вам нужен тест идентичности (напримерthis != &other
), сделайте это, иначе вы даже не сможете поместить в него свои объектыstd::vector
! (Если вы не используете те элементы / операции, которые требуют MoveAssignable; но не обращайте на это внимания.) Обратите внимание, что в предыдущем примереa = std::move(a)
тогдаthis == &other
действительно будет.источник
a = std::move(a)
отсутствие работы приводит к тому, что класс не работаетstd::vector
? Пример?std::vector<T>::erase
не разрешен, если толькоT
не MoveAssignable. (Помимо IIRC, некоторые требования MoveAssignable были смягчены до MoveInsertable вместо C ++ 14.)T
должно быть MoveAssignable, но зачемerase()
вообще зависеть от перемещения элемента в себя ?Поскольку ваша текущая
operator=
функция написана, поскольку вы указали аргумент rvalue-referenceconst
, вы не можете «украсть» указатели и изменить значения входящей ссылки rvalue ... вы просто не можете это изменить, вы мог только читать оттуда. Я бы увидел проблему только в том случае, если бы вы начали вызыватьdelete
указатели и т. Д. В своемthis
объекте, как в обычном методе lvaue-referenceoperator=
, но такого рода поражает точку rvalue-версии ... т.е. кажется излишним использовать версию rvalue для выполнения тех же операций, которые обычноconst
выполняютсяoperator=
методом -lvalue .Теперь, если вы определили, что
operator=
принимаетconst
ссылку, отличную от rvalue, то единственный способ увидеть, что требуется проверка, - это передатьthis
объект функции, которая намеренно вернула ссылку rvalue, а не временную.Например, предположим, что кто-то попытался написать
operator+
функцию и использовать сочетание ссылок rvalue и ссылок lvalue, чтобы «предотвратить» создание дополнительных временных файлов во время некоторой операции сложения с типом объекта:Теперь, исходя из того, что я понимаю о ссылках на rvalue, выполнение вышеуказанного не рекомендуется (т. Е. Вы должны просто вернуть временную ссылку, а не ссылку rvalue), но, если кто-то все еще будет это делать, тогда вы захотите проверить, чтобы убедитесь, что входящая rvalue-ссылка не ссылается на тот же объект, что и
this
указатель.источник
const
, то вы можете только читать из нее, поэтому единственное, что нужно Сделать проверку было бы, если бы вы решилиoperator=(const T&&)
выполнить ту же повторную инициализацию,this
которую вы выполняли бы обычнымoperator=(const T&)
методом, а не операцию в стиле подкачки (например, кражу указателей и т. д., а не создание глубоких копий).Мой ответ по-прежнему заключается в том, что назначение ходов не обязательно должно исключать самоопределение, но у этого есть другое объяснение. Рассмотрим std :: unique_ptr. Если бы я реализовал один, я бы сделал что-то вроде этого:
Если вы посмотрите на объяснения Скотта Мейерса, он делает нечто подобное. (Если побродить, почему бы не сделать своп - там одна лишняя запись). И это небезопасно для самостоятельного назначения.
Иногда это досадно. Рассмотрите возможность удаления из вектора всех четных чисел:
Это нормально для целых чисел, но я не верю, что вы можете заставить что-то подобное работать с семантикой перемещения.
В заключение: переместить назначение самому объекту - это не нормально, и вам нужно следить за этим.
Небольшое обновление.
swap(x, x)
должно работать. Алгоритмам это нравится! Всегда приятно, когда угловой шкаф просто работает. (И я еще не видел случая, чтобы это было не бесплатно. Хотя это не значит, что его не существует).unique_ptr& operator=(unique_ptr&& u) noexcept { reset(u.release()); ...}
это безопасно для самостоятельного присвоения.источник
Есть ситуация, о которой (это == rhs) я могу думать. Для этого утверждения: Myclass obj; std :: move (obj) = std :: move (obj)
источник