Skip to content

Last updated:2026-10-12

C++ copy-and-swap Idiom

Why do we need the copy-and-swap idiom?

Any class that manages a resource (a wrapper, such as a smart pointer) needs to implement the big three. Although the goals and implementation of the copy constructor and the destructor are straightforward.

The copy assignment operator, however, is unquestionably the most subtle and the most difficult.

What should we do? What pitfalls should we avoid?

copy-and-swap is the solution; it helps the assignment operator accomplish two things very well: avoiding code duplication and providing a strong exception guarantee.

How does it work?

Conceptually, it works by using the functionality of the copy constructor to create a local copy of the data, and then using the swap function to take over the copied data, swapping the old data with the new. The temporary copy is then destroyed, carrying the old data away with it. What we are left with is a copy of the new data.

To use copy-and-swap, we need three things:

  • a working copy constructor
  • a working destructor (both are the foundation of any wrapper anyway, so they should be complete regardless) and a swap function.

The swap function is a non-throwing function that swaps two objects, or members, of a class. We may be tempted to use std::swap instead of providing our own, but that is impossible. std::swap uses the copy-constructor and copy-assignment operator in its implementation, and we would end up trying to define the assignment operator in terms of itself!

(Not only that, an unqualified call to swap will use our custom swap operator, skipping the unnecessary class construction and destruction that going through std::swap would cause.)

A concrete example follows:

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;
}

Now, for the s1=s2 above to do its job, the assignment operator must be implemented.

Method 1

To avoid self-assignment, the following is usually written.

Not good!

It is not exception-safe; it only provides self-assignment safety.

smart_ptr &operator=(const smart_ptr &rhs) {
    if (*this != rhs) {
        delete ptr_;
        ptr_ = new T(rhs.ptr_);  // 当new 发生异常,此时ptr_指向的而是一块被删除区域,而不是被赋值对象的区域
        return *this;
    }
    return *this;
}

Method 2

If new throws, ptr_ keeps its original state! It also handles self-assignment! Still not good enough!

This leads to code bloat, which in turn leads to another problem: code redundancy

// 方法2:如果new出现异常,ptr_会保持原装!  也可以处理自我赋值! 还是不够好!
smart_ptr &operator=(const smart_ptr &rhs) {
    T *origin = ptr_;
    ptr_ = new T(rhs.ptr_);
    delete origin;
    return *this;
}

Method 3

copy and swap — very good!

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

Method 4

Switch to pass-by-value — same as Method 3!

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

C++11 move

Adding the move ctor and move assignment on top of the big three gives us the big five.

Now extend the code above again:

// 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;
}

In fact, we do not need to write that extra move assignment at all: the copy-and-swap technique and the move-and-swap technique share the same function. When the copy constructor is written as Method 4 above, for C++11, the compiler chooses between the copy constructor and the move constructor depending on whether the argument is an lvalue or an rvalue:

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

So when this coexists with the one written above,

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

you get error: ambiguous overload for 'operator='.

The call site looks like this:

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);
  • If it is s3 = s1, the copy constructor is called to initialize other (because s1 is an lvalue); the assignment operator then swaps data with the newly created object — a deep copy. This is the definition of the copy-and-swap idiom: construct a copy, swap data with the copy, and let the copy be destroyed automatically at the end of the scope.
  • If it is s4 = std::move(s1), the move constructor is called to initialize rhs (because std::move(s1) is an rvalue), so there is no deep copy here — only an efficient transfer of data.

It can therefore also be called a "unified assignment operator", because it merges "copy assignment" and "move assignment".

Comments