std::move no mueve nada. Es una idea errónea muy extendida: std::move no contiene lógica de transferencia de recursos, no es una instrucción de CPU, ni toca los punteros del objeto. Su única función es realizar un static_cast a una rvalue reference (T&&). Es, en esencia, una etiqueta que le dice al compilador: “Trata a este objeto como si fuera un temporal, incluso si es una lvalue”.
std::forward es otra herramienta de casting, pero con un propósito distinto: el perfect forwarding. A diferencia de std::move, que es un cast incondicional, std::forward es un cast condicional que utiliza la deducción de tipos de las plantillas para restaurar la categoría de valor original del argumento.
Para entenderlo, hay que entender que en C++ una variable con nombre, incluso si su tipo es T&&, es siempre una lvalue. Si tienes una función template<typename T> void f(T&& arg), y llamas a f(x) con una lvalue, arg es una lvalue. Si la pasas a otra función sin std::forward, esa otra función recibirá una lvalue, perdiendo la capacidad de mover el objeto original.
Usarás std::move cuando desees forzar la semántica de movimiento de un objeto que ya no necesitas (por ejemplo, un objeto que está a punto de morir o un miembro que se va a reemplazar). Usarás std::forward únicamente dentro de funciones que utilicen forwarding references (o referencias universales), es decir, cuando el tipo T&& sea un parámetro de plantilla deducido. Si intentas usar std::forward en un tipo que no es una referencia universal, estarás cometiendo un error de diseño o forzando casts innecesarios.
Si usas mal estas herramientas, lo más probable es que el compilador no te avise, pero el rendimiento se desplome debido a copias inesperadas, o peor, podrías terminar con comportamientos inconsistentes si intentas “mover” objetos const (lo cual, como veremos, no es posible).
#include <iostream>
#include <utility>
#include <string>
#include <vector>
#include <type_traits>
// Clase para rastrear si se ejecuta la copia o el movimiento
struct Payload {
std::string data;
explicit Payload(std::string s) : data(std::move(s)) {
std::cout << "[Construido: " << data << "]\n";
}
// Constructor de copia
Payload(const Payload& other) : data(other.data + " (copia)") {
std::cout << "[Copiado]\n";
}
// Constructor de movimiento
Payload(Payload&& other) noexcept : data(std::move(other.data)) {
std::cout << "[Movido]\n";
}
// Impresión para verificar el estado final
void print() const { std::cout << " Contenido: " << data << "\n"; }
};
// Un "Sink" que acepta lvalues o rvalues
struct Consumer {
static void process(Payload&& p) {
std::cout << " -> Sink (rvalue): ";
p.print();
}
static void process(const Payload& p) {
std::cout << " -> Sink (lvalue/const): ";
p.print();
}
};
// Un "Wrapper" que implementa Perfect Forwarding
struct Dispatcher {
template <typename T>
static void dispatch(T&& arg) {
// std::forward preserva la categoría de valor de 'arg'
// Si 'arg' era lvalue, llama a Consumer::process(const Payload&)
// Si 'arg' era rvalue, llama a Consumer::process(Payload&&)
Consumer::process(std::forward<T>(arg));
}
};
int main() {
std::cout << "--- Caso 1: Lvalue normal ---\n";
Payload p1{"Original"};
Dispatcher::dispatch(p1); // T se deduce como Payload&
std::cout << "\n--- Caso 2: Rvalue (temporal) ---\n";
Dispatcher::dispatch(Payload{"Temporal"}); // T se deduce como Payload
std::cout << "\n--- Caso 3: Uso explícito de std::move ---\n";
Payload p2{"Para mover"};
Dispatcher::dispatch(std::move(p2)); // T se deduce como Payload, pero p2 queda en estado válido pero indefinido
std::cout << "\n--- Caso 4: El peligro de std::move con const ---\n";
const Payload p3{"Constante"};
// Intentar mover un const resultará en una copia, no en un movimiento
Dispatcher::dispatch(std::move(p3));
return 0;
}
Análisis del código
En el ejemplo anterior, la clave reside en cómo Dispatcher::dispatch maneja el tipo T.
-
Caso 1 (Lvalue): Al pasar
p1, el motor de deducción de plantillas de C++ determina queTesPayload&. Debido al reference collapsing (colapso de referencias), el parámetroT&&se convierte enPayload& &, que se simplifica aPayload&.std::forward<Payload&>(arg)realiza un cast aPayload&, llamando alConsumer::processque acepta lvalues. -
Caso 2 (Rvalue): Al pasar un temporal,
Tse deduce comoPayload. El parámetroT&&esPayload&&.std::forward<Payload>(arg)realiza un cast aPayload&&, permitiendo que elConsumer::processque recibe un rvalue “robe” la cadenadatadel objeto temporal. -
Caso 3 (std::move): Aquí forzamos a
p2a ser tratada como rvalue. El compilador invoca el constructor de movimiento dePayload, transfiriendo el puntero de lastd::stringdep2al parámetro enprocess. Es vital entender quep2.dataahora es probablemente una cadena vacía o un estado indefinido; el objeto sigue existiendo, pero su contenido ha sido “canibalizado”.
El error frecuente
Un error clásico que ocurre en sistemas de alto rendimiento es intentar usar std::move sobre objetos const.
Fíjate en el Caso 4 del código. Tenemos const Payload p3. Cuando haces std::move(p3), el resultado de la expresión es de tipo const Payload&&.
El constructor de movimiento de nuestra clase Payload tiene la firma Payload(Payload&&). Sin embargo, un objeto de tipo const Payload&& no puede vincularse a un parámetro no-constante Payload&&. El sistema de resolución de sobrecarga del compilador busca la siguiente mejor opción y encuentra el constructor de copia: Payload(const Payload&).
Resultado: Se realiza una copia costosa en lugar de un movimiento rápido, y lo peor es que no hay error de compilación. Para detectarlo, deberías usar herramientas como static_assert con std::is_move_constructible o analizar la jerarquía de llamadas con un debugger. Si ves que un objeto marcado con std::move dispara un constructor de copia, es que el objeto original es const.
N° 59