Para entender por qué este es un buen patrón, debemos examinar las alternativas, tanto en C ++ 03 como en C ++ 11.
Tenemos el método C ++ 03 de tomar un std::string const&
:
struct S
{
std::string data;
S(std::string const& str) : data(str)
{}
};
en este caso, siempre se realizará una única copia. Si construye a partir de una cadena C sin procesar, std::string
se construirá a, luego se copiará nuevamente: dos asignaciones.
Existe el método C ++ 03 de tomar una referencia a a std::string
y luego cambiarla a local std::string
:
struct S
{
std::string data;
S(std::string& str)
{
std::swap(data, str);
}
};
esa es la versión C ++ 03 de "semántica de movimiento", y a swap
menudo se puede optimizar para que sea muy barata de hacer (como a move
). También debe analizarse en contexto:
S tmp("foo"); // illegal
std::string s("foo");
S tmp2(s); // legal
y te obliga a formar una no temporal std::string
, luego descartarla. (Un temporal std::string
no se puede vincular a una referencia no constante). Sin embargo, solo se realiza una asignación. La versión de C ++ 11 tomaría un &&
y requeriría que lo llamaras con std::move
, o con un temporal: esto requiere que el llamador cree explícitamente una copia fuera de la llamada y mueva esa copia a la función o constructor.
struct S
{
std::string data;
S(std::string&& str): data(std::move(str))
{}
};
Utilizar:
S tmp("foo"); // legal
std::string s("foo");
S tmp2(std::move(s)); // legal
A continuación, podemos hacer la versión completa de C ++ 11, que admite tanto la copia como 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
};
Luego podemos examinar cómo se usa esto:
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
Está bastante claro que esta técnica de sobrecarga 2 es al menos tan eficiente, si no más, que los dos estilos de C ++ 03 anteriores. Llamaré a esta versión de 2 sobrecargas la versión "más óptima".
Ahora, examinaremos la versión de tomar por copia:
struct S2 {
std::string data;
S2( std::string arg ):data(std::move(x)) {}
};
en cada uno de esos escenarios:
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
Si compara este lado a lado con la versión "más óptima", ¡hacemos exactamente uno adicional move
! Ni una sola vez hacemos un extra copy
.
Entonces, si asumimos que move
es barata, esta versión nos ofrece casi el mismo rendimiento que la versión más óptima, pero 2 veces menos código.
Y si está tomando, digamos, de 2 a 10 argumentos, la reducción en el código es exponencial: 2x veces menos con 1 argumento, 4x con 2, 8x con 3, 16x con 4, 1024x con 10 argumentos.
Ahora, podemos evitar esto a través del reenvío perfecto y SFINAE, lo que le permite escribir un solo constructor o plantilla de función que toma 10 argumentos, hace SFINAE para asegurarse de que los argumentos sean de los tipos apropiados y luego los mueve o copia en el estado local según sea necesario. Si bien esto evita el problema del aumento de mil veces en el tamaño del programa, todavía puede haber una gran cantidad de funciones generadas a partir de esta plantilla. (las instancias de funciones de plantilla generan funciones)
Y muchas funciones generadas significan un tamaño de código ejecutable más grande, lo que en sí mismo puede reducir el rendimiento.
Por el costo de unos pocos move
segundos, obtenemos un código más corto y casi el mismo rendimiento y, a menudo, un código más fácil de entender.
Ahora, esto solo funciona porque sabemos, cuando se llama a la función (en este caso, un constructor), que querremos una copia local de ese argumento. La idea es que si sabemos que vamos a hacer una copia, debemos informar a la persona que llama que estamos haciendo una copia poniéndola en nuestra lista de argumentos. Luego pueden optimizar en torno al hecho de que nos van a dar una copia (moviéndose a nuestro argumento, por ejemplo).
Otra ventaja de la técnica 'tomar por valor' es que a menudo los constructores de movimiento no son excepto. Eso significa que las funciones que toman por valor y se mueven fuera de su argumento a menudo pueden ser no excepto, moviendo cualquier throw
s fuera de su cuerpo y dentro del alcance de llamada. (quién puede evitarlo a través de la construcción directa a veces, o construir los elementos y move
en el argumento, para controlar dónde ocurre el lanzamiento) Hacer que los métodos no arrojen a menudo vale la pena.