Quali sono gli scopi principali dell'uso di std :: forward e quali problemi risolve?


438

In perfetto inoltro, std::forwardviene utilizzato per convertire i riferimenti rvalue nominati t1e t2in riferimenti rvalue senza nome. Qual è lo scopo di farlo? In che modo ciò influirebbe sulla funzione chiamata innerse lasciamo t1e t2come valori?

template <typename T1, typename T2>
void outer(T1&& t1, T2&& t2) 
{
    inner(std::forward<T1>(t1), std::forward<T2>(t2));
}

Risposte:


789

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 Edi 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è constvia:

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 deducefunzione 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 friceve un valore, Eottiene un valore. Quando friceve un valore, Eottiene 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);

1
Non fsarebbe una funzione e non un'espressione?
Michael Foukarakis,

1
Il tuo ultimo tentativo non è corretto per quanto riguarda l'affermazione del problema: inoltrerà i valori const come non const, quindi non inoltrerà affatto. Si noti inoltre che nel primo tentativo, const int iverrà accettato: Aviene dedotto a const int. I fallimenti sono per i valori letterali. Si noti inoltre che per la chiamata a deduced(1), x int&&non lo è int(l'inoltro perfetto non crea mai una copia, come si farebbe se xsi trattasse di un parametro per valore). Lo Tè solo int. Il motivo per cui viene xvalutato un valore nel server di inoltro è perché i riferimenti ai valori nominali diventano espressioni di valore.
Johannes Schaub - lett.

5
C'è qualche differenza nell'uso forwardo movequi? O è solo una differenza semantica?
0x499602D2

28
@David: std::movedovrebbe essere chiamato senza argomenti espliciti del modello e si traduce sempre in un valore, mentre std::forwardpuò finire come uno dei due. Utilizzare std::movequando si sa che non è più necessario il valore e si desidera spostarlo altrove, utilizzare std::forwardper farlo in base ai valori passati al modello di funzione.
GManNickG

5
Grazie per aver iniziato con esempi concreti prima e motivando il problema; molto utile!
ShreevatsaR,

61

Penso che un codice concettuale che implementa std :: forward possa aggiungere alla discussione. Questa è una slide del discorso di Scott Meyers un efficace campionatore C ++ 11/14

codice concettuale che implementa std :: forward

Funzione move nel codice è std::move. C'è un'implementazione (funzionante) per esso all'inizio di quel discorso. Ho trovato l'implementazione effettiva di std :: forward in libstdc ++ , nel file move.h, ma non è affatto istruttivo.

Dal punto di vista di un utente, il significato è che std::forwardè un cast condizionale a un valore. Può essere utile se sto scrivendo una funzione che prevede un valore o un valore in un parametro e desidera passarlo a un'altra funzione come valore solo se è stato passato come valore. Se non avvolgessi il parametro in std :: forward, verrebbe sempre passato come riferimento normale.

#include <iostream>
#include <string>
#include <utility>

void overloaded_function(std::string& param) {
  std::cout << "std::string& version" << std::endl;
}
void overloaded_function(std::string&& param) {
  std::cout << "std::string&& version" << std::endl;
}

template<typename T>
void pass_through(T&& param) {
  overloaded_function(std::forward<T>(param));
}

int main() {
  std::string pes;
  pass_through(pes);
  pass_through(std::move(pes));
}

Abbastanza sicuro, stampa

std::string& version
std::string&& version

Il codice si basa su un esempio del discorso precedentemente citato. Diapositiva 10, circa alle 15:00 dall'inizio.


2
Il tuo secondo link ha finito per indicare un posto completamente diverso.
Pharap,

34

Nell'inoltro perfetto, std :: forward viene utilizzato per convertire il riferimento al valore nominale t1 e t2 in riferimento al valore senza nome. Qual è lo scopo di farlo? In che modo ciò influirebbe sulla funzione chiamata inner se lasciamo t1 & t2 come lvalue?

template <typename T1, typename T2> void outer(T1&& t1, T2&& t2) 
{
    inner(std::forward<T1>(t1), std::forward<T2>(t2));
}

Se si utilizza un riferimento rvalue denominato in un'espressione, in realtà è un lvalue (poiché si fa riferimento all'oggetto per nome). Considera il seguente esempio:

void inner(int &,  int &);  // #1
void inner(int &&, int &&); // #2

Ora, se chiamiamo outercosì

outer(17,29);

vorremmo che 17 e 29 fossero inoltrati al n. 2 perché 17 e 29 sono letterali interi e come tali valori. Ma poiché t1e t2nell'espressione ci inner(t1,t2);sono i valori, dovresti invocare il numero 1 anziché il numero 2. Ecco perché dobbiamo trasformare i riferimenti in riferimenti senza nome con std::forward. Quindi, t1in outerè sempre un'espressione lvalue mentre forward<T1>(t1)può essere un'espressione rvalue a seconda T1. Quest'ultima è solo un'espressione lvalue se T1è un riferimento lvalue. E T1si deduce solo come riferimento lvalue nel caso in cui il primo argomento esterno fosse un'espressione lvalue.


Questa è una sorta di spiegazione annacquata, ma una spiegazione molto ben fatta e funzionale. Le persone dovrebbero prima leggere questa risposta e poi approfondire se lo desiderano
NicoBerrogorry,

@sellibitze Un'altra domanda, quale affermazione è corretta quando si deduce int a; f (a): "poiché a è un valore l, quindi int (T &&) equivale a int (int & &&)" o "per rendere T&& uguale a int &, quindi T dovrebbe essere int & "? Preferisco a quest'ultimo.
John

11

In che modo ciò influirebbe sulla funzione chiamata inner se lasciamo t1 & t2 come lvalue?

Se, dopo l'istanza, T1è di tipo chared T2è di una classe, si desidera passare t1per copia e t2per constriferimento. Bene, a meno che non inner()li prenda per non-const riferimento, cioè nel qual caso lo si desidera anche.

Prova a scrivere una serie di outer()funzioni che implementano questo senza riferimenti a valori, deducendo il modo giusto da cui passare gli argomentiinner() tipo di. Penso che avrai bisogno di qualcosa di 2 ^ 2 di loro, roba abbastanza pesante per meta-template per dedurre gli argomenti, e molto tempo per farlo bene in tutti i casi.

E poi qualcuno arriva con un inner()che accetta argomenti per puntatore. Penso che ora faccia 3 ^ 2. (O 4 ^ 2. Diavolo, non posso preoccuparmi di provare a pensare se il constpuntatore farebbe la differenza.)

E poi immagina di voler fare questo per cinque parametri. O sette.

Ora sai perché alcune menti brillanti hanno escogitato un "perfetto inoltro": fa sì che il compilatore faccia tutto questo per te.


5

Un punto che non è stato reso cristallino è che static_cast<T&&>gestisce const T&anche correttamente.
Programma:

#include <iostream>

using namespace std;

void g(const int&)
{
    cout << "const int&\n";
}

void g(int&)
{
    cout << "int&\n";
}

void g(int&&)
{
    cout << "int&&\n";
}

template <typename T>
void f(T&& a)
{
    g(static_cast<T&&>(a));
}

int main()
{
    cout << "f(1)\n";
    f(1);
    int a = 2;
    cout << "f(a)\n";
    f(a);
    const int b = 3;
    cout << "f(const b)\n";
    f(b);
    cout << "f(a * b)\n";
    f(a * b);
}

produce:

f(1)
int&&
f(a)
int&
f(const b)
const int&
f(a * b)
int&&

Nota che 'f' deve essere una funzione modello. Se è appena definito come 'void f (int && a)' non funziona.


buon punto, quindi T&& nel cast statico segue anche le regole del collasso di riferimento, giusto?
Barney,

3

Potrebbe essere utile sottolineare che il forward deve essere usato in tandem con un metodo esterno con forwarding / riferimento universale. L'uso di forward da solo come le seguenti affermazioni è consentito, ma non fa altro che causare confusione. Il comitato standard potrebbe voler disabilitare tale flessibilità, altrimenti perché non utilizziamo solo static_cast?

     std::forward<int>(1);
     std::forward<std::string>("Hello");

A mio avviso, spostare e andare avanti sono modelli di progettazione che sono risultati naturali dopo l'introduzione del tipo di riferimento del valore r. Non dovremmo nominare un metodo supponendo che sia usato correttamente a meno che non sia vietato un uso errato.


Non penso che il comitato C ++ abbia l'impressione che spetti a loro usare gli idiomi del linguaggio "correttamente", né definire quale sia l 'uso "corretto" (sebbene possano certamente fornire delle linee guida). A tal fine, mentre gli insegnanti, i capi e gli amici di una persona possono avere il dovere di guidarli in un modo o nell'altro, credo che il comitato C ++ (e quindi lo standard) non abbia quel dovere.
SirGuy,

Sì, ho appena letto N2951 e sono d'accordo che il comitato standard non ha l'obbligo di aggiungere limitazioni non necessarie per quanto riguarda l'uso di una funzione. Ma i nomi di questi due modelli di funzione (sposta e avanti) sono in effetti un po 'confusi nel vedere solo le loro definizioni nel file della libreria o nella documentazione standard (23.2.5 Forward / move helper). Gli esempi nella norma aiutano sicuramente a comprendere il concetto, ma potrebbe essere utile aggiungere ulteriori osservazioni per rendere le cose un po 'più chiare.
Colin,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.