Aller au contenu

Références : lvalue, rvalue et pièges

Explorez les références lvalue et rvalue, comprenez les catégories de valeur du C++, apprenez les règles de fusion des références et les références de transfert, et évitez les bogues de références pendantes.

Références lvalue

Une référence lvalue (T&) est un alias — un autre nom pour un objet existant. Contrairement aux pointeurs, les références doivent être initialisées, ne peuvent pas être nulles et ne peuvent pas être réassociées pour désigner un autre objet.

Les références lvalue se lient aux lvalues — des expressions qui ont une identité (vous pouvez prendre leur adresse). Les variables, les éléments de tableau et les pointeurs déréférencés sont tous des lvalues. Une référence lvalue non const (int&) ne peut pas se lier à un temporaire (rvalue), car modifier un temporaire est presque toujours un bogue.

Bases des références lvalue

Les références lvalue sont les bêtes de somme du C++ — utilisées pour le passage par référence, la surcharge d'opérateurs et pour éviter les copies.

lvalue_refs.cpp
#include <iostream>
#include <string>

void append_greeting(std::string& s) {
    s += ", welcome!";  // modifies the caller's string
}

int main() {
    int x = 10;
    int& ref = x;      // ref is an alias for x
    ref = 20;
    std::cout << x << '\n';  // 20 — x was modified through ref

    // const reference can bind to a temporary (rvalue)
    const std::string& greeting = std::string("Hello");
    // The temporary's lifetime is extended to match greeting's scope
    std::cout << greeting << '\n';  // "Hello"

    // Non-const lvalue reference CANNOT bind to rvalue:
    // std::string& bad = std::string("oops");  // COMPILER ERROR

    std::string name = "Alice";
    append_greeting(name);
    std::cout << name << '\n';  // "Alice, welcome!"
}

Les catégories de valeur en C++

Le C++ possède une taxonomie précise des catégories de valeur des expressions. Les comprendre est essentiel pour la sémantique de déplacement et le transfert parfait :

- lvalue — a une identité, on ne peut pas en déplacer implicitement le contenu. Exemples : variables, *ptr, arr[i], littéraux de chaîne.
- prvalue (rvalue pure) — n'a pas d'identité, peut être déplacée. Exemples : 42, x + y, std::string("temp"), une fonction qui retourne par valeur.
- xvalue (valeur expirante) — a une identité ET peut être déplacée. Exemples : std::move(x), static_cast(x), un membre d'une rvalue.
- glvalue (lvalue généralisée) — lvalue ou xvalue (a une identité).
- rvalue — prvalue ou xvalue (peut être déplacée).

L'idée clé : une variable référence rvalue est elle-même une lvalue — elle a un nom et une adresse. C'est la source de beaucoup de confusion, et c'est précisément pour cela que std::forward existe.

Références rvalue (T&&)

Les références rvalue se lient aux rvalues (les temporaires et les objets passés à std::move). Elles sont le fondement de la sémantique de déplacement — elles vous permettent de détecter qu'un objet est sur le point d'être détruit et de lui voler ses ressources.

rvalue_refs.cpp
#include <iostream>
#include <string>
#include <utility>  // std::move

void process(const std::string& s) {
    std::cout << "lvalue: " << s << '\n';
}

void process(std::string&& s) {
    std::cout << "rvalue: " << s << '\n';
    // s is an lvalue inside this function (it has a name!)
    // To move from s, you'd need std::move(s)
}

int main() {
    std::string a = "hello";
    process(a);               // calls lvalue overload
    process(std::string("world"));  // calls rvalue overload
    process(std::move(a));    // calls rvalue overload — a is now moved-from

    // IMPORTANT: a is in a valid but unspecified state
    // Only safe operations: assign to it, destroy it
    std::cout << "a after move: '" << a << "'\n";  // likely empty
}

Fusion des références et références de transfert

Lorsque des références se combinent dans des templates, les règles de fusion des références (reference collapsing) s'appliquent :

- T& & se réduit à T&
- T& && se réduit à T&
- T&& & se réduit à T&
- T&& && se réduit à T&&

La règle est simple : si l'une des deux références est une référence lvalue, le résultat est une référence lvalue. Seul && && donne &&.

Une référence de transfert (forwarding reference, aussi appelée référence universelle) est un T&& où T est un paramètre de template déduit. Elle peut se lier aussi bien aux lvalues qu'aux rvalues. auto&& est également une référence de transfert. Mais std::vector&& n'en est PAS une — T est déjà connu. const T&& n'est pas non plus une référence de transfert.

Piège

Une référence pendante (dangling reference) désigne un objet détruit. Contrairement aux pointeurs pendants, aucune vérification par nullptr n'est possible — le code utilisera silencieusement des données invalides ou plantera.

Causes courantes :

- Retourner une référence vers une variable locale — la variable est détruite au retour de la fonction.
- Capturer par référence dans une lambda qui survit à la variable référencée.
- const auto& vers un temporaire au sein d'une expression — const auto& val = get_map()["key"] est pendante si get_map() retourne par valeur, car la map temporaire est détruite au point-virgule.
- Boucle for sur intervalle portant sur un temporaire — for (auto& x : get_vector()) ne pose pas de problème (le temporaire vit pendant toute la boucle), mais for (auto& x : get_wrapper().get_vector()) peut devenir pendante si le wrapper est détruit.

Prolongation de durée de vie avec const T&

Une référence lvalue const peut prolonger la durée de vie d'un temporaire, mais uniquement lorsqu'elle y est directement liée. Cela ne fonctionne PAS à travers des appels de fonction ou des chaînes d'accès aux membres.

lifetime_extension.cpp
#include <iostream>
#include <string>

std::string make_greeting() { return "Hello, World!"; }

const std::string& identity(const std::string& s) { return s; }

int main() {
    // SAFE: const ref directly bound to temporary
    const std::string& good = make_greeting();
    std::cout << good << '\n';  // OK — lifetime extended

    // DANGEROUS: lifetime extension does NOT propagate through functions
    const std::string& bad = identity(make_greeting());
    // bad is DANGLING — the temporary was destroyed after identity() returned
    // std::cout << bad << '\n';  // UNDEFINED BEHAVIOR

    // SAFE: just use a value
    std::string safe = make_greeting();  // move or copy-elision
    std::cout << safe << '\n';  // always safe
}
Points clés
  • Les références lvalue (T&) sont des alias — elles doivent être initialisées et ne peuvent être ni nulles ni réassociées
  • const T& peut se lier à des temporaires et prolonge leur durée de vie (mais seulement en cas de liaison directe)
  • Les références rvalue (T&&) se lient aux temporaires et rendent possible la sémantique de déplacement
  • Une référence rvalue nommée est elle-même une lvalue — c'est un point crucial à comprendre
  • Fusion des références : tout & dans la chaîne fait du résultat un &
  • Les références de transfert (T&& avec T déduit) se lient aussi bien aux lvalues qu'aux rvalues

Quiz — Testez vos connaissances

(15 XP)

1. Soit `void f(std::string&& s)` : quelle est la catégorie de valeur de `s` à l'intérieur de `f` ?

2. Quelle règle de fusion des références est correcte ?

3. Quand la prolongation de durée de vie des temporaires par `const T&` fonctionne-t-elle ?