Vai al contenuto

Riferimenti: lvalue, rvalue e insidie

Esplora i riferimenti a lvalue e a rvalue, comprendi le categorie di valore del C++, impara le regole di collasso dei riferimenti e i riferimenti inoltranti, ed evita i bug da riferimenti pendenti.

Riferimenti a lvalue

Un riferimento a lvalue (T&) è un alias — un altro nome per un oggetto esistente. A differenza dei puntatori, i riferimenti devono essere inizializzati, non possono essere null e non possono essere riassegnati per riferirsi a un oggetto diverso.

I riferimenti a lvalue si legano agli lvalue — espressioni che hanno un'identità (puoi prenderne l'indirizzo). Variabili, elementi di array e puntatori dereferenziati sono tutti lvalue. Un riferimento a lvalue non const (int&) non può legarsi a un temporaneo (rvalue) perché modificare un temporaneo è quasi sempre un bug.

Le basi dei riferimenti a lvalue

I riferimenti a lvalue sono il cavallo di battaglia del C++ — si usano per il passaggio per riferimento, l'overloading degli operatori e per evitare le copie.

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!"
}

Le categorie di valore in C++

Il C++ ha una tassonomia precisa delle categorie di valore delle espressioni. Comprenderle è essenziale per la semantica di spostamento e per il perfect forwarding:

- lvalue — ha un'identità, non può essere spostato implicitamente. Esempi: variabili, *ptr, arr[i], letterali stringa.
- prvalue (rvalue puro) — non ha identità, può essere spostato. Esempi: 42, x + y, std::string("temp"), una funzione che restituisce per valore.
- xvalue (valore in scadenza) — ha un'identità E può essere spostato. Esempi: std::move(x), static_cast(x), un membro di un rvalue.
- glvalue (lvalue generalizzato) — lvalue o xvalue (ha un'identità).
- rvalue — prvalue o xvalue (può essere spostato).

L'intuizione chiave: una variabile di tipo riferimento a rvalue è essa stessa un lvalue — ha un nome e un indirizzo. Questa è la fonte di molta confusione ed è esattamente il motivo per cui esiste std::forward.

Riferimenti a rvalue (T&&)

I riferimenti a rvalue si legano agli rvalue (temporanei e oggetti passati a std::move). Sono il fondamento della semantica di spostamento — ti permettono di rilevare quando un oggetto sta per essere distrutto e di rubarne le risorse.

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
}

Collasso dei riferimenti e riferimenti inoltranti

Quando i riferimenti si combinano nei template, si applicano le regole di collasso dei riferimenti:

- T& & collassa in T&
- T& && collassa in T&
- T&& & collassa in T&
- T&& && collassa in T&&

La regola è semplice: se almeno uno dei due riferimenti è un riferimento a lvalue, il risultato è un riferimento a lvalue. Solo && && dà &&.

Un riferimento inoltrante (forwarding reference, detto anche riferimento universale) è T&& dove T è un parametro template dedotto. Può legarsi sia a lvalue sia a rvalue. Anche auto&& è un riferimento inoltrante. Ma std::vector&& NON lo è — T è già noto. Nemmeno const T&& è un riferimento inoltrante.

Insidia

Un riferimento pendente si riferisce a un oggetto distrutto. A differenza dei puntatori pendenti, non c'è alcun controllo nullptr — il codice userà silenziosamente dati spazzatura oppure andrà in crash.

Cause comuni:

- Restituire un riferimento a una variabile locale — la variabile viene distrutta quando la funzione ritorna.
- Catturare per riferimento in una lambda che sopravvive alla variabile referenziata.
- const auto& a un temporaneo all'interno di un'espressione — const auto& val = get_map()["key"] è pendente se get_map() restituisce per valore, perché la mappa temporanea viene distrutta al punto e virgola.
- Ciclo range-based for su un temporaneo — for (auto& x : get_vector()) va bene (il temporaneo sopravvive per tutto il ciclo), ma for (auto& x : get_wrapper().get_vector()) può risultare pendente se il wrapper viene distrutto.

Estensione della durata di vita con const T&

Un riferimento a lvalue const può estendere la durata di vita di un temporaneo, ma solo quando il legame è diretto. Questo NON funziona attraverso chiamate di funzione o catene di accessi ai membri.

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
}
Concetti chiave
  • I riferimenti a lvalue (T&) sono alias — devono essere inizializzati, non possono essere null né riassegnati
  • const T& può legarsi ai temporanei e ne estende la durata di vita (ma solo con legame diretto)
  • I riferimenti a rvalue (T&&) si legano ai temporanei e abilitano la semantica di spostamento
  • Un riferimento a rvalue con un nome è esso stesso un lvalue — è fondamentale capirlo
  • Collasso dei riferimenti: qualsiasi & nella catena rende il risultato &
  • I riferimenti inoltranti (T&& con T dedotto) si legano sia a lvalue sia a rvalue

Quiz — Metti alla prova le tue conoscenze

(15 XP)

1. Dato `void f(std::string&& s)`, qual è la categoria di valore di `s` all'interno di `f`?

2. Quale regola di collasso dei riferimenti è corretta?

3. Quando funziona l'estensione della durata di vita con `const T&` per i temporanei?