Ir al contenido

Semantica de Copia: Profunda vs Superficial

Aprende la diferencia entre copias superficiales y profundas, como implementar operaciones de copia correctas para clases que poseen recursos, y el idioma de copiar-e-intercambiar.

Por Que Importa la Semantica de Copia

Cuando copias un objeto en C++, que es exactamente lo que se copia? Para tipos simples, la respuesta es sencilla — todos los miembros se copian bit a bit. Pero cuando un objeto posee un recurso a traves de un puntero, el constructor de copia por defecto (generado por el compilador) realiza una copia superficial: copia el valor del puntero pero no los datos a los que apunta. Esto significa que dos objetos ahora comparten el mismo recurso, causando errores de doble liberacion y corrupcion de datos.

Entender la semantica de copia es esencial porque las copias ocurren implicitamente en muchos lugares: al pasar por valor, retornar de funciones, insertar en contenedores y asignacion.

Problema de la Copia Superficial

El constructor de copia generado por el compilador copia cada miembro. Para punteros, esto copia la direccion — no los datos apuntados. Esto es una copia superficial y es peligrosa para clases que poseen recursos.

shallow_copy_bug.cpp
#include <iostream>
#include <cstring>

class BrokenString {
    char* data_;
    std::size_t len_;

public:
    BrokenString(const char* s)
        : len_{std::strlen(s)}
        , data_{new char[std::strlen(s) + 1]}
    {
        std::strcpy(data_, s);
    }

    ~BrokenString() {
        delete[] data_;  // frees the buffer
    }

    // No copy constructor defined!
    // Compiler generates shallow copy: copies the pointer

    void print() const {
        std::cout << data_ << '\n';
    }
};

int main() {
    BrokenString a{"Hello"};
    {
        BrokenString b = a;  // shallow copy: b.data_ == a.data_
        b.print();           // "Hello" — works for now
    }  // b destroyed: delete[] b.data_ (same pointer as a.data_!)

    // a.data_ is now a dangling pointer — UNDEFINED BEHAVIOR
    a.print();  // may crash, print garbage, or appear to work
    return 0;
}

Implementando Copia Profunda

Una copia profunda asigna nueva memoria y copia los datos reales. Esto asegura que cada objeto posea su propia copia independiente del recurso.

deep_copy.cpp
#include <iostream>
#include <cstring>
#include <algorithm>

class SafeString {
    char* data_;
    std::size_t len_;

public:
    explicit SafeString(const char* s)
        : len_{std::strlen(s)}
        , data_{new char[std::strlen(s) + 1]}
    {
        std::strcpy(data_, s);
    }

    // Deep copy constructor
    SafeString(const SafeString& other)
        : len_{other.len_}
        , data_{new char[other.len_ + 1]}
    {
        std::strcpy(data_, other.data_);
    }

    // Deep copy assignment with self-assignment check
    SafeString& operator=(const SafeString& other) {
        if (this == &other) return *this;  // self-assignment guard

        // Allocate new resource BEFORE releasing old one
        char* new_data = new char[other.len_ + 1];
        std::strcpy(new_data, other.data_);

        // Release old resource
        delete[] data_;

        // Adopt new resource
        data_ = new_data;
        len_ = other.len_;
        return *this;
    }

    ~SafeString() { delete[] data_; }

    void print() const {
        std::cout << data_ << " (len=" << len_ << ")\n";
    }
};

int main() {
    SafeString a{"Hello"};
    SafeString b = a;     // deep copy: b gets its own buffer
    SafeString c{"World"};
    c = a;                // deep copy assignment
    a.print();  // Hello (len=5)
    b.print();  // Hello (len=5)
    c.print();  // Hello (len=5)
    return 0;
}

El Idioma Copiar-e-Intercambiar

El idioma copiar-e-intercambiar proporciona una implementacion elegante y segura ante excepciones del operador de asignacion por copia. Reutiliza el constructor de copia y una funcion swap, proporcionando seguridad fuerte ante excepciones con minima duplicacion de codigo.

copy_and_swap.cpp
#include <iostream>
#include <cstring>
#include <utility>

class Buffer {
    char* data_;
    std::size_t size_;

public:
    explicit Buffer(std::size_t size)
        : size_{size}, data_{new char[size]{}} {}

    Buffer(const Buffer& other)
        : size_{other.size_}, data_{new char[other.size_]}
    {
        std::memcpy(data_, other.data_, size_);
    }

    ~Buffer() { delete[] data_; }

    // Copy-and-swap: take parameter by VALUE (invokes copy ctor)
    Buffer& operator=(Buffer other) {   // copy happens here
        swap(*this, other);             // swap resources
        return *this;
    }   // 'other' destroyed here, freeing our old resource

    // Non-throwing swap — essential for strong exception safety
    friend void swap(Buffer& a, Buffer& b) noexcept {
        using std::swap;
        swap(a.data_, b.data_);
        swap(a.size_, b.size_);
    }

    char& operator[](std::size_t i) { return data_[i]; }
    std::size_t size() const { return size_; }
};

int main() {
    Buffer a{5};
    a[0] = 'H'; a[1] = 'i';

    Buffer b{10};
    b = a;  // copy-and-swap: exception-safe, self-assignment-safe

    std::cout << b[0] << b[1] << " (size " << b.size() << ")\n";
    return 0;
}

Deshabilitando la Copia

Algunos tipos no deberian ser copiables — por ejemplo, tipos que representan recursos unicos como handles de archivo, conexiones de red o hilos. Usa =delete para comunicar y forzar esto claramente:

class UniqueConnection {
public:
    UniqueConnection(const UniqueConnection&) = delete;
    UniqueConnection& operator=(const UniqueConnection&) = delete;
    // Las operaciones de movimiento aun pueden habilitarse
};

Antes de C++11, esto se hacia declarando las operaciones de copia como private sin definicion — un truco confuso que =delete reemplaza elegantemente.

Error común

Una asignacion por copia ingenua que elimina el recurso antiguo antes de asignar el nuevo esta rota de dos maneras: (1) la auto-asignacion (a = a) elimina los datos y luego intenta copiar de ellos, y (2) si new lanza una excepcion despues de delete, el objeto queda en un estado invalido. El idioma copiar-e-intercambiar resuelve ambos problemas: la copia se hace antes de cualquier modificacion, y swap es noexcept.

Buena práctica

La mejor manera de evitar errores de copia es nunca gestionar recursos en bruto directamente. Usa std::string en lugar de char*, std::vector en lugar de arrays en bruto, y std::unique_ptr en lugar de punteros en bruto propietarios. Cuando todos tus miembros tienen semantica de copia correcta, las operaciones de copia generadas por el compilador son automaticamente correctas. Esta es la Regla de Cero en la practica.

Puntos clave
  • La copia generada por el compilador es superficial — copia valores de punteros, no los datos apuntados
  • Las clases que poseen recursos necesitan un constructor de copia profunda y un operador de asignacion
  • El idioma copiar-e-intercambiar te da seguridad ante excepciones y seguridad de auto-asignacion gratis
  • Usa =delete para prohibir explicitamente la copia de tipos de recurso unico
  • La mejor estrategia es la Regla de Cero: usa wrappers RAII para que nunca necesites logica de copia personalizada

Quiz — Pon a prueba tus conocimientos

(15 XP)

1. Que sucede cuando haces una copia superficial de un objeto que posee memoria del monton a traves de un puntero en bruto?

2. Por que el idioma copiar-e-intercambiar toma el parametro por valor?

3. Que enfoque se recomienda para evitar necesitar operaciones de copia personalizadas por completo?