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.

  1. Caso 1 (Lvalue): Al pasar p1, el motor de deducción de plantillas de C++ determina que T es Payload&. Debido al reference collapsing (colapso de referencias), el parámetro T&& se convierte en Payload& &, que se simplifica a Payload&. std::forward<Payload&>(arg) realiza un cast a Payload&, llamando al Consumer::process que acepta lvalues.

  2. Caso 2 (Rvalue): Al pasar un temporal, T se deduce como Payload. El parámetro T&& es Payload&&. std::forward<Payload>(arg) realiza un cast a Payload&&, permitiendo que el Consumer::process que recibe un rvalue “robe” la cadena data del objeto temporal.

  3. Caso 3 (std::move): Aquí forzamos a p2 a ser tratada como rvalue. El compilador invoca el constructor de movimiento de Payload, transfiriendo el puntero de la std::string de p2 al parámetro en process. Es vital entender que p2.data ahora 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.

59