Как пользовательский деструктор влияет на неявное перемещение объекта при присваивании ему временного значения?
Пользовательский деструктор подавляет неявное объявление перемещающего конструктора и перемещающего оператора присваивания. Поэтому присваивание временному объекту обычно выполняется через неявно сгенерированный копирующий оператор присваивания, если он доступен.
Это может быть корректно для простых типов, но опасно для класса, владеющего ресурсом: копирование указателя вместо передачи владения способно привести к двойному освобождению ресурса.
В C++03 перемещения объектов не существовало, поэтому классы, управляющие ресурсами, обычно требовали ручного объявления деструктора, копирующего конструктора и копирующего присваивания. Этот набор правил известен как правило трёх.
В C++11 появились перемещающие специальные функции-члены. Они позволяют передавать владение ресурсами вместо их копирования, но для обратной совместимости компилятор не стал автоматически добавлять перемещение в класс, явно заявивший о ручном управлении временем жизни объекта.
Рассмотрим класс с пользовательским деструктором:
В выражении присваивания справа находится временный объект, то есть rvalue. Интуитивно можно ожидать вызов перемещающего присваивания, но оно не будет неявно объявлено только потому, что у класса есть пользовательский деструктор.
Если копирующая операция выполняет поверхностное копирование ресурса, временный объект и first могут начать владеть одним и тем же ресурсом. Последствия включают двойное освобождение, обращение к уже освобождённой памяти и нарушение инвариантов класса.
Правила генерации специальных функций учитывают наличие пользовательски объявленных операций. Пользовательский деструктор, включая явно defaulted-деструктор, препятствует неявному объявлению перемещающего конструктора и перемещающего оператора присваивания.
При этом копирующий оператор присваивания может быть неявно объявлен. Его параметр обычно имеет вид ссылки на const, поэтому он способен принимать и временный объект. В результате выражение с присваиванием временного значения выбирает копирование, если перемещающей операции нет.
Важно различать следующие случаи:
Практическое правило — правило пяти: если классу действительно требуется пользовательский деструктор, копирующий конструктор, копирующий оператор присваивания, перемещающий конструктор или перемещающий оператор присваивания, нужно осознанно проверить все пять специальных функций. Часто более безопасный вариант — хранить ресурс в стандартном RAII-типе, например std::unique_ptr, и позволить компилятору корректно вывести нужную семантику.
Минимальный вариант явного восстановления перемещения выглядит так:
Здесь перемещающие операции объявлены явно, поэтому временное значение может выбирать перемещающее присваивание. Однако = default гарантирует полезное перемещение только тогда, когда перемещение всех членов действительно имеет нужную семантику.
В библиотеке обработки изображений класс Image хранил указатель на большой буфер и освобождал его в пользовательском деструкторе. После добавления операции, возвращающей временный Image, присваивание результата существующему объекту стало выполнять копирующую операцию, хотя разработчики ожидали передачу владения.
Рассматривались два варианта. Первый — вручную реализовать перемещающий конструктор и перемещающий оператор присваивания; это даёт контроль над обнулением исходного указателя, но увеличивает объём кода и риск ошибок. Второй — заменить сырой указатель на std::unique_ptr и сделать класс правилом нуля; это упрощает владение, но требует проверить совместимость интерфейса и стоимость выбранного выделения памяти.
Был выбран второй вариант. Владение стало однозначным, копирование явно запретили или реализовали через глубокую копию, а перемещение стало автоматически безопасным. В результате исчез риск двойного освобождения, а передача временных объектов не требовала ручного дублирования логики управления ресурсом.
Нет. Пользовательский деструктор сам по себе не запрещает неявное копирование. Копирующий конструктор и копирующий оператор присваивания всё ещё могут быть неявно объявлены, если другие правила не делают их удалёнными.
Именно поэтому наличие деструктора может создать опасную ситуацию: копирование формально доступно, но его автоматически сгенерированная реализация может быть неверной для владения сырой памятью, файловым дескриптором или другим уникальным ресурсом.
Нет. Если класс явно объявляет доступный перемещающий оператор присваивания, он обычно будет выбран для неконстантного временного объекта. Если перемещающая операция отсутствует или удалена, а копирующая операция доступна, временный объект может быть передан ей через const-ссылку.
Кроме того, выбор зависит от конкретных свойств членов и перегруженных операций. Нельзя делать вывод только по наличию временного объекта: нужно проверить, какие специальные функции реально объявлены и не удалены.
Потому что класс может создавать временные объекты не только присваиванием. Для полной семантики перемещения обычно нужны и перемещающий конструктор, и перемещающий оператор присваивания.
Например, инициализация нового объекта из временного значения использует конструктор, а замена уже существующего объекта — оператор присваивания. Если реализовать только одну операцию, другая ситуация может по-прежнему выполнять копирование или стать ошибкой компиляции. Также перемещение должно оставлять исходный объект в валидном, обычно пустом состоянии.