Eu vi um código em algum lugar no qual alguém decidiu copiar um objeto e, posteriormente, movê-lo para um membro de dados de uma classe. Isso me deixou confuso, pois pensei que o objetivo de mover era evitar a cópia. Aqui está o exemplo:
struct S
{
S(std::string str) : data(std::move(str))
{}
};
Aqui estão minhas perguntas:
- Por que não estamos tomando uma referência a rvalue
str
? - Uma cópia não será cara, especialmente com algo parecido
std::string
? - Qual seria a razão para o autor decidir fazer uma cópia e depois uma mudança?
- Quando devo fazer isso sozinho?
c++
c++11
move-semantics
user2030677
fonte
fonte
Respostas:
Antes de responder às suas perguntas, uma coisa parece que você está se enganando: tomar por valor em C ++ 11 nem sempre significa copiar. Se um rvalue for passado, ele será movido (desde que exista um construtor de movimento viável) em vez de ser copiado. E
std::string
tem um construtor de movimento.Ao contrário do C ++ 03, no C ++ 11 muitas vezes é idiomático tomar parâmetros por valor, pelos motivos que explicarei a seguir. Veja também estas perguntas e respostas no StackOverflow para um conjunto mais geral de diretrizes sobre como aceitar parâmetros.
Porque isso tornaria impossível passar lvalores, como em:
Se
S
apenas tivesse um construtor que aceita rvalues, o acima não seria compilado.Se você passar um rvalue, isso será movido para
str
e, eventualmente, será movido paradata
. Nenhuma cópia será executada. Se você passar um lvalue, por outro lado, esse lvalue será copiado parastr
e depois movido paradata
.Então, para resumir, dois movimentos para rvalues, uma cópia e um movimento para lvalues.
Em primeiro lugar, como mencionei acima, o primeiro nem sempre é uma cópia; Dito isto, a resposta é: " Porque é eficiente (movimentos de
std::string
objetos são baratos) e simples ".Partindo do pressuposto de que os movimentos são baratos (ignorando o SSO aqui), eles podem ser praticamente desconsiderados ao considerar a eficiência geral desse design. Se fizermos isso, teremos uma cópia para lvalues (como teríamos se aceitássemos uma referência lvalue para
const
) e nenhuma cópia para rvalues (embora ainda teríamos uma cópia se aceitássemos uma referência lvalue paraconst
).Isso significa que tomar por valor é tão bom quanto tomar por referência lvalue para
const
quando lvalues são fornecidos, e melhor quando rvalues são fornecidos.PS: Para fornecer um contexto, acredito que este é o Q&A ao qual o OP se refere.
fonte
const T&
passagem de argumento: no pior caso (lvalue) é o mesmo, mas no caso de um temporário você só precisa mover o temporário. Vencer / Vencer.data
membro)? Você teria uma cópia mesmo se pegasse por referência de valor paraconst
data
!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&
: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 localstd::string
: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 amove
). Também deve ser analisado no contexto:e o força a formar um não temporário e
std::string
, em seguida, descarte-o. (Um temporáriostd::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 comstd::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.Usar:
Em seguida, podemos fazer a versão C ++ 11 completa, que suporta cópia e
move
:Podemos então examinar como isso é usado:
É 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:
em cada um desses cenários:
Se você comparar isso lado a lado com a versão "ideal", faremos exatamente mais uma
move
! Nenhuma vez fazemos um extracopy
.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 emove
dentro do argumento, para controlar onde o lançamento acontece.) Fazer métodos não-lançados geralmente vale a pena.fonte
noexcept
. Pegando dados por cópia, você pode fazer sua funçãonoexcept
, e fazer com que qualquer construção de cópia que possa causar potenciais (como falta de memória) ocorra fora de sua invocação de função.Isso provavelmente é intencional e é semelhante ao idioma de cópia e troca . Basicamente, como a string é copiada antes do construtor, o próprio construtor é seguro contra exceções, pois apenas troca (move) a string temporária str.
fonte
Você não quer se repetir escrevendo um construtor para o movimento e outro para a cópia:
Este é um código padronizado, especialmente se você tiver vários argumentos. Sua solução evita essa duplicação ao custo de uma mudança desnecessária. (A operação de movimentação deve ser bastante barata, no entanto.)
O idioma concorrente é usar o encaminhamento perfeito:
A mágica do modelo escolherá mover ou copiar dependendo do parâmetro que você passar. Basicamente, ela se expande para a primeira versão, onde ambos os construtores foram escritos à mão. Para obter informações básicas, consulte a postagem de Scott Meyer em referências universais .
Do ponto de vista de desempenho, a versão de encaminhamento perfeita é superior à sua versão, pois evita os movimentos desnecessários. No entanto, pode-se argumentar que sua versão é mais fácil de ler e escrever. O possível impacto no desempenho não deve importar na maioria das situações, de qualquer maneira, então parece ser uma questão de estilo no final.
fonte