Cuando diseñas una clase que gestiona recursos propios (memoria dinámica, descriptores de archivos o sockets), no puedes confiar en el comportamiento por defecto del compilador. El compilador, en su afán de ser eficiente, genera automáticamente un constructor de copia y un operador de asignación que realizan una copia miembro a miembro (member-wise copy). Si tu clase contiene un puntero crudo, el compilador simplemente copiará la dirección de memoria, no el contenido al que apunta. Esto se conoce como copia superficial (shallow copy).
Un constructor de copia se invoca cuando estás inicializando un nuevo objeto a partir de uno ya existente (por ejemplo, al pasar un objeto por valor a una función o al declarar Foo a = b;). Por el contrario, el operador de asignación por copia se ejecuta cuando ambos objetos ya han sido construidos y quieres que uno adopte el estado del otro (por ejemplo, en a = b;).
Debes implementar manualmente una copia profunda (deep copy) cuando tu clase tiene “propiedad” (ownership) sobre un recurso. Si no lo haces, caerás en el problema de la doble liberación (double free) o de punteros colgantes (dangling pointers), ya que dos objetos creerán que son dueños de la misma dirección de memoria. La forma más robusta y segura ante excepciones (exception-safe) de implementar la asignación es mediante el patrón copy-and-swap, el cual aprovecha el constructor de copia para crear una copia temporal y luego intercambia los recursos. Si la asignación falla durante la creación de la copia, el objeto original permanece intacto.
Si por diseño tu clase no debe ser duplicada (como un objeto que representa un hilo o un std::unique_ptr), debes deshabilitar estas operaciones usando = delete.
#include <iostream>
#include <algorithm> // std::swap
#include <utility> // std::move
#include <type_traits>
// Clase que gestiona un recurso (memoria dinámica)
class Buffer {
public:
explicit Buffer(size_t size)
: m_size(size), m_data(new int[size]{}) {
std::cout << "[Log] Buffer construido con tamaño " << m_size << "\n";
}
// Destructor: RAII para liberar la memoria
~Buffer() {
delete[] m_data;
std::cout << "[Log] Buffer destruido\n";
}
// 1. Constructor de copia (Deep Copy)
// Se usa para: Buffer b = a;
Buffer(const Buffer& other)
: m_size(other.m_size), m_data(new int[other.m_size]) {
std::copy(other.m_data, other.m_data + other.m_size, m_data);
std::cout << "[Log] Buffer copiado (Deep Copy)\n";
}
// 2. Operador de asignación por copia (Patrón Copy-and-Swap)
// Se usa para: a = b;
// Al pasar 'other' por VALOR, el compilador usa el constructor de copia
// para crear una copia temporal. Esto garantiza Exception Safety.
Buffer& operator=(Buffer other) noexcept {
// Intercambiamos los recursos de 'this' con la copia temporal 'other'
swap(*this, other);
return *this;
}
// Al salir de la función, 'other' (que ahora tiene los datos viejos)
// se destruye automáticamente.
// Implementación de swap para el idiom copy-and-swap
friend void swap(Buffer& first, Buffer& second) noexcept {
using std::swap;
swap(first.m_size, second.m_size);
swap(first.m_data, second.m_data);
}
void set(size_t index, int value) {
if (index < m_size) m_data[index] = value;
}
int get(size_t index) const {
return (index < m_size) ? m_data[index] : 0;
}
private:
size_t m_size;
int* m_data;
};
// Clase que NO permite ser copiada (Non-copyable)
class UniqueResource {
public:
UniqueResource() = default;
// Deshabilitamos la copia explícitamente
UniqueResource(const UniqueResource&) = delete;
UniqueResource& operator=(const UniqueResource&) = delete;
};
int main() {
// Verificación en tiempo de compilación mediante traits
static_assert(std::is_copy_constructible_v<Buffer>, "Buffer debe ser copiable");
static_assert(!std::is_copy_constructible_v<UniqueResource>, "UniqueResource NO debe ser copiable");
Buffer b1(5);
b1.set(0, 42);
// Uso del constructor de copia
Buffer b2 = b1;
// Uso del operador de asignación
Buffer b3(2);
b3.set(0, 10);
b3 = b1; // b3 ahora tiene el contenido de b1
std::cout << "b1[0]: " << b1.get(0) << "\n";
std::cout << "b2[0]: " << b2.get(0) << "\n";
std::cout << "b3[0]: " << b3.get(0) << "\n";
return 0;
}
Desglose del ejemplo
En el código, la clase Buffer gestiona un array de enteros en el heap.
Para evitar la copia superficial, implementamos el constructor de copia Buffer(const Buffer& other). En lugar de copiar el puntero m_data, primero reservamos nueva memoria con new int[other.m_size] y luego usamos std::copy para transferir los valores. Esto asegura que b1 y b2 tengan direcciones de memoria distintas pero con el mismo contenido.
El operador de asignación operator=(Buffer other) utiliza el idiom copy-and-swap. Es un truco de diseño elegante: al declarar el parámetro other por valor (no por referencia), obligamos al compilador a usar el constructor de copia para crear una instancia temporal. Si la asignación de memoria dentro de ese constructor falla (lanza std::bad_alloc), la función operator= nunca llega a ejecutarse y el objeto original (*this) permanece inalterado, cumpliendo con la garantía de seguridad ante excepciones. Luego, el swap intercambia los punteros de this con los de la copia temporal.
Finalmente, UniqueResource utiliza = delete para indicar al compilador que cualquier intento de copiar este objeto debe resultar en un error de compilación, impidiendo que se usen objetos que representan recursos únicos.
El error frecuente
Un error clásico es implementar la asignación de forma manual sin verificar la auto-asignación o sin asegurar la excepción:
// ERROR: Mala implementación
Buffer& operator=(const Buffer& other) {
if (this != &other) { // Check de auto-asignación (necesario aquí)
delete[] m_data; // Si 'new' falla aquí, el objeto queda corrupto
m_size = other.m_size;
m_data = new int[other.m_size];
std::copy(other.m_data, other.m_data + m_size, m_data);
}
return *this;
}
Si new lanza una excepción en la línea intermedia, el objeto original se queda sin sus datos antiguos y en un estado inconsistente. Además, si olvidas el check if (this != &other), la expresión a = a; provocará que delete[] m_data destruya el propio contenido que intentas copiar. Este tipo de errores de memoria se detectan fácilmente con AddressSanitizer (compila con -fsanitize=address) o mediante herramientas como Valgrind.
N° 38