Per capire perché questo è un buon modello, dovremmo esaminare le alternative, sia in C ++ 03 che in C ++ 11.
Abbiamo il metodo C ++ 03 per prendere un std::string const&
:
struct S
{
std::string data;
S(std::string const& str) : data(str)
{}
};
in questo caso verrà eseguita sempre una sola copia. Se costruisci da una stringa C non elaborata, std::string
verrà costruito a, quindi copiato di nuovo: due allocazioni.
Esiste il metodo C ++ 03 per prendere un riferimento a a std::string
, quindi scambiarlo in un locale std::string
:
struct S
{
std::string data;
S(std::string& str)
{
std::swap(data, str);
}
};
questa è la versione C ++ 03 di "sposta semantica", e swap
spesso può essere ottimizzata per essere molto economica (molto simile a a move
). Dovrebbe anche essere analizzato nel contesto:
S tmp("foo"); // illegal
std::string s("foo");
S tmp2(s); // legal
e ti costringe a formare un non temporaneo std::string
, quindi scartarlo. (Un temporaneo std::string
non può legarsi a un riferimento non const). Tuttavia, viene eseguita una sola assegnazione. La versione C ++ 11 richiederebbe un &&
e richiederebbe di chiamarlo con std::move
, o con un temporaneo: questo richiede che il chiamante crei esplicitamente una copia al di fuori della chiamata e sposti quella copia nella funzione o nel costruttore.
struct S
{
std::string data;
S(std::string&& str): data(std::move(str))
{}
};
Uso:
S tmp("foo"); // legal
std::string s("foo");
S tmp2(std::move(s)); // legal
Successivamente, possiamo fare la versione completa C ++ 11, che supporta sia la copia che move
:
struct S
{
std::string data;
S(std::string const& str) : data(str) {} // lvalue const, copy
S(std::string && str) : data(std::move(str)) {} // rvalue, move
};
Possiamo quindi esaminare come viene utilizzato:
S tmp( "foo" ); // a temporary `std::string` is created, then moved into tmp.data
std::string bar("bar"); // bar is created
S tmp2( bar ); // bar is copied into tmp.data
std::string bar2("bar2"); // bar2 is created
S tmp3( std::move(bar2) ); // bar2 is moved into tmp.data
È abbastanza chiaro che questa tecnica di sovraccarico 2 è almeno altrettanto efficiente, se non di più, dei due stili C ++ 03 sopra. Denominerò questa versione con 2 sovraccarichi la versione "più ottimale".
Ora esamineremo la versione take-by-copy:
struct S2 {
std::string data;
S2( std::string arg ):data(std::move(x)) {}
};
in ciascuno di questi scenari:
S2 tmp( "foo" ); // a temporary `std::string` is created, moved into arg, then moved into S2::data
std::string bar("bar"); // bar is created
S2 tmp2( bar ); // bar is copied into arg, then moved into S2::data
std::string bar2("bar2"); // bar2 is created
S2 tmp3( std::move(bar2) ); // bar2 is moved into arg, then moved into S2::data
Se confronti questa versione fianco a fianco con la versione "ottimale", ne facciamo esattamente un'altra move
! Non una volta facciamo un extra copy
.
Quindi, se assumiamo che move
sia economico, questa versione ci offre quasi le stesse prestazioni della versione più ottimale, ma 2 volte meno codice.
E se prendi da 2 a 10 argomenti, la riduzione del codice è esponenziale - 2x volte inferiore con 1 argomento, 4x con 2, 8x con 3, 16x con 4, 1024x con 10 argomenti.
Ora, possiamo aggirare questo problema tramite un inoltro perfetto e SFINAE, che consente di scrivere un singolo costruttore o modello di funzione che richiede 10 argomenti, esegue SFINAE per garantire che gli argomenti siano di tipi appropriati e quindi li sposta o li copia nel stato locale come richiesto. Sebbene ciò prevenga l'aumento di mille volte della dimensione del programma, può esserci ancora un intero mucchio di funzioni generate da questo modello. (le istanze di funzioni modello generano funzioni)
E molte funzioni generate significano dimensioni del codice eseguibile più grandi, che possono a sua volta ridurre le prestazioni.
Per il costo di pochi move
secondi, otteniamo codice più breve e quasi le stesse prestazioni, e spesso codice più facile da capire.
Ora, questo funziona solo perché sappiamo, quando viene chiamata la funzione (in questo caso, un costruttore), che avremo bisogno di una copia locale di quell'argomento. L'idea è che se sappiamo che ne faremo una copia, dovremmo far sapere al chiamante che stiamo facendo una copia inserendola nel nostro elenco di argomenti. Possono quindi ottimizzare il fatto che stanno per darcene una copia (entrando nel nostro argomento, per esempio).
Un altro vantaggio della tecnica "prendi per valore" è che spesso i costruttori di spostamento non fanno eccezione. Ciò significa che le funzioni che prendono per valore e si spostano fuori dal loro argomento spesso non possono essere eccetto, spostando qualsiasi throw
s fuori dal loro corpo e nello scope chiamante (chi può evitarlo tramite la costruzione diretta a volte, o costruire gli oggetti e move
nell'argomento, per controllare dove avviene il lancio) .Rendere i metodi nothrow spesso vale la pena.