La semántica de movimiento [C++11] permite transferir la propiedad de un recurso (como memoria dinámica, descriptores de archivos o punteros a buffers) de un objeto a otro, en lugar de realizar una duplicación costosa. En una copia profunda (deep copy), el sistema debe asignar nueva memoria y copiar cada elemento del origen al destino. En un movimiento, simplemente “robamos” el puntero del objeto original, transformando una operación $O(n)$ en una operación $O(1)$.
Este mecanismo funciona gracias a la distinción entre lvalues (objetos con nombre y dirección de memoria persistente) y rvalues (objetos temporales o que pueden ser tratados como tales). Cuando un objeto está a punto de ser destruido, podemos usar std::move para convertir su lvalue en un rvalue, lo que permite que los sobrecargados de constructor de movimiento y operador de asignación por movimiento tomen sus recursos. Debemos usar estas técnicas cuando gestionamos recursos pesados en objetos que tienen una vida útil corta o que se van a transferir a contenedores. Si no implementas correctamente el constructor de movimiento, el compilador podría optar por realizar copias profundas innecesarias, degradando el rendimiento de tu sistema. Si, por el contrario, dejas el objeto origen en un estado inconsistente que provoque un error al llamar a su destructor, causarás un comportamiento indefinido (como un double free).
#include <iostream>
#include <utility> // std::move, std::exchange
#include <vector>
#include <algorithm>
class Buffer {
private:
size_t m_size;
int* m_data;
public:
// Constructor normal
explicit Buffer(size_t size)
: m_size(size), m_data(new int[size]) {
std::cout << "Buffer creado con tamaño " << m_size << "\n";
}
// Destructor
~Buffer() {
delete[] m_data;
std::cout << "Buffer destruido\n";
}
// Copy Constructor (Copia profunda)
Buffer(const Buffer& other)
: m_size(other.m_size), m_data(new int[other.m_size]) {
std::copy(other.m_data, other.m_data + m_size, m_data);
std::cout << "Copia profunda realizada\n";
}
// Move Constructor [C++11]
// Es vital usar noexcept para que los contenedores como std::vector
// puedan mover en lugar de copiar durante las reasignaciones.
Buffer(Buffer&& other) noexcept
: m_data(std::exchange(other.m_data, nullptr)),
m_size(std::exchange(other.m_size, 0)) {
std::cout << "Movimiento de constructor ejecutado\n";
}
// Copy Assignment Operator
Buffer& operator=(const Buffer& other) {
if (this != &other) {
delete[] m_data;
m_size = other.m_size;
m_data = new int[m_size];
std::copy(other.m_data, other.m_data + m_size, m_data);
std::cout << "Asignación por copia ejecutada\n";
}
return *this;
}
// Move Assignment Operator [C++11]
Buffer& operator=(Buffer&& other) noexcept {
if (this != &other) {
// Liberamos los recursos actuales del objeto destino
delete[] m_data;
// "Robamos" los recursos del objeto origen
m_data = std::exchange(other.m_data, nullptr);
m_size = std::exchange(other.m_size, 0);
std::cout << "Asignación por movimiento ejecutada\n";
}
return *this;
}
void print() const {
if (!m_data) {
std::cout << "Buffer vacío\n";
return;
}
std::cout << "Buffer con " << m_size << " elementos\n";
}
};
int main() {
Buffer b1(100);
// std::move no mueve nada; es un cast a rvalue.
// Permite que el constructor de movimiento sea elegido.
Buffer b2 = std::move(b1);
std::vector<Buffer> vec;
vec.reserve(1);
vec.push_back(Buffer(50)); // Se usa el constructor de movimiento si es posible
return 0;
}
Desglose del ejemplo
En el constructor de movimiento Buffer(Buffer&& other), utilizamos std::exchange. Esta utilidad de <utility> es fundamental para una implementación limpia: m_data(std::exchange(other.m_data, nullptr)) hace dos cosas en una: devuelve el valor original de other.m_data para inicializar nuestro miembro y, simultáneamente, establece other.m_data en nullptr. Esto garantiza que el objeto origen quede en un “estado movido” (moved-from state) válido: el puntero es nullptr, por lo que su destructor ejecutará delete[] nullptr, lo cual es una operación segura que no hace nada.
En el operador de asignación por movimiento operator=(Buffer&& other), implementamos una comprobación de autovía (if (this != &other)) para evitar problemas si intentas asignarte un objeto a ti mismo (por ejemplo, b1 = std::move(b1)). Si no hiciéramos esta comprobación y el objeto fuera el mismo, el delete[] m_data liberaría la memoria que estamos intentando transferir, dejando al objeto con un puntero colgante (dangling pointer).
Fíjate en la etiqueta noexcept. Es la diferencia entre un sistema de alto rendimiento y uno que sufre picos de latencia inesperados. Cuando un std::vector necesita redimensionizarse para albergar nuevos elementos, verifica si el tipo que contiene tiene un constructor de movimiento marcado con noexcept. Si no lo tiene, el vector optará por copiar todos los elementos en la nueva ubicación en lugar de moverlos. Esto se hace para cumplir con la garantía fuerte de excepciones: si una copia falla, el vector puede volver a su estado original, pero si un movimiento falla (lanza una excepción), el vector quedaría en un estado corrupto ya que no puede “deshacer” el movimiento.
El error frecuente
Un error clásico es olvidar noexcept en el constructor de movimiento. Aunque el código compile y funcione en pruebas simples, en producción causarás un problema de rendimiento masivo.
// ERROR: Sin noexcept
Buffer(Buffer&& other) : m_data(other.m_data), m_size(other.m_size) {
other.m_data = nullptr;
// ...
}
Si usas esta versión con std::vector<Buffer>, notarás que al hacer push_back o reserve, el compilador preferirá realizar copias profundas en cada reasignación de memoria para mantener la seguridad de excepciones. Esto anula por completo el propósito de haber implementado la semántica de movimiento. Para detectarlo, puedes usar un static_assert(std::is_nothrow_move_constructible_v<Buffer>) o revisar la salida de optimización de tu compilador.
N° 39