Copy-and-Swap Idiom в C++: гайд 2025 с примерами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Что такое Copy-and-Swap Idiom и почему это must-have в 2025 году

Представьте, что вы пишете класс для работы с динамической памятью. Всё идёт хорошо, пока не доходит до перегрузки оператора присваивания. Вы пишете operator=, проверяете на самоприсваивание, руками освобождаете старые ресурсы, выделяете новые… И в 90% случаев допускаете баг, который приведёт к утечке памяти или двойному удалению. Знакомо?

В C++ существует элегантное решение — copy-and-swap idiom. Это не просто модный трюк, а фундаментальная техника, которая позволяет писать безопасный, исключение-устойчивый и лаконичный код. В 2025 году, когда C++23 уже активно используется, а стандарт C++26 на горизонте, эта идиома остаётся золотым стандартом для классов, управляющих ресурсами.

В этой статье мы разберём copy-and-swap idiom от и до: начиная с проблемы, которую он решает, заканчивая практическими примерами и лучшими практиками 2025 года. Готовьте компилятор — будет много кода!

Проблема: почему наивный operator= — это зло

Давайте начнём с классического примера класса, который управляет динамическим массивом. Без идиомы copy-and-swap код часто выглядит так:

class DumbArray {
public:
    DumbArray(size_t size) 
        : size_(size), data_(new int[size]) {}
    
    ~DumbArray() { delete[] data_; }
    
    // Наивный оператор присваивания
    DumbArray& operator=(const DumbArray& other) {
        if (this == &other) return *this; // проверка на самоприсваивание
        delete[] data_;                    // освобождаем старые ресурсы
        size_ = other.size_;
        data_ = new int[size_];           // выделяем новые
        std::copy(other.data_, other.data_ + size_, data_);
        return *this;
    }
    
private:
    size_t size_;
    int* data_;
};

На первый взгляд всё логично. Но есть три проблемы:

  • Отсутствие базовой гарантии исключений. Если new int[size_] выбросит std::bad_alloc, объект останется в некорректном состоянии: data_ будет указывать на удалённую память.
  • Дублирование логики. Копирование данных уже реализовано в конструкторе копирования. Почему мы пишем то же самое вручную?
  • Проверка на самоприсваивание. Она замедляет код и легко забывается. К тому же, в 95% случаев самоприсваивания не происходит, а проверка всё равно выполняется.

И это ещё не всё. Если добавить в класс другие ресурсы (например, файловые дескрипторы или мьютексы), код станет не просто громоздким — он станет опасным. Именно здесь на сцену выходит copy-and-swap idiom.

Суть идиомы: три шага к идеальному operator=

Copy-and-swap idiom базируется на простой идее: не пиши логику копирования в операторе присваивания — используй конструктор копирования и функцию swap. Алгоритм выглядит так:

  1. Copy: создаём временную копию объекта с помощью конструктора копирования.
  2. Swap: обмениваем содержимое текущего объекта с временной копией.
  3. Destroy: временный объект уничтожается, освобождая старые ресурсы.

В коде это выглядит так:

class SmartArray {
public:
    SmartArray(size_t size = 0) 
        : size_(size), data_(size ? new int[size] : nullptr) {}
    
    // Конструктор копирования
    SmartArray(const SmartArray& other) 
        : size_(other.size_), data_(other.size_ ? new int[other.size_] : nullptr) {
        std::copy(other.data_, other.data_ + size_, data_);
    }
    
    // Деструктор
    ~SmartArray() { delete[] data_; }
    
    // Дружественная функция swap
    friend void swap(SmartArray& first, SmartArray& second) noexcept {
        using std::swap;
        swap(first.size_, second.size_);
        swap(first.data_, second.data_);
    }
    
    // Оператор присваивания — всего 3 строки!
    SmartArray& operator=(SmartArray other) noexcept { // Принимаем по значению!
        swap(*this, other);
        return *this;
    }
    
private:
    size_t size_;
    int* data_;
};

Обратите внимание: operator= принимает аргумент по значению, а не по константной ссылке. Это ключевой момент. Когда вы пишете a = b, компилятор вызывает конструктор копирования для параметра other. Если же вы пишете a = std::move(b), компилятор использует конструктор перемещения (если он определён). Таким образом, одна функция работает и для копирующего, и для перемещающего присваивания!

Почему это работает: магия исключений и swap

Главное преимущество copy-and-swap — сильная гарантия исключений (strong exception safety). Если в процессе копирования произойдёт исключение (например, не хватит памяти), оригинальный объект останется нетронутым. Почему? Потому что:

  • Конструктор копирования либо создаёт полностью валидную копию, либо выбрасывает исключение, не изменяя исходный объект.
  • Функция swap помечена как noexcept — она никогда не выбрасывает исключений.
  • Деструктор временного объекта также noexcept по умолчанию.

Таким образом, если исключение произошло на этапе копирования, operator= даже не начнётся. Если же копирование прошло успешно, swap гарантированно выполнится без ошибок, и старые ресурсы будут корректно освобождены при разрушении временного объекта.

Лучшие практики 2025: современные рекомендации

1. Правило пяти и copy-and-swap

В современном C++ (C++11 и новее) действует правило пяти: если вы определяете один из следующих методов, определите все пять:

  • Деструктор
  • Конструктор копирования
  • Оператор присваивания копированием
  • Конструктор перемещения
  • Оператор присваивания перемещением

Copy-and-swap idiom упрощает эту задачу. Вам нужно определить деструктор, конструктор копирования, конструктор перемещения и функцию swap. А оба оператора присваивания (копирующий и перемещающий) реализуются через swap:

class ModernArray {
public:
    // ... конструкторы, деструктор ...
    
    friend void swap(ModernArray& first, ModernArray& second) noexcept { ... }
    
    // Один оператор на оба случая
    ModernArray& operator=(ModernArray other) noexcept {
        swap(*this, other);
        return *this;
    }
};

Но будьте внимательны: если в вашем классе есть ресурсы, которые не должны копироваться (например, уника

Блоги

Рекомендаций по книгам