Зачем при копирующем присваивании RAII-обёртки сначала создавать копию нового ресурса?
Копию нового ресурса создают до изменения текущего состояния объекта, чтобы обеспечить сильную гарантию исключений: если копирование завершится исключением, исходный объект останется без изменений. Практический способ — принять копию по значению, а затем безошибочно обменять её состояние с текущим объектом.
RAII связывает владение ресурсом с временем жизни объекта, но само по себе не решает задачу безопасного присваивания уже существующему объекту. При копировании ресурса операция может выделять память или обращаться к внешней системе и потому завершиться исключением.
Идиома copy-and-swap появилась как универсальный способ разделить потенциально опасное копирование и короткую фазу изменения состояния объекта. Она помогает сохранять инвариант: объект либо содержит старый корректный ресурс, либо полностью принимает новый.
Предположим, RAII-обёртка уже владеет ресурсом и получает присваивание из другого объекта. Если сначала освободить старый ресурс, а затем попытаться скопировать новый, сбой копирования оставит объект пустым или частично изменённым.
Такое поведение нарушает сильную гарантию исключений и может быть особенно опасным, если присваивание выполняется внутри более сложной операции. Сам ресурс при этом не обязательно будет потерян, но состояние самой обёртки станет неожиданным для вызывающего кода.
Сначала создаётся независимая копия источника. Если конструктор копирования успешно завершился, объект временной копии уже владеет новым ресурсом. После этого текущий объект и временная копия обмениваются внутренними состояниями.
При присваивании из lvalue параметр other сначала копируется. Если выделение памяти или копирование значения выбросит исключение, тело оператора не начнётся, а *this сохранит прежний ресурс. При успешном обмене старый ресурс окажется во временном объекте и будет освобождён его деструктором при завершении функции.
Такая форма также корректно обрабатывает самоприсваивание: источник сначала копируется, поэтому исходный ресурс не уничтожается до завершения копирования. Для этого требуется корректный копирующий конструктор, а операция обмена должна быть noexcept и сохранять инварианты обоих объектов.
У подхода есть компромисс: присваивание из lvalue обычно создаёт временную копию даже тогда, когда можно было бы переиспользовать существующий ресурс. Если ресурс нельзя глубоко копировать, копирующее присваивание следует удалить или определить явную семантику клонирования. Сильная гарантия также не распространяется автоматически на внешние операции, которые сами необратимы или имеют ненадёжное освобождение.
Системный класс владеет буфером, а его копирование выделяет память. Вариант с немедленным освобождением старого буфера экономит временный объект, но при исключении во время выделения оставляет экземпляр без данных. Вариант с ручной проверкой самоприсваивания предотвращает только один частный случай и не решает проблему исключения после освобождения.
Можно реализовать присваивание через предварительное выделение нового буфера с последующей заменой указателя. Это эффективно и даёт сильную гарантию, но требует отдельного кода для копирования и освобождения. Выбранный в примере copy-and-swap проще проверять: копирование выполняется до изменения объекта, а освобождение старого ресурса делегируется временному RAII-объекту.
Результат — при ошибке объект сохраняет старое содержимое, при успехе ресурс освобождается ровно один раз. Цена решения — возможное дополнительное выделение памяти и временная копия.
swap как noexcept?Да, если на нём основана сильная гарантия присваивания. Если обмен может выбросить исключение после частичного изменения двух объектов, нельзя гарантировать сохранение исходного состояния. Для std::unique_ptr обмен указателями и соответствующими состояниями обычно не выбрасывает исключений, но пользовательская RAII-обёртка должна явно поддерживать тот же инвариант.
Нет; деструкторы владеющих RAII-типов должны быть noexcept. Деструктор временного объекта вызывается после обмена, и его задача — освободить старый ресурс. Исключение из деструктора может привести к std::terminate, особенно во время раскрутки стека, поэтому освобождение ресурса должно быть невыбрасывающим.
Проверка this != &source устраняет только самоприсваивание. Для разных объектов копирование нового ресурса всё ещё может завершиться исключением после уничтожения старого, и целевой объект останется изменённым. Предварительная копия или иной способ подготовить новое состояние до его публикации нужен именно для защиты от ошибки копирования.