conceptC++ Moderno~4 min de lecturaActualizado 2026-07-02#cpp#move-semantics#value-categories#rvalue-references#ownership

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::move desde un objeto const normalmente no mueve. Un move constructor suele necesitar T&&, no const 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 noexcept puede 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::string y std::unique_ptr own 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::move en 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 tomes T&& 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, -S o 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::move como 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/