Vai al contenuto

Perfect forwarding e std::forward

Comprendi perché i riferimenti a rvalue con un nome sono lvalue, impara il pattern canonico di forwarding con std::forward e scopri come emplace_back usa il forwarding variadico per costruire gli oggetti sul posto.

Il problema: i riferimenti a rvalue con un nome sono lvalue

Ecco il problema fondamentale che il perfect forwarding risolve: quando accetti un argomento come T&& e lo passi a un'altra funzione, il nome lo rende un lvalue:

void inner(int& x)  { std::cout << "lvalue\n"; }
void inner(int&& x) { std::cout << "rvalue\n"; }

void outer(int&& x) {
    inner(x);  // chiama inner(int&) — x è un lvalue!
}

La categoria di valore va persa a ogni passaggio di funzione. Se stai scrivendo una funzione wrapper (logging, misurazione dei tempi, factory), devi inoltrare l'argomento preservandone la categoria di valore originale. Passare semplicemente x passa sempre un lvalue. Usare std::move(x) passa sempre un rvalue. Nessuna delle due soluzioni è corretta per un wrapper generico.

std::forward: il cast condizionale

std::forward(x) è un cast condizionale:

- Se T è un riferimento a lvalue (dedotto perché è stato passato un lvalue), std::forward(x) restituisce un riferimento a lvalue — nessun cast.
- Se T è un non riferimento (dedotto perché è stato passato un rvalue), std::forward(x) restituisce un riferimento a rvalue — effettua il cast a T&&.

La differenza chiave rispetto a std::move:
- std::move effettua sempre il cast a rvalue (incondizionato).
- std::forward effettua il cast a rvalue solo se l'argomento originale era un rvalue (condizionale).

std::forward andrebbe usato SOLO con i riferimenti inoltranti (T&& dove T è dedotto). Usarlo altrove è un bug.

Il pattern canonico di forwarding

Il pattern è questo: accetta un riferimento inoltrante T&&, poi usa std::forward(arg) per preservare la categoria di valore quando lo passi a un'altra funzione.

perfect_forwarding.cpp
#include <iostream>
#include <string>
#include <utility>
#include <chrono>

// The function we want to wrap
void process(const std::string& s) {
    std::cout << "process(lvalue): " << s << '\n';
}
void process(std::string&& s) {
    std::cout << "process(rvalue): " << s << '\n';
    // Can steal s's resources since it's an rvalue
}

// Generic wrapper that perfectly forwards
template <typename T>
void timed_process(T&& arg) {
    auto start = std::chrono::steady_clock::now();

    // std::forward<T>(arg) preserves the original value category
    process(std::forward<T>(arg));

    auto elapsed = std::chrono::steady_clock::now() - start;
    std::cout << "Took: " << elapsed.count() << " ns\n";
}

int main() {
    std::string name = "Alice";
    timed_process(name);              // T = string&,  forwards as lvalue
    timed_process(std::string("Bob")); // T = string, forwards as rvalue
    timed_process("Charlie");          // T = const char(&)[8], forwards as lvalue
}

Forwarding variadico ed emplace_back

Il perfect forwarding dà il meglio di sé con i template variadici. emplace_back inoltra gli argomenti del costruttore direttamente nel contenitore, costruendo l'oggetto sul posto senza alcuna copia né spostamento.

variadic_forwarding.cpp
#include <iostream>
#include <vector>
#include <string>
#include <utility>

struct Employee {
    std::string name;
    int age;
    Employee(std::string n, int a) : name(std::move(n)), age(a) {
        std::cout << "Constructed: " << name << '\n';
    }
    Employee(const Employee&) { std::cout << "Copied\n"; }
    Employee(Employee&&) noexcept { std::cout << "Moved\n"; }
};

// Custom factory using variadic forwarding
template <typename T, typename... Args>
std::vector<T> make_vector_of(Args&&... args) {
    std::vector<T> v;
    // Fold expression (C++17) + emplace_back
    (v.emplace_back(std::forward<Args>(args)), ...);
    return v;
}

int main() {
    std::vector<Employee> team;

    // push_back: creates temporary, then MOVES it into vector
    team.push_back(Employee("Alice", 30));
    // Output: Constructed: Alice, Moved

    // emplace_back: constructs IN-PLACE — no copy, no move
    team.emplace_back("Bob", 25);
    // Output: Constructed: Bob

    std::cout << "---\n";
    // Variadic forwarding
    auto v = make_vector_of<std::string>("hello", std::string("world"), "!");
    for (const auto& s : v) std::cout << s << ' ';
    std::cout << '\n';
}

move vs forward: conosci la differenza

| | std::move | std::forward |
|---|---|---|
| Tipo di cast | Cast incondizionato a T&& | Condizionale — lvalue se T è T&, rvalue se T è T |
| Caso d'uso | "Ho finito con questo, rubalo pure" | "Passa questo oltre con la sua categoria di valore originale" |
| Contesto | Tipi concreti, ultimo utilizzo di una variabile | Solo riferimenti inoltranti nei template |
| Pericolo | Spostare per sbaglio un valore che ti serve ancora | Usarlo senza un riferimento inoltrante (privo di senso) |

Regola pratica: usa std::move quando possiedi il valore e vuoi cederlo. Usa std::forward quando stai inoltrando il valore di qualcun altro.

Insidia

1. Inoltrare più di una volta: std::forward(arg) può effettuare il cast a rvalue. Se inoltri lo stesso argomento a due funzioni, la prima potrebbe rubarne le risorse, lasciando spazzatura alla seconda. Inoltra ogni argomento al massimo una volta.

2. auto&& è un riferimento inoltrante: for (auto&& elem : container) deduce la categoria di valore. Va benissimo nel range-for, ma fai attenzione quando usi auto&& in altri contesti.

3. std::forward senza deduzione: std::forward(s) non è altro che uno std::move(s) travestito. Usa std::forward solo con un parametro template dedotto.

4. Liste di inizializzazione tra graffe: {1, 2, 3} non ha un tipo e non può essere inoltrata. Assegnala prima a una variabile.

Buona pratica

1. Usa std::forward solo con i riferimenti inoltranti (T&& dove T è dedotto).
2. Inoltra ogni argomento al massimo una volta — l'inoltro può comportare uno spostamento, lasciando la sorgente vuota.
3. Preferisci emplace_back a push_back quando costruisci oggetti dentro i contenitori.
4. Non inoltrare all'interno di un ciclo — la prima iterazione potrebbe rubare l'argomento.
5. Usa if constexpr con std::is_lvalue_reference_v quando ti serve un comportamento diverso per lvalue e rvalue.

Concetti chiave
  • I riferimenti a rvalue con un nome sono lvalue — la categoria di valore si perde al passaggio di funzione
  • std::forward effettua un cast condizionale: un lvalue resta lvalue, un rvalue resta rvalue
  • std::move è incondizionato (sempre rvalue); std::forward è condizionale (preserva l'originale)
  • emplace_back usa il forwarding variadico per costruire gli oggetti sul posto senza alcuna copia
  • Inoltra ogni argomento al massimo una volta — l'inoltro può rubare le risorse

Quiz — Metti alla prova le tue conoscenze

(20 XP)

1. Qual è la differenza tra `std::move` e `std::forward`?

2. Perché `emplace_back` è più efficiente di `push_back` per costruire elementi?

3. Che cosa succede se inoltri con `std::forward` lo stesso argomento a due funzioni diverse?