Categorie di valori e riferimenti

Questo argomento presenta e descrive le varie categorie di valori (e i riferimenti ai valori) presenti in C++:

  • glvalue
  • lvalue
  • xlvalue
  • prvalue
  • rvalue

Alcuni, come lvalue e rvalue, sono ben noti. Tuttavia, potrebbero essere considerati diversamente da come vengono presentati in questo argomento.

Ogni espressione in C++ restituisce un valore che appartiene a una delle cinque categorie illustrate sopra. Alcuni aspetti del linguaggio C++, come funzionalità e regole, richiedono una corretta conoscenza di queste categorie di valori e dei riferimenti a questi valori. Tra questi aspetti vi sono recuperare l'indirizzo di un valore, copiare un valore, spostare un valore e inoltrare un valore a un'altra funzione. Questo argomento non illustra nel dettaglio tutti questi aspetti, ma fornisce informazioni essenziali per una buona conoscenza degli stessi.

Le informazioni contenute in questo argomento sono strutturate in termini di analisi di Stroustrup delle categorie di valori in base alle due proprietà indipendenti di identità e spostabilità [Stroustrup, 2013].

Un valore lvalue ha un'identità

Cosa significa per un valore avere un'identità? Se si ha (o si può recuperare) l'indirizzo di memoria di un valore e questo viene usato in modo sicuro, il valore ha un'identità. In questo modo, oltre a confrontare i contenuti dei valori, è possibile confrontare o distinguere i valori in base all'identità.

Un valore lvalue ha un'identità. Il fatto che la lettera "l" in "lvalue" sia l'abbreviazione di "left", ossia sinistra (a sinistra di un'assegnazione) ha ormai un puro interesse storico. In C++, infatti, un lvalue può apparire a sinistra o a destra di un'assegnazione. La "l" in "lvalue", quindi, non aiuta realmente a comprendere o a definire il valore. L'unica cosa che devi sapere è che un lvalue è un valore che ha un'identità.

Sono esempi di espressioni che corrispondono a lvalue le variabili o costanti denominate e le funzioni che restituiscono un riferimento. Sono esempi di espressioni che non corrispondono a lvalue i valori temporanei e le funzioni che vengono restituite per valore.

int& get_by_ref() { ... }
int get_by_val() { ... }

int main()
{
    std::vector<byte> vec{ 99, 98, 97 };
    std::vector<byte>* addr1{ &vec }; // ok: vec is an lvalue.
    int* addr2{ &get_by_ref() }; // ok: get_by_ref() is an lvalue.

    int* addr3{ &(get_by_ref() + 1) }; // Error: get_by_ref() + 1 is not an lvalue.
    int* addr4{ &get_by_val() }; // Error: get_by_val() is not an lvalue.
}

Così come i valori lvalue, anche i valori xvalue hanno un'identità. Questo argomento illustrerà esattamente che cos'è un xvalue. Per il momento basta sapere che esiste una categoria di valore chiamata glvalue, che significa "lvalue generalizzato". Il set di glvalue è il superset che contiene sia gli lvalue (detti anche lvalue classici) che gli xvalue. Quindi, benché sia vero che un lvalue ha un'identità, il set completo di elementi che hanno un'identità è il set di glvalue, come mostra questa illustrazione.

Un valore lvalue ha un'identità

Un rvalue è spostabile, mentre un lvalue non lo è

Esistono però valori che non sono glvalue. In altre parole, ci sono valori per i quali non è possibile ottenere un indirizzo di memoria (o sulla cui validità non si può fare affidamento). Alcuni valori di questo tipo sono inclusi nell'esempio di codice riportato sopra.

Non avere un indirizzo di memoria affidabile può sembrare uno svantaggio. In realtà il vantaggio di un valore di questo tipo è che è possibile spostarlo (operazione generalmente poco costosa), invece di copiarlo (operazione generalmente dispendiosa). Spostare un valore significa che quel valore non è più dove si trovava prima. Quindi, cercare di accedervi nella posizione in cui si trovava abitualmente è una cosa da evitare. Illustrare quando e come spostare un valore non rientra tra gli scopi di questo argomento. Tutto ciò che occorre sapere è che un valore che può essere spostato è detto rvalue (o rvalue classico).

La lettera "r" in "rvalue" è un'abbreviazione di "destra" (a destra di un'assegnazione). Ma puoi comunque usare i valori rvalue e i relativi riferimenti all'esterno delle assegnazioni. La "r" in "rvalue" non è quindi degna di particolare attenzione. L'unica cosa che ti basta sapere è che un rvalue è un valore che può essere spostato.

Un lvalue, invece, non può essere spostato, come mostra questa illustrazione. Se un lvalue dovesse muoversi, allora ciò sarebbe in contrasto con la definizione stessa di lvalue. E sarebbe un problema imprevisto per il codice che dovrebbe ragionevolmente essere in grado di continuare ad accedere all'lvalue.

Un rvalue è spostabile, mentre un lvalue non lo è

Non è possibile quindi spostare un lvalue. Eppure esiste un tipo di glvalue (il set di elementi con identità) che può essere spostato, se si sa cosa si sta facendo e si è attenti a non accedervi dopo lo spostamento: il valore xvalue. Questa idea verrà rivisitata un'altra volta più avanti in questo argomento quando si esaminerà l'immagine completa delle categorie di valori.

Riferimenti a rvalue e regole di binding dei riferimenti

Questa sezione illustra la sintassi per un riferimento a un rvalue. Dovremo attendere un altro argomento per entrare nel merito delle operazioni di spostamento e inoltro, ma basti dire che i riferimenti rvalue costituiscono una parte necessaria della soluzione di questi problemi. Prima di esaminare i riferimenti rvalue, però, è opportuno chiarire il concetto di T&, ciò che precedentemente abbiamo semplicemente definito "un riferimento". Si tratta in realtà di un riferimento lvalue (non const), che fa riferimento a un valore a cui l'utente del riferimento può scrivere.

template<typename T> T& get_by_lvalue_ref() { ... } // Get by lvalue (non-const) reference.
template<typename T> void set_by_lvalue_ref(T&) { ... } // Set by lvalue (non-const) reference.

Un riferimento lvalue può eseguire il binding a un lvalue, ma non a un rvalue.

Esistono quindi riferimenti lvalue di tipo const (T const&) che fano riferimento a oggetti su cui l'utente del riferimento non può scrivere (ad esempio una costante).

template<typename T> T const& get_by_lvalue_cref() { ... } // Get by lvalue const reference.
template<typename T> void set_by_lvalue_cref(T const&) { ... } // Set by lvalue const reference.

Un riferimento lvalue di tipo const può eseguire il binding a un lvalue o a un rvalue.

La sintassi per un riferimento rvalue di tipo T è scritta come T&&. Un riferimento rvalue fa riferimento a un valore spostabile, ossia un valore di cui non è necessario conservare il contenuto dopo l'uso (ad esempio un elemento temporaneo). Dato che stiamo parlando di spostarsi dal valore (quindi modificandolo) associato a un riferimento rvalue, i qualificatori const e volatile (detti anche qualificatori di tipo const o volatile) non si applicano ai riferimenti rvalue.

template<typename T> T&& get_by_rvalue_ref() { ... } // Get by rvalue reference.
struct A { A(A&& other) { ... } }; // A move constructor takes an rvalue reference.

Un riferimento rvalue esegue il binding a un rvalue. In effetti, in termini di risoluzione dell'overload, un rvalue preferisce essere associato a un riferimento rvalue piuttosto che a un riferimento lvalue di tipo const. Ma un riferimento rvalue non può eseguire il binding a un lvalue perché, come abbiamo detto, fa riferimento a un valore di cui non è necessario conservare il contenuto (ad esempio, il parametro di un costruttore di spostamento).

È possibile anche passare un rvalue dove è previsto un argomento per valore, mediante un costruttore di copia (o un costruttore di spostamento, se l'elemento rvalue è un xvalue).

Un glvalue ha un'identità, mentre un prvalue non ce l'ha

A questo punto dovrebbe essere chiaro quali elementi hanno un'identità, così come quali sono spostabili e quali non lo sono. Ma non abbiamo ancora parlato del set di valori che non hanno un'identità. Questo set è detto prvalue, o rvalue puro.

int& get_by_ref() { ... }
int get_by_val() { ... }

int main()
{
    int* addr3{ &(get_by_ref() + 1) }; // Error: get_by_ref() + 1 is a prvalue.
    int* addr4{ &get_by_val() }; // Error: get_by_val() is a prvalue.
}

Un glvalue ha un'identità, mentre un prvalue non ce l'ha

Quadro completo delle categorie di valori

Non resta che combinare le informazioni e le illustrazioni riportate sopra in un unico quadro generale.

Quadro completo delle categorie di valori

glvalue (i)

Un glvalue (lvalue generalizzato) ha un'identità. Si userà "i" come abbreviazione di "ha identità".

lvalue (i&!m)

Un lvalue (un tipo di glvalue) ha un'identità, ma non è spostabile. Si tratta in genere di valori di lettura/scrittura che vengono passati per riferimento o per riferimento di tipo const oppure, se l'operazione di copia è economica, per valore. Un lvalue non può essere associato a un riferimento rvalue.

xvalue (i&s)

Un xvalue (un tipo di glvalue, ma anche di rvalue) ha un'identità ed è anche spostabile. Potrebbe essere un lvalue precedente che hai deciso di spostare perché la copia è troppo costosa, quindi dovrai prestare attenzione a non accedervi in futuro. Ecco come puoi convertire un lvalue in un xvalue.

struct A { ... };
A a; // a is an lvalue...
static_cast<A&&>(a); // ...but this expression is an xvalue.

Nell'esempio di codice riportato sopra non è ancora stato spostato nulla. È stato soltanto creato un xvalue eseguendo il cast di un lvalue a un riferimento rvalue senza nome. Può essere ancora identificato tramite il nome dell'elemento lvalue, ma, come xvalue, è ora idoneo per lo spostamento. I motivi per eseguire lo spostamento e una descrizione di questa operazione sono forniti in un altro argomento. Ma può essere utile attribuire alla "x" in "xvalue" il significato di "solo per esperti". Tramite il cast di un lvalue in un xvalue (un tipo di rvalue), il valore diventa quindi idoneo per essere associato a un riferimento rvalue.

Ecco altri due esempi di xvalue: la chiamata di una funzione che restituisce un riferimento rvalue senza nome e l'accesso a un membro di un xvalue.

struct A { int m; };
A&& f();
f(); // This expression is an xvalue...
f().m; // ...and so is this.

prvalue (!i&m)

Un prvalue (rvalue puro, un tipo di rvalue) non ha un'identità, ma è spostabile. Si tratta in genere di elementi temporanei, o il risultato della chiamata di una funzione che viene restituita per valore o della valutazione di qualsiasi espressione diversa da un glvalue.

rvalue (s)

Un rvalue è spostabile. "m" verrà usato come abbreviazione di "is movable".

Un riferimento rvalue fa sempre riferimento a un rvalue (un valore di cui non è necessario conservare il contenuto).

Ma un riferimento rvalue è di per sé un rvalue? Un riferimento rvalue senza nome (come quelli illustrati negli esempi di codice xvalue riportati sopra) è un xvalue, quindi è anche un rvalue. Preferisce essere associato a un parametro di funzione di riferimento rvalue, come quello di un costruttore di spostamento. Al contrario (e forse in modo non intuitivo), se un riferimento rvalue ha un nome, l'espressione costituita da quel nome è un lvalue. Quindi non può essere associato a un parametro di riferimento rvalue. Tuttavia, per farlo basta eseguirne di nuovo il cast a un riferimento rvalue senza nome (un xvalue).

void foo(A&) { ... }
void foo(A&&) { ... }
void bar(A&& a) // a is a named rvalue reference; so it's an lvalue.
{
    foo(a); // Calls foo(A&).
    foo(static_cast<A&&>(a)); // Calls foo(A&&).
}
A&& get_by_rvalue_ref() { ... } // This unnamed rvalue reference is an xvalue.

!i&!m

Il tipo di valore che non ha un'identità e non è spostabile è l'unica combinazione che non abbiamo ancora illustrato, ma che può essere ignorata, in quanto questa categoria non è utile nel linguaggio C++.

Regole di compressione dei riferimenti

Più riferimenti dello stesso tipo in un'espressione (un riferimento lvalue a un riferimento lvalue o un riferimento rvalue a un riferimento rvalue) si annullano l'un l'altro.

  • A& & viene compresso in A&.
  • A&& && viene compresso in A&&.

Più riferimenti di tipo diverso in un'espressione vengono compressi in un riferimento lvalue.

  • A& && viene compresso in A&.
  • A&& & viene compresso in A&.

Riferimenti di inoltro

Questa sezione finale confronta i riferimenti rvalue, già illustrati, con il concetto di riferimento di inoltro. Prima che il termine "riferimento all'inoltro" fosse coniato, alcune persone usavano il termine "riferimento universale".

void foo(A&& a) { ... }
  • A&& è un riferimento rvalue, come abbiamo già visto. I tipi const e volatile non si applicano ai riferimenti rvalue.
  • foo accetta solo rvalue di tipo A.
  • I riferimenti rvalue (come A&&) esistono per consentire la creazione di un overload ottimizzato per il passaggio di un elemento temporaneo (o un altro rvalue).
template <typename _Ty> void bar(_Ty&& ty) { ... }
  • _Ty&& è un riferimento di inoltro. A seconda dell'elemento passato a bar, il tipo _Ty potrebbe essere const/non-const indipendentemente dal tipo volatile/non-volatile.
  • bar accetta qualsiasi lvalue o rvalue di tipo _Ty.
  • Con il passaggio di un lvalue il riferimento di inoltro diventa _Ty& &&, che viene compresso nel riferimento lvalue _Ty&.
  • Con il passaggio di un rvalue il riferimento di inoltro diventa il riferimento rvalue _Ty&&.
  • I riferimenti di inoltro (come _Ty&&) non esistono ai fini dell'ottimizzazione, ma per inoltrare gli elementi che vengono loro passati in modo trasparente ed efficiente. Generalmente si incontrerà un riferimento di inoltro solo se si scrive (o si studia nello specifico) il codice di libreria, ad esempio una funzione che inoltra argomenti del costruttore.

Origini

  • [Stroustrup, 2013] B. Stroustrup: The C++ Programming Language, quarta edizione. Addison-Wesley. 2013.