Aller au contenu

Transfert parfait et std::forward

Comprenez pourquoi les références rvalue nommées sont des lvalues, apprenez le schéma canonique de transfert avec std::forward, et voyez comment emplace_back utilise le transfert variadique pour construire les objets sur place.

Le problème : les références rvalue nommées sont des lvalues

Voici le problème fondamental que résout le transfert parfait : lorsque vous acceptez un argument en tant que T&& et que vous le passez à une autre fonction, le nom en fait une lvalue :

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

void outer(int&& x) {
    inner(x);  // appelle inner(int&) — x est une lvalue !
}

La catégorie de valeur est perdue à chaque frontière de fonction. Si vous écrivez une fonction enveloppe (journalisation, mesure du temps, fabrique), vous devez transférer l'argument en préservant sa catégorie de valeur d'origine. Passer simplement x transmet toujours une lvalue. Utiliser std::move(x) transmet toujours une rvalue. Aucune des deux solutions n'est correcte pour une enveloppe générique.

std::forward : la conversion conditionnelle

std::forward(x) est une conversion conditionnelle :

- Si T est une référence lvalue (déduite parce qu'une lvalue a été passée), std::forward(x) retourne une référence lvalue — aucune conversion.
- Si T n'est pas une référence (déduit parce qu'une rvalue a été passée), std::forward(x) retourne une référence rvalue — conversion en T&&.

La différence clé avec std::move :
- std::move convertit toujours en rvalue (inconditionnel).
- std::forward convertit en rvalue uniquement si l'argument d'origine était une rvalue (conditionnel).

std::forward ne doit être utilisé QU'AVEC des références de transfert (T&& où T est déduit). L'utiliser ailleurs est un bogue.

Le schéma canonique de transfert

Le schéma : accepter une référence de transfert T&&, puis utiliser std::forward(arg) pour préserver la catégorie de valeur lors du passage à une autre fonction.

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
}

Transfert variadique et emplace_back

Le transfert parfait révèle toute sa puissance avec les templates variadiques. emplace_back transfère les arguments du constructeur directement dans le conteneur, construisant l'objet sur place sans aucune copie ni aucun déplacement.

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 ou forward : bien faire la différence

| | std::move | std::forward |
|---|---|---|
| Type de conversion | Conversion inconditionnelle en T&& | Conditionnelle — lvalue si T est T&, rvalue si T est T |
| Cas d'usage | « J'en ai fini avec ça, vole-le » | « Transmets ceci avec sa catégorie de valeur d'origine » |
| Contexte | Types concrets, dernière utilisation d'une variable | Uniquement les références de transfert dans les templates |
| Danger | Déplacer par accident une valeur dont on a encore besoin | L'utiliser sans référence de transfert (cela n'a aucun sens) |

Règle empirique : utilisez std::move lorsque vous possédez la valeur et voulez la céder. Utilisez std::forward lorsque vous transférez la valeur de quelqu'un d'autre.

Piège

1. Transférer plus d'une fois : std::forward(arg) peut convertir en rvalue. Si vous transférez le même argument à deux fonctions, la première risque de voler ses ressources et de ne laisser que des données invalides à la seconde. Transférez chaque argument au plus une fois.

2. auto&& est une référence de transfert : for (auto&& elem : container) déduit la catégorie de valeur. C'est très bien pour la boucle for sur intervalle, mais soyez prudent lorsque vous utilisez auto&& dans d'autres contextes.

3. std::forward sans déduction : std::forward(s) n'est qu'un std::move(s) déguisé. N'utilisez std::forward qu'avec un paramètre de template déduit.

4. Listes d'initialisation entre accolades : {1, 2, 3} n'a pas de type et ne peut pas être transféré. Affectez-la d'abord à une variable.

Bonne pratique

1. N'utilisez std::forward qu'avec des références de transfert (T&& où T est déduit).
2. Transférez chaque argument au plus une fois — le transfert peut déplacer, laissant la source vide.
3. Préférez emplace_back à push_back pour construire des objets dans des conteneurs.
4. Ne transférez pas dans une boucle — la première itération risque de voler l'argument.
5. Utilisez if constexpr avec std::is_lvalue_reference_v lorsque vous avez besoin d'un comportement différent pour les lvalues et les rvalues.

Points clés
  • Les références rvalue nommées sont des lvalues — la catégorie de valeur est perdue aux frontières des fonctions
  • std::forward convertit de manière conditionnelle : une lvalue reste une lvalue, une rvalue reste une rvalue
  • std::move est inconditionnel (toujours une rvalue) ; std::forward est conditionnel (préserve la catégorie d'origine)
  • emplace_back utilise le transfert variadique pour construire les objets sur place, sans aucune copie
  • Transférez chaque argument au plus une fois — le transfert peut voler les ressources

Quiz — Testez vos connaissances

(20 XP)

1. Quelle est la différence entre `std::move` et `std::forward` ?

2. Pourquoi `emplace_back` est-il plus efficace que `push_back` pour construire des éléments ?

3. Que se passe-t-il si vous appliquez `std::forward` au même argument pour deux fonctions différentes ?