Menu
Coddy logo textTech

Transfert parfait

Fait partie de la section Programmation Orientée Objet du Journey C++ de Coddy — leçon 84 sur 104.

Lors de l'écriture de fonctions templates qui acceptent des arguments et les transmettent à d'autres fonctions, un problème se pose : comment préserver le fait que l'argument d'origine était une lvalue ou une rvalue ? Le perfect forwarding résout ce problème en transmettant les arguments exactement tels qu'ils ont été reçus, tout en conservant leur catégorie de valeur.

Les ingrédients clés sont les références de transfert (écrites sous la forme T&& dans un contexte de template) et std::forward. Une référence de transfert peut se lier aussi bien aux lvalues qu'aux rvalues, et std::forward convertit conditionnellement l'argument vers son type d'origine :

#include <iostream>
#include <utility>

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

template<typename T>
void wrapper(T&& arg) {
    process(std::forward<T>(arg));
}

int main() {
    int n = 10;
    wrapper(n);       // Appelle process(int&) - lvalue préservée
    wrapper(20);      // Appelle process(int&&) - rvalue préservée
}

Sans std::forward, les paramètres nommés sont toujours des lvalues à l'intérieur de la fonction, même si une rvalue a été passée. Cela empêcherait la sémantique de mouvement de fonctionner correctement lors du transfert vers des constructeurs ou d'autres fonctions.

Le transfert parfait est essentiel pour écrire des fonctions d'usine, des classes wrappers et tout code générique qui doit construire des objets ou appeler des fonctions tout en préservant l'intention de l'appelant concernant les déplacements par rapport aux copies.

challenge icon

Défi

Facile

Construisons une fonction d'usine (factory function) qui démontre le transfert parfait (perfect forwarding) en construisant des objets tout en préservant la catégorie de valeur des arguments qui lui sont passés. Vous allez créer une fonction générique make_object qui transfère les arguments à un constructeur de classe, garantissant que les lvalues sont copiées et les rvalues sont déplacées.

Vous organiserez votre code sur trois fichiers :

  • Widget.h : Définissez une classe Widget qui suit la manière dont elle reçoit ses données.

    Votre Widget doit stocker un nom de type std::string et une valeur de type int. Fournissez deux constructeurs :

    • Un qui prend const std::string& (référence lvalue) et int — affiche Widget constructed (copy): [name]
    • Un qui prend std::string&& (référence rvalue) et int — affiche Widget constructed (move): [name] et déplace la chaîne de caractères

    Ajoutez également une méthode display() qui affiche [name]: [value].

  • Factory.h : Créez votre fonction d'usine à transfert parfait.

    Écrivez une fonction template make_object qui accepte une référence de transfert (forwarding reference) pour le nom et un int classique pour la valeur. Utilisez std::forward pour passer l'argument du nom au constructeur de Widget tout en préservant sa catégorie de valeur d'origine.

    La fonction doit retourner le Widget construit par valeur.

  • main.cpp : Lisez deux entrées :
    1. Un nom (chaîne de caractères)
    2. Une valeur (entier)

    Démontrez le transfert parfait en créant deux widgets :

    1. Premièrement, stockez le nom dans une variable locale et passez-la à make_object en tant que lvalue
    2. Deuxièmement, passez une chaîne temporaire (le même nom concaténé avec "_temp") directement à make_object en tant que rvalue

    Après avoir créé chaque widget, appelez sa méthode display().

Par exemple, avec les entrées Gadget et 42 :

Widget constructed (copy): Gadget
Gadget: 42
Widget constructed (move): Gadget_temp
Gadget_temp: 42

Avec les entrées Device et 100 :

Widget constructed (copy): Device
Device: 100
Widget constructed (move): Device_temp
Device_temp: 100

L'idée clé est que votre fonction make_object ne sait pas si elle a reçu une lvalue ou une rvalue — mais en utilisant std::forward, elle préserve cette information lors de l'appel au constructeur de Widget. Cela permet à la résolution de surcharge du constructeur de sélectionner la version appropriée, permettant des déplacements efficaces pour les temporaires tout en copiant en toute sécurité les variables nommées.

Aide-mémoire

Le transfert parfait (perfect forwarding) permet aux fonctions templates de passer des arguments à d'autres fonctions tout en préservant leur catégorie de valeur d'origine (lvalue ou rvalue).

Le mécanisme utilise des références de transfert (T&& dans un contexte de template) combinées avec std::forward :

#include <utility>

template<typename T>
void wrapper(T&& arg) {
    process(std::forward<T>(arg));
}

Une référence de transfert peut se lier à la fois aux lvalues et aux rvalues. std::forward<T> convertit conditionnellement l'argument vers son type d'origine.

Sans std::forward, les paramètres nommés à l'intérieur d'une fonction sont toujours traités comme des lvalues, même si une rvalue a été passée. Cela empêcherait la sémantique de mouvement de fonctionner correctement.

Exemple illustrant la préservation de la catégorie de valeur :

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

template<typename T>
void wrapper(T&& arg) {
    process(std::forward<T>(arg));
}

int n = 10;
wrapper(n);       // Appelle process(int&) - lvalue préservée
wrapper(20);      // Appelle process(int&&) - rvalue préservée

Le transfert parfait est essentiel pour les fonctions de fabrique, les classes d'enveloppement (wrappers) et le code générique qui construit des objets ou appelle des fonctions tout en respectant l'intention de l'appelant concernant les déplacements ou les copies.

Essayez vous-même

#include <iostream>
#include <string>
#include "Widget.h"
#include "Factory.h"

using namespace std;

int main() {
    // Lire l'entrée
    string name;
    int value;
    cin >> name >> value;
    
    // TODO: Créer le premier widget en passant name comme une lvalue
    // Stocker le name dans une variable locale et le passer à make_object
    
    
    // TODO: Appeler display() sur le premier widget
    
    
    // TODO: Créer le deuxième widget en passant une chaîne temporaire comme une rvalue
    // Passer (name + "_temp") directement à make_object
    
    
    // TODO: Appeler display() sur le deuxième widget
    
    
    return 0;
}
quiz iconTestez-vous

Cette leçon comprend un petit quiz. Commencez la leçon pour y répondre et suivre votre progression.

Toutes les leçons de Programmation Orientée Objet