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 est une conversion conditionnelle :
- Si T est une référence lvalue (déduite parce qu'une lvalue a été passée), std::forward 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 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 pour préserver la catégorie de valeur lors du passage à une autre fonction.
#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.
#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.
1. Transférer plus d'une fois : std::forward 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 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.
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.
- Les références rvalue nommées sont des lvalues — la catégorie de valeur est perdue aux frontières des fonctions
std::forwardconvertit de manière conditionnelle : une lvalue reste une lvalue, une rvalue reste une rvaluestd::moveest inconditionnel (toujours une rvalue) ;std::forwardest conditionnel (préserve la catégorie d'origine)emplace_backutilise 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 ?