Ir para o conteúdo

Última atualização:2026-10-12

Idiom copy-and-swap em C++

Por que precisamos do idiom de copiar e trocar?

Qualquer classe que gerencie recursos (wrappers, como smart pointers) precisa implementar o big three. Embora o objetivo e a implementação do construtor de cópia e do destrutor sejam simples,

o operador de atribuição por cópia é, sem dúvida, o mais sutil e o mais difícil.

O que fazer? Quais armadilhas evitar?

copy-swap é a solução: ele ajuda muito o operador de atribuição a cumprir duas coisas: evitar duplicação de código e fornecer uma forte garantia de exceção.

Como ele funciona?

Conceitualmente, ele funciona usando a funcionalidade do construtor de cópia para criar uma cópia local dos dados e, em seguida, usando a função de troca para obter os dados copiados, trocando os dados antigos pelos novos. Depois, a cópia temporária é destruída, levando consigo os dados antigos. O que nos resta é uma cópia dos dados novos.

Para usar copy-swap, precisamos de três coisas:

  • um construtor de cópia válido
  • um destrutor válido (ambos são a base de qualquer wrapper e, portanto, já deveriam estar completos de qualquer forma) e uma função de troca.

A função de troca é uma função que não lança exceções e que troca dois objetos ou membros de uma classe. Ficaríamos tentados a usar std::swap em vez de fornecer o nosso próprio método, mas isso não é possível. std::swap usa em sua implementação o construtor de cópia e o operador de atribuição por cópia, e acabaríamos tentando definir o operador de atribuição em termos de si mesmo!

(E não só isso: a chamada incondicional a swap usará o nosso operador swap personalizado, pulando as construções e destruições desnecessárias de classes que std::swap causaria.)

O exemplo concreto é o seguinte:

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

Agora, para que o s1=s2 acima funcione, é preciso implementar o operador de atribuição.

Método 1

Para evitar a autoatribuição, costuma-se escrever assim.

Ruim!

Não tem segurança de exceção, apenas segurança contra autoatribuição

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

Método 2

Se new lançar uma exceção, ptr_ permanece intacto! Também lida com a autoatribuição! Ainda não é bom o bastante!

Isso provoca inchaço de código e, com isso, outro problema: redundância de código

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

Método 3

copy and swap, muito bom!

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

Método 4

Mude para passar por valor; igual ao método 3!

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

C++11 move

Ao big three acrescentamos o move ctor e o move assignment, obtendo o big five.

Agora estendemos mais uma vez o código acima:

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

Na verdade, não precisamos escrever aquele código extra do move assignment: a técnica copy-and-swap e a técnica move-and-swap compartilham a mesma função. Quando o construtor de cópia é o do método 4 acima, no C++11, o compilador escolhe entre o construtor de cópia e o construtor de move conforme o argumento seja lvalue ou rvalue:

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

Assim, quando este e aquele escrito acima

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

existem ao mesmo tempo, ocorre error: ambiguous overload for 'operator='.

A chamada é a seguinte:

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);
  • No caso de s3 = s1, o construtor de cópia é chamado para inicializar other (porque s1 é um lvalue); o operador de atribuição troca os dados com o objeto recém-criado — uma cópia profunda. Esta é a definição do idiom copy and swap: construir uma cópia, trocar os dados com a cópia e deixar que a cópia seja destruída automaticamente ao sair de escopo.
  • No caso de s4 = std::move(s1), o construtor de move é chamado para inicializar rhs (porque std::move(s1) é um rvalue); portanto, aqui não há cópia profunda, apenas uma transferência de dados eficiente.

Por isso, ele também pode ser chamado de "operador de atribuição unificado", pois funde a "atribuição por cópia" e a "atribuição por move".

Tópicos relacionados

Comentários