Перейти к содержанию

Последнее обновление: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 — отлично!

smart_ptr &operator=(smart_ptr &rhs) noexcept {
    smart_ptr tmp(rhs);
    swap(tmp);
    return *this;
}

Способ 4

Изменим на передачу по значению — то же, что способ 3!

smart_ptr &operator=(smart_ptr rhs) noexcept {
    swap(rhs);
    return *this;
}

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:

smart_ptr &operator=(smart_ptr rhs) noexcept {
    swap(rhs);
    return *this;
}

Поэтому, когда это сосуществует с написанным выше

smart_ptr &operator=(smart_ptr &&rhs) noexcept{}

возникает 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), поэтому глубокого копирования здесь нет — только эффективная передача данных.

Поэтому её также можно называть «универсальным оператором присваивания», поскольку она объединяет «копирующее присваивание» и «перемещающее присваивание».

Похожие темы

Комментарии