Para entender por que esse é um bom padrão, devemos examinar as alternativas, tanto em C ++ 03 quanto em C ++ 11.
Temos o método C ++ 03 para obter std::string const&
:
struct S
{
std::string data;
S(std::string const& str) : data(str)
{}
};
neste caso, sempre haverá uma única cópia realizada. Se você construir a partir de uma string C bruta, a std::string
será construído e copiado novamente: duas alocações.
Existe o método C ++ 03 de tomar uma referência a um e std::string
, em seguida, trocá-lo por um local std::string
:
struct S
{
std::string data;
S(std::string& str)
{
std::swap(data, str);
}
};
essa é a versão C ++ 03 de "semântica de movimentação", e swap
muitas vezes pode ser otimizada para ser muito barata de fazer (bem como a move
). Também deve ser analisado no contexto:
S tmp("foo"); // illegal
std::string s("foo");
S tmp2(s); // legal
e o força a formar um não temporário e std::string
, em seguida, descarte-o. (Um temporário std::string
não pode ser vinculado a uma referência não const). No entanto, apenas uma alocação é feita. A versão C ++ 11 tomaria um &&
e exigiria que você o chamasse com std::move
, ou com um temporário: isso exige que o chamador crie explicitamente uma cópia fora da chamada e mova essa cópia para a função ou construtor.
struct S
{
std::string data;
S(std::string&& str): data(std::move(str))
{}
};
Usar:
S tmp("foo"); // legal
std::string s("foo");
S tmp2(std::move(s)); // legal
Em seguida, podemos fazer a versão C ++ 11 completa, que suporta cópia e 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
};
Podemos então examinar como isso é usado:
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
É bastante claro que esta 2 técnica de sobrecarga é pelo menos tão eficiente, senão mais, do que os dois estilos C ++ 03 acima. Vou apelidar esta versão de 2 sobrecargas de versão "ideal".
Agora, examinaremos a versão tomada por cópia:
struct S2 {
std::string data;
S2( std::string arg ):data(std::move(x)) {}
};
em cada um desses cenários:
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 você comparar isso lado a lado com a versão "ideal", faremos exatamente mais uma move
! Nenhuma vez fazemos um extra copy
.
Portanto, se assumirmos que move
é barato, essa versão nos oferece quase o mesmo desempenho que a versão ideal, mas 2 vezes menos código.
E se você estiver tomando, digamos, 2 a 10 argumentos, a redução no código é exponencial - 2x vezes menos com 1 argumento, 4x com 2, 8x com 3, 16x com 4, 1024x com 10 argumentos.
Agora, podemos contornar isso por meio de encaminhamento perfeito e SFINAE, permitindo que você escreva um único construtor ou modelo de função que receba 10 argumentos, faça SFINAE para garantir que os argumentos sejam de tipos apropriados e, em seguida, os mova ou copie para o estado local conforme necessário. Embora isso evite o problema de aumento de mil vezes no tamanho do programa, ainda pode haver uma pilha inteira de funções geradas a partir desse modelo. (as instâncias de função de modelo geram funções)
E muitas funções geradas significam um tamanho de código executável maior, o que pode reduzir o desempenho.
Pelo custo de alguns move
segundos, obtemos um código mais curto e quase o mesmo desempenho, e geralmente mais fácil de entender o código.
Agora, isso só funciona porque sabemos, quando a função (neste caso, um construtor) é chamada, que queremos uma cópia local desse argumento. A ideia é que, se sabemos que faremos uma cópia, devemos informar ao chamador que estamos fazendo uma cópia, colocando-a em nossa lista de argumentos. Eles podem, então, otimizar em torno do fato de que vão nos dar uma cópia (passando para o nosso argumento, por exemplo).
Outra vantagem da técnica de "tomar por valor" é que muitas vezes os construtores de movimento são noexcept. Isso significa que as funções que tomam por valor e saem de seu argumento podem frequentemente ser noexcept, movendo qualquer throw
s para fora de seu corpo e para o escopo de chamada (que pode evitá-lo por meio de construção direta às vezes, ou construir os itens e move
dentro do argumento, para controlar onde o lançamento acontece.) Fazer métodos não-lançados geralmente vale a pena.