Estou tentando entender as referências rvalue e mover a semântica do C ++ 11.
Qual é a diferença entre esses exemplos e qual deles não fará cópia vetorial?
Primeiro exemplo
std::vector<int> return_vector(void)
{
std::vector<int> tmp {1,2,3,4,5};
return tmp;
}
std::vector<int> &&rval_ref = return_vector();
Segundo exemplo
std::vector<int>&& return_vector(void)
{
std::vector<int> tmp {1,2,3,4,5};
return std::move(tmp);
}
std::vector<int> &&rval_ref = return_vector();
Terceiro exemplo
std::vector<int> return_vector(void)
{
std::vector<int> tmp {1,2,3,4,5};
return std::move(tmp);
}
std::vector<int> &&rval_ref = return_vector();
c++
c++11
move-semantics
rvalue-reference
c++-faq
Tarântula
fonte
fonte
std::move()
criou uma "cópia" persistente.std::move(expression)
não cria nada, simplesmente converte a expressão em um xvalue. Nenhum objeto é copiado ou movido no processo de avaliaçãostd::move(expression)
.Respostas:
Primeiro exemplo
O primeiro exemplo retorna um temporário que é capturado por
rval_ref
. Esse temporário terá sua vida estendida além darval_ref
definição e você pode usá-lo como se o tivesse captado por valor. Isso é muito semelhante ao seguinte:exceto que, na minha reescrita, você obviamente não pode usar de
rval_ref
maneira não-constante.Segundo exemplo
No segundo exemplo, você criou um erro em tempo de execução.
rval_ref
agora mantém uma referência aos destruídostmp
dentro da função. Com alguma sorte, esse código falharia imediatamente.Terceiro exemplo
Seu terceiro exemplo é aproximadamente equivalente ao seu primeiro. O
std::move
ontmp
é desnecessário e pode realmente ser uma pessimização de desempenho, pois inibirá a otimização do valor de retorno.A melhor maneira de codificar o que você está fazendo é:
Melhor prática
Ou seja, como você faria em C ++ 03.
tmp
é implicitamente tratado como um rvalue na declaração de retorno. Ele será retornado via otimização do valor de retorno (sem cópia, sem movimentação) ou se o compilador decidir que não pode executar o RVO, utilizará o construtor de movimentação do vetor para fazer o retorno . Somente se o RVO não for executado, e se o tipo retornado não tiver um construtor de movimentação, o construtor de cópia será usado para o retorno.fonte
return my_local;
. Várias declarações de retorno estão ok e não inibem o RVO.move
não cria um temporário. Ele lança um valor l para um valor x, não fazendo cópias, criando nada, destruindo nada. Esse exemplo é exatamente a mesma situação que você retornou por lvalue-reference e removeu amove
linha de retorno: De qualquer maneira, você tem uma referência pendente para uma variável local dentro da função e que foi destruída.Nenhum deles será copiado, mas o segundo se referirá a um vetor destruído. Referências rvalues nomeadas quase nunca existem no código regular. Você escreve exatamente como teria escrito uma cópia em C ++ 03.
Exceto agora, o vetor é movido. O usuário de uma classe não lida com suas referências de valor na grande maioria dos casos.
fonte
tmp
não é movido para dentrorval_ref
, mas gravado diretamente norval_ref
uso do RVO (ou seja, cópia elision). Há uma distinção entrestd::move
e cópia elision. Astd::move
ainda pode envolver alguns dados a serem copiados; no caso de um vetor, um novo vetor é realmente construído no construtor de cópia e os dados são alocados, mas a maior parte da matriz de dados é copiada apenas pela cópia do ponteiro (essencialmente). A cópia elision evita 100% de todas as cópias.rval_ref
variáveis são construídos usando o construtor move destd::vector
. Não há nenhum construtor de cópias envolvido com / semstd::move
.tmp
é tratado como um rvalue nareturn
instrução nesse caso.A resposta simples é que você deve escrever um código para as referências rvalue, como faria com o código de referência regular, e tratá-las mentalmente 99% do tempo. Isso inclui todas as regras antigas sobre o retorno de referências (ou seja, nunca retorne uma referência a uma variável local).
A menos que você esteja escrevendo uma classe de contêiner de modelo que precise tirar proveito do std :: forward e possa escrever uma função genérica que use referências lvalue ou rvalue, isso é mais ou menos verdadeiro.
Uma das grandes vantagens para o construtor de movimento e a atribuição de movimento é que, se você os definir, o compilador poderá usá-los nos casos em que o RVO (otimização do valor de retorno) e o NRVO (otimização do valor de retorno) não serão chamados. Isso é muito grande para devolver objetos caros, como contêineres e strings, pelo valor de forma eficiente dos métodos.
Agora, onde as coisas ficam interessantes com as referências rvalue, é que você também pode usá-las como argumentos para funções normais. Isso permite que você escreva contêineres com sobrecargas para referência const (const foo & other) e rvalue reference (foo && other). Mesmo que o argumento seja muito difícil de passar com uma simples chamada de construtor, ele ainda pode ser feito:
Os contêineres STL foram atualizados para sobrecargas de movimentação para quase qualquer coisa (chave e valores de hash, inserção de vetor etc.) e é onde você os vê mais.
Você também pode usá-los para funções normais e, se você fornecer apenas um argumento de referência rvalue, poderá forçar o chamador a criar o objeto e permitir que a função faça a movimentação. Isso é mais um exemplo do que um uso realmente bom, mas, na minha biblioteca de renderização, atribui uma string a todos os recursos carregados, para que seja mais fácil ver o que cada objeto representa no depurador. A interface é algo como isto:
É uma forma de "abstração com vazamento", mas me permite aproveitar o fato de que eu já tinha criado a string na maior parte do tempo e evitar fazer mais uma cópia dela. Este não é exatamente um código de alto desempenho, mas é um bom exemplo das possibilidades de as pessoas entenderem esse recurso. Esse código realmente exige que a variável seja temporária para a chamada ou std :: move invocada:
ou
ou
mas isso não será compilado!
fonte
Não é uma resposta em si , mas uma orientação. Na maioria das vezes, não há muito sentido em declarar
T&&
variável local (como você fez comstd::vector<int>&& rval_ref
). Você ainda precisarástd::move()
deles para usar nosfoo(T&&)
métodos de tipo. Também existe o problema que já foi mencionado: quando você tenta retornar talrval_ref
função, você obtém o fiasco de referência padrão ao fiasco temporário destruído.Na maioria das vezes, eu seguia o seguinte padrão:
Você não mantém nenhuma referência a objetos temporários retornados, assim evita o erro do programador (inexperiente) que deseja usar um objeto movido.
Obviamente, existem casos (embora bastante raros) em que uma função realmente retorna um
T&&
que é uma referência a um objeto não temporário que você pode mover para o seu objeto.Com relação ao RVO: esses mecanismos geralmente funcionam e o compilador pode evitar a cópia, mas nos casos em que o caminho de retorno não é óbvio (exceções,
if
condicionais determinando o objeto nomeado que você retornará e provavelmente outros) rrefs são seus salvadores (mesmo que potencialmente mais caro).fonte
Nenhum deles fará cópias extras. Mesmo que o RVO não seja usado, o novo padrão diz que é preferível copiar a construção de movimento ao fazer retornos, acredito.
Eu acredito que seu segundo exemplo causa comportamento indefinido porque você está retornando uma referência a uma variável local.
fonte
Como já mencionado nos comentários da primeira resposta, o
return std::move(...);
construto pode fazer a diferença em casos que não sejam o retorno de variáveis locais. Aqui está um exemplo executável que documenta o que acontece quando você retorna um objeto de membro com e semstd::move()
:Presumivelmente,
return std::move(some_member);
só faz sentido se você realmente deseja mover um membro da classe específico, por exemplo, em um caso em queclass C
represente objetos adaptadores de vida curta com o único objetivo de criar instâncias destruct A
.Observe como
struct A
sempre fica copiado fora declass B
, mesmo quando oclass B
objeto é um valor-R. Isso ocorre porque o compilador não tem como dizer que essaclass B
instânciastruct A
não será mais usada. Emclass C
, o compilador tem esta informação a partirstd::move()
, que é por isso questruct A
se moveu , a menos que a instância doclass C
é constante.fonte