Последнее обновление:2026-10-12
Идиома copy-and-swap в C++¶
Зачем нам нужна идиома копирования и обмена?
Любому классу, управляющему ресурсами (обёртке, например умному указателю), необходимо реализовать big three. Хотя цели и реализация копирующего конструктора и деструктора просты.
Но оператор копирующего присваивания, бесспорно, самый тонкий и трудный.
Как это сделать? Каких ловушек следует избегать?
copy-swap — это решение, которое отлично помогает оператору присваивания добиться двух вещей: избежать дублирования кода и обеспечить сильную гарантию безопасности относительно исключений.
Как это работает?
Концептуально это работает так: с помощью копирующего конструктора создаётся локальная копия данных, затем с помощью операции обмена скопированные данные получают, а старые данные меняются местами с новыми. После этого временная копия уничтожается, унося с собой старые данные. У нас остаётся копия новых данных.
Чтобы использовать copy-swap, нам нужны три вещи:
- работающий копирующий конструктор
- работающий деструктор (оба — основа любой обёртки, поэтому в любом случае должны быть полными), а также функция обмена.
Функция обмена — это функция, не выбрасывающая исключений, которая обменивает местами два объекта или члена класса. У нас может возникнуть соблазн использовать std::swap вместо собственной реализации, но это невозможно. std::swap в своей реализации использует copy-constructor и оператор copy-assignment, и мы в итоге попытаемся определить оператор присваивания через самого себя!
(Мало того, безусловный вызов swap будет использовать наш собственный оператор swap, пропуская ненужные конструкции и разрушения класса, к которым привёл бы std::swap.)
Конкретный пример таков:
namespace A {
template<typename T>
class smart_ptr {
public:
smart_ptr() noexcept : ptr_(new T()) {
}
smart_ptr(const T &ptr) noexcept : ptr_(new T(ptr)) {
}
smart_ptr(smart_ptr &rhs) noexcept {
ptr_ = rhs.release(); // 释放所有权,此时rhs的ptr_指针为nullptr
}
void swap(smart_ptr &rhs) noexcept { // noexcept == throw() 保证不抛出异常
using std::swap;
swap(ptr_, rhs.ptr_);
}
T *release() noexcept {
T *ptr = ptr_;
ptr_ = nullptr;
return ptr;
}
T *get() const noexcept {
return ptr_;
}
private:
T *ptr_;
};
// 提供一个非成员swap函数for ADL(Argument Dependent Lookup)
template<typename T>
void swap(A::smart_ptr<T> &lhs, A::smart_ptr<T> &rhs) noexcept {
lhs.swap(rhs);
}
}
// 注释开启,会引发ADL冲突
//namespace std {
// // 提供一个非成员swap函数for ADL(Argument Dependent Lookup)
// template<typename T>
// void swap(A::smart_ptr<T> &lhs, A::smart_ptr<T> &rhs) noexcept {
// lhs.swap(rhs);
// }
//
//}
int main() {
using std::swap;
A::smart_ptr<std::string> s1("hello"), s2("world");
// 交换前
std::cout << *s1.get() << " " << *s2.get() << std::endl;
swap(s1, s2); // 这里swap 能够通过Koenig搜索或者说ADL根据s1与s2的命名空间来查找swap函数
// 交换后
std::cout << *s1.get() << " " << *s2.get() << std::endl;
s1=s2;
}
Теперь, чтобы s1=s2 из приведённого выше кода заработал, необходимо реализовать оператор присваивания.
Способ 1
Чтобы избежать самоприсваивания, обычно используется следующая запись.
Плохо!
Нет безопасности относительно исключений, есть лишь безопасность при самоприсваивании
smart_ptr &operator=(const smart_ptr &rhs) {
if (*this != rhs) {
delete ptr_;
ptr_ = new T(rhs.ptr_); // 当new 发生异常,此时ptr_指向的而是一块被删除区域,而不是被赋值对象的区域
return *this;
}
return *this;
}
Способ 2
Если new выбросит исключение, ptr_ останется в исходном состоянии! Самоприсваивание тоже обрабатывается! Но всё равно недостаточно хорошо!
Это приводит к разрастанию кода и, как следствие, к другой проблеме: избыточности кода
// 方法2:如果new出现异常,ptr_会保持原装! 也可以处理自我赋值! 还是不够好!
smart_ptr &operator=(const smart_ptr &rhs) {
T *origin = ptr_;
ptr_ = new T(rhs.ptr_);
delete origin;
return *this;
}
Способ 3
copy and swap — отлично!
Способ 4
Изменим на передачу по значению — то же, что способ 3!
C++11 move
Если к big three добавить move-конструктор и move-присваивание, получится big five.
Теперь ещё раз расширим приведённый выше код:
// move ctor
smart_ptr(smart_ptr &&rhs) noexcept {
std::cout << "move ctor" << std::endl;
ptr_ = rhs.ptr_;
if (ptr_)
rhs.ptr_ = nullptr;
}
// move assignment
smart_ptr &operator=(smart_ptr &&rhs) noexcept {
std::cout << "move assignment" << std::endl;
smart_ptr tmp(rhs);
swap(rhs);
return *this;
}
На самом деле нам не нужно писать move assignment отдельно — приёмы copy-and-swap и move-and-swap используют одну и ту же функцию. Когда копирующее конструирование выполнено способом 4 выше, в C++11 компилятор выбирает между копирующим и перемещающим конструкторами в зависимости от того, является ли аргумент lvalue или rvalue:
Поэтому, когда это сосуществует с написанным выше
возникает error: ambiguous overload for 'operator='.
Место вызова таково:
A::smart_ptr<std::string> s1("hello"), s2("world");
A::smart_ptr<std::string> s3 = s1;
A::smart_ptr<std::string> s4 = std::move(s1);
- Если это s3 = s1, для инициализации other будет вызван копирующий конструктор (поскольку s1 — lvalue), и оператор присваивания обменяется данными с вновь созданным объектом — глубокое копирование. Это и есть определение идиомы copy and swap: построить копию, обменяться с ней данными и позволить копии автоматически уничтожиться при выходе из области видимости.
- Если это s4 = std::move(s1), для инициализации rhs будет вызван перемещающий конструктор (поскольку std::move(s1) — rvalue), поэтому глубокого копирования здесь нет — только эффективная передача данных.
Поэтому её также можно называть «универсальным оператором присваивания», поскольку она объединяет «копирующее присваивание» и «перемещающее присваивание».
Похожие темы¶
- Отказ от конструктора по умолчанию в C++ | C++那些事
- Идиома списков инициализации класса в C++ | C++那些事
- C++: классы-шаблоны вместо шаблонов функций | C++那些事
- Внедрение зависимостей в C++ | C++那些事
- Сильнотипизированные перечисления enum class в C++ | C++那些事
- Header-Only в C++ против разделения реализации | C++那些事
- Идиома ленивых вычислений в C++ | C++那些事
- Идиома таблиц поиска в C++ | C++那些事
- Указатели на члены и информация о типах в C++ | C++那些事
- Идиома Mixin в C++ | C++那些事