Программирование

Идиома copy-and-swap в C++: что это, когда использовать

Разбираем идиому copy-and-swap в C++: суть, проблемы, которые она решает (exception safety, self-assignment), примеры реализации. Изменения в C++11 с move-семантикой. Когда применять для RAII-классов.

Что такое идиома copy-and-swap в C++? Когда её следует использовать, и какие проблемы она решает? Изменилось ли её реализация или использование в C++11?

Идиома copy-and-swap в C++ — это классический паттерн для реализации оператора присваивания, где копируется новый объект, а затем происходит быстрый обмен ресурсами с помощью swap. Она обеспечивает strong exception safety, упрощает код и решает проблемы с управлением памятью в классах с динамическими ресурсами. В C++11 её реализация улучшилась благодаря move-семантике, но базовый принцип остался: copy-and-swap всё так же актуален для exception-safe присваивания.


Содержание


Что такое идиома copy-and-swap

Представьте: у вас класс с указателем на кучу, и вы пишете оператор присваивания. Без идиомы код разрастается — сначала проверка self-assignment, потом delete старых ресурсов, new для новых, обработка исключений. А copy-and-swap? Всё проще: создайте копию справа, обменяйтесь с собой через swap, и пусть деструктор старого объекта сам почистит мусор.

Почему это работает? Swap обменивает внутренние состояния мгновенно, без выделения памяти. Если копирование кинет исключение — ваш объект остаётся нетронутым. Классика Rule of Three: destructor, copy ctor, copy assign. А swap — её сердце.

Вот базовый шаблон:

cpp
class MyClass {
private:
 std::unique_ptr<int> ptr; // или raw pointer в старом стиле

public:
 MyClass& operator=(MyClass other) { // по значению! Копирование в параметре.
 swap(other);
 return *this;
 }

 void swap(MyClass& other) noexcept {
 ptr.swap(other.ptr);
 }
};

Заметьте: параметр по значению вызывает copy ctor автоматически. Гениально, правда?


Проблемы, которые решает copy-and-swap

Copy-and-swap бьёт в точку по трём болячкам C++.

Во-первых, exception safety. Обычный assign может частично провалиться: память выделили, но исключение при копировании данных — и объект в полумёртвом состоянии. С идиомой? Копирование в other — если бум, *this цел. Обмен — noexcept.

Во-вторых, self-assignment. if (this != &other)? Забудьте. Если присваиваем себе — копируем копию себя, swap меняет ничто. Безопасно и чисто.

В-третьих, code duplication. Copy ctor и assign похожи? Идиома их сливает: один swap — и оба работают идеально.

А ещё она заставляет писать хороший swap — noexcept, member или free-function. По данным cppreference.com, это стандарт для RAII-классов. Без неё рискуете leaks или UB.

Но подождите, а если класс тривиальный? Не трогайте — compiler сам сгенерит.


Когда стоит использовать эту идиому

Не везде. Copy-and-swap сияет в классах с “big three”: динамическая память, файлы, сокеты. Vector, string — все std:: контейнеры на ней построены.

Используйте, когда:

  • Класс владеет ресурсами (pointers, handles).
  • Нужна strong guarantee: присваивание либо полностью succeeds, либо no change.
  • Хотите минимум boilerplate.

Избегайте, если:

  • Move-семантика доминирует (но даже тогда — hybrid).
  • Класс stateless или POD.
  • Performance critical без исключений (copy ctor может быть дорогим).

Спросите себя: “А если OOM во время присваивания?” Если ответ “всё сломается” — берите copy-and-swap. В многопотоке? Делайте swap thread-safe, если нужно.

По опыту, в legacy-коде это спасает от багов чаще, чем debuggers.


Реализация copy-and-swap: классика и примеры

Классика до C++11 — raw pointers. Но лучше smart pointers.

Полный пример с std::string внутри:

cpp
class Buffer {
 std::string data_;
public:
 Buffer(const std::string& s) : data_(s) {}

 Buffer& operator=(Buffer other) {
 swap(other);
 return *this;
 }

 friend void swap(Buffer& a, Buffer& b) noexcept {
 using std::swap;
 swap(a.data_, b.data_);
 }
};

Free-function swap — best practice, ADL-friendly.

Тестируем self-assign:

cpp
Buffer b("hello");
b = b; // Копирует "hello" в temp, swap — noop. Идеально.

И исключение:

cpp
Buffer x("x"); // data_ требует много памяти? Boom в copy ctor — x нетронут.

В production добавьте noexcept к swap. Compiler использует для move в containers.


Изменения в C++11 и новее

C++11 не сломал copy-and-swap, а усилил. Move-семантика + perfect forwarding сделали её гибридной.

Раньше: operator=(T other) — всегда copy. Теперь:

cpp
MyClass& operator=(MyClass other) noexcept {
 swap(other);
 return *this;
}

Other может быть rvalue — move ctor сработает! Автоматически.

Но для universal assign:

cpp
template <class T>
MyClass& operator=(T&& rhs) {
 MyClass temp(std::forward<T>(rhs));
 swap(temp);
 return *this;
}

Perfect! Lvalue — copy, rvalue — move. Herb Sutter рекомендует именно так для C++11+.

Ещё noexcept swap влияет на std::move_if_noexcept в containers.

В C++17/20? Идиома жива, но с unique_ptr/growable vectors — ещё проще. Проблемы copy-and-swap (дорогое копирование) решаются moves.

Итог: в C++11 реализация эволюционировала, но суть — та же. Используйте с moves для perf.


Источники

  1. cppreference.com: Copy assignment operator
  2. cppreference.com: Rule of three/five/zero
  3. Herb Sutter’s GotW #104: The Forwarding Problem
  4. Scott Meyers: Effective C++ Item 45 (книга, но онлайн-фрагменты доступны)

Заключение

Идиома copy-and-swap в C++ — timeless инструмент для безопасного присваивания, решающий exception safety и self-assign проблемы одним махом. Используйте в RAII-классах, особенно с ресурсами, — код короче, баги реже. В C++11+ она расцвела с moves, став ещё эффективнее. Не игнорируйте: один swap спасёт часы дебаггинга. Попробуйте в своём проекте — разница ощутима.

Авторы
Проверено модерацией
Модерация
Идиома copy-and-swap в C++: что это, когда использовать