Идиома 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
- Проблемы, которые решает copy-and-swap
- Когда стоит использовать эту идиому
- Реализация copy-and-swap: классика и примеры
- Изменения в C++11 и новее
- Источники
- Заключение
Что такое идиома copy-and-swap
Представьте: у вас класс с указателем на кучу, и вы пишете оператор присваивания. Без идиомы код разрастается — сначала проверка self-assignment, потом delete старых ресурсов, new для новых, обработка исключений. А copy-and-swap? Всё проще: создайте копию справа, обменяйтесь с собой через swap, и пусть деструктор старого объекта сам почистит мусор.
Почему это работает? Swap обменивает внутренние состояния мгновенно, без выделения памяти. Если копирование кинет исключение — ваш объект остаётся нетронутым. Классика Rule of Three: destructor, copy ctor, copy assign. А swap — её сердце.
Вот базовый шаблон:
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 внутри:
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:
Buffer b("hello");
b = b; // Копирует "hello" в temp, swap — noop. Идеально.
И исключение:
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. Теперь:
MyClass& operator=(MyClass other) noexcept {
swap(other);
return *this;
}
Other может быть rvalue — move ctor сработает! Автоматически.
Но для universal assign:
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.
Источники
- cppreference.com: Copy assignment operator
- cppreference.com: Rule of three/five/zero
- Herb Sutter’s GotW #104: The Forwarding Problem
- Scott Meyers: Effective C++ Item 45 (книга, но онлайн-фрагменты доступны)
Заключение
Идиома copy-and-swap в C++ — timeless инструмент для безопасного присваивания, решающий exception safety и self-assign проблемы одним махом. Используйте в RAII-классах, особенно с ресурсами, — код короче, баги реже. В C++11+ она расцвела с moves, став ещё эффективнее. Не игнорируйте: один swap спасёт часы дебаггинга. Попробуйте в своём проекте — разница ощутима.