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.
#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, 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.
#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.
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.
#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
}- 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&&conTdedotto) 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?