Существует сильный аргумент против вашей "канонической" реализации - это неправильно.
Вы заканчиваете время жизни исходного объекта и создаете новый объект вего место. Однако указатели, ссылки и т. Д. На исходный объект не обновляются автоматически, чтобы указывать на новый объект - вы должны использовать std::launder
. (Это предложение неверно для большинства классов; см. Комментарий Дэвиса Херринга).) Затем деструктор автоматически вызывается для исходного объекта, вызывая неопределенное поведение .
Ссылка: (выделено мной) [class.dtor]/ 16
Как только деструктор вызывается для объекта, объект больше не существует;Поведение не определено, если деструктор вызывается для объекта, время жизни которого истекло. [ Пример: Если деструктор для автоматического объекта вызывается явно, и блок впоследствии остается таким образом, чтообычно вызывает неявное уничтожение объекта, поведение не определено. - конец примера ]
[basic.life] / 1
[...] Время жизниобъект o типа T
заканчивается, когда:
, если T
является типом класса с нетривиальным деструктором ([class.dtor]), начинается вызов деструктора, или
освобождается память, которую занимает объект, или повторно используется объектом, который не вложен в o ([intro.object]).
(В зависимости от того, тривиален ли деструктор вашего класса, строка кода, заканчивающая время жизниобъекта отличается. Если деструктор нетривиален, явный вызов деструктора завершает время жизни объекта, в противном случае, размещение new повторно использует хранение текущего объекта, заканчивая его время жизни. В любом случае время жизни объектаобъект был завершен, когда оператор присваивания возвращается.)
Вы можете подумать, что это еще один "любой вменяемый"реализация будет делать правильные вещи "неопределенного поведения", но на самом деле многие оптимизации компилятора включают в себя значения кэширования, которые используют преимущества этой спецификации. Следовательно, ваш код может сломаться в любое время, когда код компилируется на другом уровне оптимизации, с помощью другого компилятора, с другой версией того же компилятора или когда у компилятора был просто ужасный день и он в плохом настроении.
Фактический "канонический" способ заключается в использовании идиомы копирования и обмена :
// copy constructor is implemented normally
C::C(const C& other)
: // ...
{
// ...
}
// move constructor = default construct + swap
C::C(C&& other) noexcept
: C{}
{
swap(*this, other);
}
// assignment operator = (copy +) swap
C& C::operator=(C other) noexcept // C is taken by value to handle both copy and move
{
swap(*this, other);
return *this;
}
Обратите внимание, что здесь вам необходимо предоставитьпользовательская функция swap
вместо использования std::swap
, как упомянуто Говардом Хиннантом:
friend void swap(C& lhs, C& rhs) noexcept
{
// swap the members
}
При правильном использовании копирование и замена не производят затрат, если соответствующие функцииправильно встроены (что должно быть довольно тривиально). Эта идиома очень часто используется, и среднестатистическому программисту на С ++ не должно быть проблем с ее пониманием. Вместо того, чтобы бояться, что это приведет к путанице, просто потратьте 2 минуты, чтобы изучить его, а затем использовать его.
На этот раз мы меняем значения объектов, и время жизни объекта не изменяется. Объект по-прежнему является исходным объектом, просто с другим значением, а не новым объектом. Подумайте об этом так: вы хотите, чтобы ребенок не запугивал других. Обмен значениями - это как гражданское воспитание, тогда как «уничтожение + конструирование» - это как убивание их , превращение их в временно мертвых и предоставление им совершенно нового мозга (возможно, спомощь магии). Последний метод может иметь некоторые нежелательные побочные эффекты, если не сказать больше.
Как и любая другая идиома, используйте ее, когда это уместно, - не просто используйте ее ради ее использования.