Move semantics y value categories
Move semantics permite que un objeto C++ transfiera recursos en vez de duplicarlos. Un move
de std::vector puede robar tres punteros; una copy tiene que alocar y copiar elementos. El
lenguaje decide si mover está disponible mediante value categories: los lvalues tienen
identidad, los prvalues son valores computados puros y los xvalues tienen identidad pero
pueden tratarse como expirando. La regla diaria es simple: std::move(x) no mueve nada por
sí mismo; dice "este objeto nombrado puede tratarse como expiring, así que seleccioná
operaciones move si existen".
El reset: move semantics es transferencia de ownership más overload resolution. No es una copia rápida mágica.
Cómo funciona de verdad
C++11 agregó rvalue references, escritas T&&, y move constructors/assignments. Un tipo
que own un recurso puede definir:
Buffer(Buffer &&other) noexcept;
Buffer &operator=(Buffer &&other) noexcept;
Esas operaciones suelen copiar metadata chica, robar un puntero o handle y dejar al objeto origen válido pero vacío. Un objeto moved-from todavía debe poder destruirse y recibir assignment salvo que el tipo documente un contrato más estrecho. La biblioteca estándar describe los objetos moved-from como válidos pero no especificados, lo que significa que podés destruirlos, asignarles o llamar operaciones sin precondiciones; no podés asumir que su valor viejo sigue ahí.
Las value categories deciden qué overload es viable:
| Expresión | Idea de categoría | Ejemplo | Consecuencia |
|---|---|---|---|
| variable nombrada | lvalue | buffer |
bind a T& / const T& |
| valor temporario | prvalue | Buffer{4} |
puede inicializar directo o bind a T&& |
| objeto expiring | xvalue | std::move(buffer) |
puede bind a T&& y mover |
| parámetro rvalue-reference nombrado | lvalue | other dentro de Buffer(Buffer&& other) |
necesita std::move(other.member) para mover miembros |
La última fila es el gotcha clásico. Un parámetro declarado Buffer&& other tiene nombre,
así que la expresión other es un lvalue. Dentro de un move constructor, los moves de
miembros deben ser explícitos:
Buffer(Buffer &&other) noexcept
: size_(std::exchange(other.size_, 0)),
data_(std::move(other.data_)) {}
noexcept importa porque containers como std::vector necesitan preservar garantías
fuertes de excepciones durante reallocation. Si mover un elemento podría lanzar y copiar
está disponible, el container puede copiar en vez de mover. Para tipos owning recursos, los
moves usualmente deberían ser noexcept.
Artefacto ejecutable: contar copies y moves
El demo ejecutable vive en
examples/modern-cpp/move-semantics-and-value-categories/. Define un Buffer owning heap
con operaciones copy y move explícitas, y después pushea lvalues, lvalues movidos y
temporarios dentro de un vector.
cd examples/modern-cpp/move-semantics-and-value-categories
./run.sh
Las operaciones clave son:
buffers.push_back(first); // first is an lvalue, so copy
buffers.push_back(std::move(first)); // xvalue, so move
buffers.push_back(Buffer{2}); // temporary, so move or direct construction
El move constructor roba el puntero y vacía el origen:
Buffer(Buffer &&other) noexcept
: size_(std::exchange(other.size_, 0)),
data_(std::move(other.data_)) {
++moves;
}
El script también escribe assembly optimizado:
g++ -std=c++20 -O2 -S demo.cpp -o demo.O2.s
Para un tipo trivial que own un puntero, un move suele bajar a copias de punteros, nulling de metadata del origen y llamadas a destructores en los owners finales. Una copy baja a allocation y copia de elementos. Esa es toda la historia de performance: move es rápido solo cuando la operación move del tipo puede transferir una representación barata.
Fallas típicas y trade-offs
std::movedesde un objetoconstnormalmente no mueve. Un move constructor suele necesitarT&&, noconst T&&, porque debe modificar el origen.- Moved-from es válido, no valioso. Chequear
empty()está bien si el tipo lo documenta. Asumir que el contenido viejo sigue ahí no. - Olvidar
noexceptpuede convertir moves en copies. Los containers estándar pueden preferir copy durante reallocation si move puede lanzar y copy está disponible. - Self-move necesita un resultado sensato. Los tipos de biblioteca estándar toleran self-move assignment como válido pero no especificado. Tu propio move assignment debería evitar double-free e invariantes rotas.
- Copy elision puede esconder moves. Devolver un local por valor puede construir
directamente en el caller. Eso es bueno; no agregues
std::move(local)en un return salvo que sepas por qué, porque puede bloquear elision. - Mover un puntero no mueve el pointee. Si un tipo guarda referencias externas, mover el objeto quizá solo transfiera handles. Aliasing y lifetime todavía requieren diseño.
En la práctica
- Preferí rule of zero. Si miembros estándar como
std::vector,std::stringystd::unique_ptrown los recursos, dejá que sus special members hagan el trabajo. - Escribí todos los special members cuando own recursos crudos. Destructor, copy, move y assignments son un conjunto. Un puntero owning crudo con solo destructor es un futuro double-free.
- Usá
std::moveen bordes de ownership. Mover hacia un miembro, container u objeto de retorno es útil. Esparcirlo por todos lados hace el código más difícil de leer y puede empeorar performance. - Tratales a las rvalue references como significado de API.
T&&suele significar "esta función puede consumir el argumento". No tomesT&&solo para evitar una copy si la función solo lee. - Inspeccioná el código generado cuando performance es el argumento. Move semantics es
una transformación de source que debería convertirse en menos allocations y copies.
Chequeá con Compiler Explorer,
-So un profiler.
Conecta con: Smart pointers: unique_ptr, shared_ptr, ownership · RAII: la idea que cambia todo · Structs, unions y bitfields · El heap: malloc/free y el allocator por debajo · Memory leaks y disciplina de ownership · Optimización: qué hace -O2 con tu código
Fuentes
- cppreference - Value categories - lvalue, prvalue, xvalue, glvalue y cómo hacen bind las expresiones. https://en.cppreference.com/w/cpp/language/value_category
- cppreference -
std::move-std::movecomo cast a xvalue y la regla de moved-from válido pero no especificado. https://en.cppreference.com/w/cpp/utility/move - cppreference - Move constructors - declaración, generación, borrado y ejemplos de move constructors. https://en.cppreference.com/w/cpp/language/move_constructor
- cppreference - The rule of three/five/zero - por qué los tipos owning recursos necesitan special members coherentes. https://en.cppreference.com/w/cpp/language/rule_of_three
- C++ working draft -
[class.copy.ctor]- wording estándar para copy y move constructors. https://eel.is/c++draft/class.copy.ctor - C++ working draft -
[lib.types.movedfrom]- requisitos de objetos moved-from en la biblioteca estándar. https://eel.is/c++draft/lib.types.movedfrom - Compiler Explorer - inspeccioná si un move realmente elimina allocation o copying para un tipo y optimizer concretos. https://godbolt.org/