Какие инварианты должен обеспечить перемещающий конструктор собственной RAII-обёртки, чтобы после перемещения ресурс освобождался ровно один раз?
Перемещающий конструктор должен передать дескриптор ресурса новому объекту и перевести исходный объект в состояние, в котором он больше не владеет ресурсом. После этого деструктор исходного объекта обязан быть безопасным и не освобождать уже переданный ресурс.
Ключевой инвариант таков: в каждый момент времени ресурсом владеет ровно один объект-обёртка либо ресурс уже освобождён. Копирование такой обёртки обычно запрещают, если оно не реализует осмысленное глубокое копирование.
До появления перемещающей семантики передача владеющих объектов часто требовала ручной передачи указателей, специальных функций освобождения и соглашений о том, кто отвечает за ресурс. Это повышало риск двойного освобождения, утечек и использования уже переданного ресурса.
RAII объединил ресурс с временем жизни объекта, а перемещающая семантика C++11 позволила передавать владение без копирования самого ресурса. Такой подход используется в std::unique_ptr, потоках, файловых дескрипторах, сокетах и других ресурсных обёртках.
Пусть RAII-обёртка хранит файловый дескриптор или другой внешний ресурс. При перемещении новый объект должен получить этот дескриптор, а старый не должен продолжать считать себя владельцем.
Если исходный объект не обнулить или не перевести в специальное неактивное состояние, оба деструктора попытаются освободить один ресурс. Это приводит к двойному освобождению, закрытию уже повторно использованного дескриптора или неопределённому поведению.
Обратная ошибка тоже опасна: если новый объект не получил владение, ресурс может остаться без владельца и утечь. Поэтому перемещение должно атомарно с точки зрения инвариантов обёртки: новый владелец получает ресурс, старый становится не владеющим.
Перемещающий конструктор обычно выполняет три действия:
Пример минимальной обёртки:
Значение -1 здесь является состоянием «ресурсом не владею». После перемещения деструктор исходного объекта ничего не освобождает, а деструктор нового объекта освобождает дескриптор ровно один раз.
Перемещённый объект должен оставаться корректным объектом: его можно уничтожить, присвоить ему новое значение или использовать операции, явно разрешённые интерфейсом. Стандарт обычно допускает для перемещённого объекта состояние, зависящее от реализации, но для RAII-обёртки практически важно обеспечить проверяемое пустое состояние.
Перемещающий конструктор стоит объявлять noexcept, если перенос действительно не может выбросить исключение. Это позволяет стандартным контейнерам безопаснее перемещать такие объекты и соответствует природе операции: передача дескриптора не должна завершаться исключением.
Если ресурс сопровождается пользовательским освобождателем, нужно учитывать и его состояние. Нельзя просто перенести дескриптор, оставив в новом объекте освобождатель, несовместимый с этим ресурсом; иначе деструктор может вызвать неправильную операцию освобождения.
В сервере файловые дескрипторы оборачиваются в класс, чтобы закрытие происходило автоматически при выходе из функции. Функция создаёт дескриптор и возвращает обёртку вызывающему коду.
Рассматривались следующие варианты:
Выбран последний вариант. При возврате из функции дескриптор перемещается в результат, локальная обёртка становится пустой, а вызывающий объект получает ответственность за закрытие. В результате ранний выход и исключения не требуют отдельных ветвей очистки, а двойное закрытие предотвращается инвариантом пустого перемещённого состояния.
Нет. Простое копирование значения не передаёт владение: исходный объект всё ещё содержит тот же дескриптор и при уничтожении попытается его освободить. После копирования дескриптора исходный объект необходимо перевести в состояние без владения.
Сначала целевой объект должен корректно освободить свой текущий ресурс, затем получить ресурс из исходного объекта, после чего исходный объект должен стать пустым. Иначе старый ресурс целевого объекта утечёт, а при сохранении владения в источнике возникнет двойное освобождение.
Оператор также должен учитывать самоприсваивание перемещением. Практическая реализация должна не допустить, чтобы объект сначала освободил свой ресурс, а затем попытался забрать его уже из самого себя.
Перемещение меняет состояние объекта, но не завершает его время жизни. Его деструктор всё равно будет вызван, поэтому состояние после перемещения обязано быть безопасным для уничтожения.
Обычно объект переводят в пустое состояние, допускающее повторное присваивание или вызов операций вроде проверки наличия ресурса. Обращение к самому ресурсу после перемещения допустимо только в пределах явно гарантированного интерфейсом поведения; наличие корректного деструктора не означает сохранения прежнего значения.