Что такое идиома copy-and-swap в C++?

Ответ

Идиома copy-and-swap — это надежный и эффективный паттерн для реализации оператора присваивания копированием (operator=), обеспечивающий строгую гарантию безопасности исключений и автоматическую поддержку move-семантики.

Суть идиомы: Вместо того чтобы копировать данные внутри operator=, мы создаем временную копию аргумента (используя конструктор копирования или перемещения), а затем обмениваем (swap) её внутреннее состояние с состоянием текущего объекта.

Полная реализация для класса, управляющего ресурсом:

#include <algorithm> // для std::swap
#include <utility>   // для std::exchange

class Buffer {
private:
    int* data_ = nullptr;
    size_t size_ = 0;

    // Дружественная функция обмена
    friend void swap(Buffer& a, Buffer& b) noexcept {
        using std::swap;
        swap(a.data_, b.data_);
        swap(a.size_, b.size_);
    }

public:
    // Конструкторы, деструктор...
    ~Buffer() { delete[] data_; }

    // Ключевой метод обмена
    void swap(Buffer& other) noexcept {
        ::swap(*this, other); // Вызов дружественной функции
    }

    // Оператор присваивания по ИДИОМЕ COPY-AND-SWAP
    Buffer& operator=(Buffer other) noexcept { // ПАРАМЕТР ПЕРЕДАЕТСЯ ПО ЗНАЧЕНИЮ!
        other.swap(*this); // Обменяли состояние с временной копией
        return *this;
        // Деструктор 'other' (который теперь содержит старые данные *this)
        // автоматически освобождает старый ресурс.
    }

    // Конструктор копирования (для полноты примера)
    Buffer(const Buffer& other) : size_(other.size_) {
        data_ = new int[size_];
        std::copy(other.data_, other.data_ + size_, data_);
    }
    // Конструктор перемещения генерируется автоматически или может быть =default
};

Преимущества:

  1. Безопасность исключений: Вся работа, которая может бросить исключение (выделение памяти, копирование), происходит при создании параметра other до изменения состояния текущего объекта. Если это не удастся, *this останется неизменным.
  2. Автоматическая поддержка перемещения: Поскольку параметр передается по значению, компилятор сам вызовет конструктор перемещения для r-value аргументов, делая operator= универсальным.
  3. Устранение дублирования кода: Логика копирования содержится только в конструкторе копирования.

Ответ 18+ 🔞

Да ты посмотри, какая хитрая жопа эти программисты придумали! Сидят, блядь, думают, как бы так сделать, чтобы и безопасно, и красиво, и чтобы ничего не проебать. И родилась эта идиома — copy-and-swap, ёпта!

Суть-то проще пареной репы, если разобраться. Вместо того чтобы внутри оператора присваивания (operator=) городить огород с проверками, копированием и «а вдруг исключение вылетит», мы делаем проще, как дурак.

Мы берём и сразу делаем копию того, что нам передали. Ага, параметр в оператор принимается по значению. Это ключевой момент, запомни, как «Отче наш»!

Buffer& operator=(Buffer other) noexcept { // Смотри сюда! По значению!
    other.swap(*this);
    return *this;
}

Вот и вся магия. Компилятор, когда видит buffer1 = buffer2, сначала создаёт этот самый временный объект other. Если buffer2 — lvalue, вызовется конструктор копирования. Если rvalue (типа std::move(buffer2)), то вызовется конструктор перемещения. Удобно, блядь? Одна запись, а работает для обоих случаев!

А дальше — разводка лёгкая. У нас же есть метод swap, который честно и без исключений меняет местами все внутренности двух объектов. Меняем состояние свежесозданной копии other с состоянием текущего объекта *this.

И тут наступает момент истины. Временный объект other теперь хранит старые данные *this. Функция заканчивается, other уничтожается, и его деструктор автоматом чистит за нами старый ресурс. Красота, ядрёна вошь!

В чём, собственно, профит, чувак?

  1. Про безопасность исключений. Вся опасная работа — выделение памяти, копирование байтов — происходит до строки other.swap(*this). То есть пока мы создаём копию. Если там что-то пошло не так и вылетело исключение — наш текущий объект (*this) даже не тронут, стоит себе как ни в чём не бывало. Гарантия — железная.
  2. Про универсальность. Не нужно городить два оператора (operator=(const Buffer&) и operator=(Buffer&&)). Один, ёб твою мать, делает всё. Потому что перемещение или копирование определяется на этапе создания аргумента other.
  3. Про DRY (Don't Repeat Yourself). Логика копирования живёт в одном месте — в конструкторе копирования. В операторе присваивания её не дублируем. Меньше кода — меньше шансов накосячить.

Единственный нюанс, про который иногда спорят — это производительность. Мол, лишнее копирование/перемещение при передаче параметра. Но, во-первых, компиляторы часто это оптимизируют. А во-вторых, надёжность и ясность кода в 99% случаев важнее этой микрооптимизации. Не гонись за призраками, лучше спи спокойно.

В общем, идиома — огонь. Пользуйся, не ошибёшься.