Saltar a contenido

Última actualización:2026-10-12

C++ Idioma copy-and-swap

¿Por qué necesitamos el idioma de copiar e intercambiar?

Toda clase que gestiona recursos (envoltorios, como los punteros inteligentes) necesita implementar el big three. Aunque el objetivo y la implementación del constructor de copia y del destructor son sencillos.

Sin embargo, el operador de asignación por copia es sin duda el más sutil y el más difícil.

¿Cómo hacerlo? ¿Qué trampas hay que evitar?

copy-swap es la solución, y ayuda muy bien al operador de asignación a lograr dos cosas: evitar la duplicación de código y ofrecer una garantía de excepción fuerte.

¿Cómo funciona?

Conceptualmente, funciona usando la funcionalidad del constructor de copia para crear una copia local de los datos y usando después la función de intercambio para tomar los datos copiados, intercambiando los datos antiguos por los nuevos. Después, la copia temporal se destruye, llevándose consigo los datos antiguos. Lo que nos queda es una copia de los datos nuevos.

Para usar copy-swap necesitamos tres cosas:

  • Un constructor de copia correcto
  • Un destructor correcto (ambos son la base de cualquier envoltorio, así que deberían estar completos de todos modos) y una función de intercambio.

La función de intercambio es una función que no lanza excepciones e intercambia dos objetos o miembros de una clase. Puede que nos tiente usar std::swap en lugar de proporcionar nuestro propio método, pero eso no es posible: std::swap usa en su implementación el constructor de copia y el operador de asignación por copia, ¡y acabaríamos intentando definir el operador de asignación en términos de sí mismo!

(Y no solo eso: la llamada incondicional a swap usará nuestro operador swap personalizado, evitando las construcciones y destrucciones de clases innecesarias que provocaría usar std::swap.)

Un ejemplo concreto es el siguiente:

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

Ahora, para que el s1=s2 anterior funcione, hay que implementar el operador de asignación.

Método 1

Para evitar la autoasignación, normalmente se escribe de la siguiente manera.

¡No es bueno!

No ofrece seguridad ante excepciones, solo seguridad ante la autoasignación

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

Si new lanza una excepción, ¡ptr_ se mantiene intacto! También gestiona la autoasignación. ¡Pero aún no es suficiente!

Esto provoca hinchazón del código, lo que conduce a otro problema: redundancia 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 es genial!

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

Método 4

Se cambia a paso por valor, ¡igual que el método 3!

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

C++11 move

Sobre el big three añadimos el move ctor y la move assignment, y obtenemos el big five.

Extendamos de nuevo el código anterior:

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

En realidad, no necesitamos escribir más código para la move assignment: la técnica copy-and-swap y la técnica move-and-swap comparten la misma función. Cuando la construcción por copia es la del método 4 anterior, en C++ 11 el compilador elige entre el constructor de copia y el constructor de movimiento según si el argumento es un lvalue o un rvalue:

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

Por eso, cuando este coexiste con el escrito así antes

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

aparece un error: ambiguous overload for 'operator='.

La llamada sería así:

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);
  • Si es s3 = s1, se invoca al constructor de copia para inicializar other (porque s1 es un lvalue); el operador de asignación intercambia los datos con el objeto recién creado, con una copia profunda. Esta es la definición del idioma copy and swap: construir una copia, intercambiar los datos con la copia y dejar que la copia se destruya automáticamente al salir del ámbito.
  • Si es s4 = std::move(s1), se invoca al constructor de movimiento para inicializar rhs (porque std::move(s1) es un rvalue), así que aquí no hay copia profunda, solo una transferencia de datos eficiente.

Por eso también se le puede llamar «operador de asignación unificado», porque fusiona la «asignación por copia» y la «asignación por movimiento».

Temas relacionados

Comentarios