Devi capire il problema di inoltro. Puoi leggere l'intero problema in dettaglio , ma riassumerò.
Fondamentalmente, data l'espressione E(a, b, ... , c)
, vogliamo che l'espressione f(a, b, ... , c)
sia equivalente. In C ++ 03, questo è impossibile. Ci sono molti tentativi, ma falliscono tutti in qualche modo.
Il più semplice è usare un riferimento lvalue:
template <typename A, typename B, typename C>
void f(A& a, B& b, C& c)
{
E(a, b, c);
}
Ma questo non riesce a gestire i valori temporanei f(1, 2, 3);
:, poiché questi non possono essere associati a un riferimento lvalue.
Il prossimo tentativo potrebbe essere:
template <typename A, typename B, typename C>
void f(const A& a, const B& b, const C& c)
{
E(a, b, c);
}
Il che risolve il problema sopra, ma lancia i flop. Ora non riesce a consentire E
di avere argomenti non const:
int i = 1, j = 2, k = 3;
void E(int&, int&, int&); f(i, j, k); // oops! E cannot modify these
Il terzo tentativo accetta riferimenti cost, ma poi const_cast
è const
via:
template <typename A, typename B, typename C>
void f(const A& a, const B& b, const C& c)
{
E(const_cast<A&>(a), const_cast<B&>(b), const_cast<C&>(c));
}
Questo accetta tutti i valori, può trasmettere tutti i valori, ma potenzialmente porta a comportamenti indefiniti:
const int i = 1, j = 2, k = 3;
E(int&, int&, int&); f(i, j, k); // ouch! E can modify a const object!
Un'ultima soluzione gestisce tutto correttamente ... al costo di essere impossibile da mantenere. Si forniscono sovraccarichi di f
, con tutte le combinazioni di const e non const:
template <typename A, typename B, typename C>
void f(A& a, B& b, C& c);
template <typename A, typename B, typename C>
void f(const A& a, B& b, C& c);
template <typename A, typename B, typename C>
void f(A& a, const B& b, C& c);
template <typename A, typename B, typename C>
void f(A& a, B& b, const C& c);
template <typename A, typename B, typename C>
void f(const A& a, const B& b, C& c);
template <typename A, typename B, typename C>
void f(const A& a, B& b, const C& c);
template <typename A, typename B, typename C>
void f(A& a, const B& b, const C& c);
template <typename A, typename B, typename C>
void f(const A& a, const B& b, const C& c);
N argomenti richiedono 2 N combinazioni, un incubo. Vorremmo farlo automaticamente.
(Questo è effettivamente ciò che facciamo fare al compilatore per noi in C ++ 11.)
In C ++ 11, abbiamo la possibilità di risolvere questo problema. Una soluzione modifica le regole di detrazione dei modelli sui tipi esistenti, ma ciò potenzialmente rompe una grande quantità di codice. Quindi dobbiamo trovare un altro modo.
La soluzione consiste invece nell'utilizzare i riferimenti rvalue appena aggiunti ; possiamo introdurre nuove regole quando deduciamo i tipi di riferimento rvalue e creare qualsiasi risultato desiderato. Dopotutto, non possiamo assolutamente rompere il codice ora.
Se viene dato un riferimento a un riferimento (nota di riferimento è un termine comprendente che significa sia T&
e che T&&
), usiamo la seguente regola per capire il tipo risultante:
"[dato] un tipo TR che è un riferimento a un tipo T, un tentativo di creare il tipo" riferimento lvalue a cv TR "crea il tipo" riferimento lvalue a T ", mentre un tentativo di creare il tipo" riferimento rvalue a cv TR "crea il tipo TR."
O in forma tabellare:
TR R
T& & -> T& // lvalue reference to cv TR -> lvalue reference to T
T& && -> T& // rvalue reference to cv TR -> TR (lvalue reference to T)
T&& & -> T& // lvalue reference to cv TR -> lvalue reference to T
T&& && -> T&& // rvalue reference to cv TR -> TR (rvalue reference to T)
Quindi, con la deduzione dell'argomento template: se un argomento è un valore l A, forniamo all'argomento template un riferimento lvalue ad A. Altrimenti, deduciamo normalmente. Ciò fornisce i cosiddetti riferimenti universali (il termine riferimento di inoltro è ora quello ufficiale).
Perché è utile? Poiché combinati manteniamo la capacità di tenere traccia della categoria di valore di un tipo: se si trattava di un lvalue, abbiamo un parametro di riferimento lvalue, altrimenti abbiamo un parametro di riferimento rvalue.
Nel codice:
template <typename T>
void deduce(T&& x);
int i;
deduce(i); // deduce<int&>(int& &&) -> deduce<int&>(int&)
deduce(1); // deduce<int>(int&&)
L'ultima cosa è "inoltrare" la categoria di valore della variabile. Tieni presente che, una volta all'interno della funzione, il parametro può essere passato come valore a qualsiasi cosa:
void foo(int&);
template <typename T>
void deduce(T&& x)
{
foo(x); // fine, foo can refer to x
}
deduce(1); // okay, foo operates on x which has a value of 1
Non va bene. E deve ottenere lo stesso tipo di categoria di valore che abbiamo ottenuto! La soluzione è questa:
static_cast<T&&>(x);
Cosa fa questo? Considera che siamo all'interno della deduce
funzione e ci è stato superato un valore. Questo significa che T
è un A&
, e quindi il tipo di destinazione per il cast statico è A& &&
, o solo A&
. Dato che x
è già un A&
, non facciamo nulla e ci viene lasciato un riferimento al valore.
Quando abbiamo superato un valore, T
è A
, quindi il tipo di destinazione per il cast statico è A&&
. Il cast produce un'espressione rvalore, che non può più essere passata a un riferimento lvalue . Abbiamo mantenuto la categoria di valore del parametro.
Metterli insieme ci dà "inoltro perfetto":
template <typename A>
void f(A&& a)
{
E(static_cast<A&&>(a));
}
Quando f
riceve un valore, E
ottiene un valore. Quando f
riceve un valore, E
ottiene un valore. Perfetto.
E, naturalmente, vogliamo sbarazzarci dei brutti. static_cast<T&&>
è criptico e strano da ricordare; facciamo invece una funzione di utilità chiamata forward
, che fa la stessa cosa:
std::forward<A>(a);
// is the same as
static_cast<A&&>(a);
f
sarebbe una funzione e non un'espressione?