Acabei de ouvir a entrevista de podcast de rádio da Engenharia de Software com Scott Meyers sobre C ++ 0x . A maioria dos novos recursos fazia sentido para mim, e estou realmente empolgado com o C ++ 0x agora, com exceção de um. Ainda não entendo a semântica de movimentos ... O que é exatamente?
c++
c++-faq
c++11
move-semantics
dicroce
fonte
fonte
Respostas:
Acho mais fácil entender a semântica de movimentação com código de exemplo. Vamos começar com uma classe de seqüência de caracteres muito simples que contém apenas um ponteiro para um bloco de memória alocado por heap:
Como escolhemos gerenciar a memória por nós mesmos, precisamos seguir a regra dos três . Vou adiar a gravação do operador de atribuição e implementar apenas o destruidor e o construtor de cópia por enquanto:
O construtor de cópia define o que significa copiar objetos de sequência. O parâmetro
const string& that
liga-se a todas as expressões do tipo string que permitem fazer cópias nos seguintes exemplos:Agora vem a principal visão sobre a semântica de movimentos. Observe que somente na primeira linha em que copiamos
x
essa cópia profunda é realmente necessária, pois podemos querer inspecionarx
mais tarde e ficaríamos muito surpresos sex
tivessem mudado de alguma forma. Você notou como eu dissex
três vezes (quatro vezes, se você incluir esta frase) e sempre quis dizer exatamente o mesmo objeto ? Chamamos expressões comox
"lvalues".Os argumentos nas linhas 2 e 3 não são lvalues, mas rvalues, porque os objetos de cadeia subjacentes não têm nomes; portanto, o cliente não tem como inspecioná-los novamente posteriormente. rvalues denota objetos temporários que são destruídos no ponto e vírgula seguinte (para ser mais preciso: no final da expressão completa que contém lexicamente o rvalue). Isso é importante porque durante a inicialização de
b
ec
, poderíamos fazer o que quiséssemos com a cadeia de origem, e o cliente não poderia dizer a diferença !O C ++ 0x introduz um novo mecanismo chamado "referência de rvalue" que, entre outras coisas, nos permite detectar argumentos de rvalue por sobrecarga de função. Tudo o que precisamos fazer é escrever um construtor com um parâmetro de referência rvalue. Dentro desse construtor, podemos fazer o que quisermos com a fonte, desde que a deixemos em algum estado válido:
O que fizemos aqui? Em vez de copiar profundamente os dados da pilha, apenas copiamos o ponteiro e, em seguida, definimos o ponteiro original como nulo (para impedir que 'delete []' do destruidor do objeto de origem libere nossos 'dados apenas roubados'). Com efeito, "roubamos" os dados que originalmente pertenciam à string de origem. Novamente, o principal insight é que, sob nenhuma circunstância, o cliente pode detectar que a fonte foi modificada. Como realmente não fazemos uma cópia aqui, chamamos esse construtor de "construtor de movimentação". Seu trabalho é mover recursos de um objeto para outro em vez de copiá-los.
Parabéns, agora você entende o básico da semântica de movimentos! Vamos continuar implementando o operador de atribuição. Se você não estiver familiarizado com o idioma de copiar e trocar , aprenda-o e volte, pois é um idioma incrível do C ++ relacionado à segurança de exceções.
Huh, é isso? "Onde está a referência de valor?" você pode perguntar. "Nós não precisamos disso aqui!" é a minha resposta :)
Observe que passamos o parâmetro
that
por valor , portanto,that
ele deve ser inicializado como qualquer outro objeto de string. Exatamente comothat
será inicializado? Nos velhos tempos do C ++ 98 , a resposta teria sido "pelo construtor de cópias". No C ++ 0x, o compilador escolhe entre o construtor de cópia e o construtor de movimentação, com base no argumento para o operador de atribuição ser um lvalue ou um rvalue.Portanto, se você disser
a = b
, o construtor de cópia será inicializadothat
(porque a expressãob
é um valor l) e o operador de atribuição troca o conteúdo por uma cópia profunda recém-criada. Essa é a própria definição da expressão copiar e trocar - faça uma cópia, troque o conteúdo pela cópia e depois se livre da cópia, deixando o escopo. Nada de novo aqui.Mas se você disser
a = x + y
, o construtor da movimentação será inicializadothat
(porque a expressãox + y
é um rvalor), portanto, não há cópia profunda envolvida, apenas uma movimentação eficiente.that
ainda é um objeto independente do argumento, mas sua construção foi trivial, pois os dados do heap não precisaram ser copiados, apenas movidos. Não foi necessário copiá-lo porquex + y
é um rvalue e, novamente, não há problema em mudar de objetos de string indicados por rvalues.Para resumir, o construtor de cópias faz uma cópia profunda, porque a fonte deve permanecer intocada. O construtor de movimentação, por outro lado, pode simplesmente copiar o ponteiro e, em seguida, definir o ponteiro na fonte como nulo. Não há problema em "anular" o objeto de origem dessa maneira, porque o cliente não tem como inspecionar o objeto novamente.
Espero que este exemplo tenha entendido o ponto principal. Há muito mais para avaliar as referências e mover a semântica que deixei intencionalmente para simplificar. Se você quiser mais detalhes, consulte minha resposta complementar .
fonte
that.data = 0
, os personagens seriam destruídos muito cedo (quando o temporário morrer), e também duas vezes. Você quer roubar os dados, não compartilhá-los!delete[]
em um nullptr é definido pelo padrão C ++ como não operacional.Minha primeira resposta foi uma introdução extremamente simplificada para mover a semântica, e muitos detalhes foram deixados de propósito para mantê-la simples. No entanto, há muito mais para mover a semântica, e pensei que era hora de uma segunda resposta para preencher as lacunas. A primeira resposta já é bastante antiga e não parecia correto substituí-la por um texto completamente diferente. Eu acho que ainda serve bem como uma primeira introdução. Mas se você quiser aprofundar, continue lendo :)
Stephan T. Lavavej dedicou um tempo para fornecer feedback valioso. Muito obrigado, Stephan!
Introdução
A semântica de movimentação permite que um objeto, sob certas condições, aproprie-se dos recursos externos de algum outro objeto. Isso é importante de duas maneiras:
Transformando cópias caras em movimentos baratos. Veja minha primeira resposta para um exemplo. Observe que, se um objeto não gerenciar pelo menos um recurso externo (direta ou indiretamente através de seus objetos membros), a semântica de movimentação não oferecerá nenhuma vantagem sobre a semântica de cópia. Nesse caso, copiar e mover um objeto significa exatamente a mesma coisa:
Implementando tipos seguros de "somente movimento"; isto é, tipos para os quais copiar não faz sentido, mas mover faz. Os exemplos incluem bloqueios, identificadores de arquivo e ponteiros inteligentes com semântica de propriedade exclusiva. Nota: Esta resposta discute
std::auto_ptr
um modelo de biblioteca padrão C ++ 98 descontinuado, que foi substituído porstd::unique_ptr
C ++ 11. Os programadores intermediários de C ++ provavelmente estão pelo menos um pouco familiarizados estd::auto_ptr
, devido à "semântica de movimento" exibida, parece um bom ponto de partida para discutir a semântica de movimento no C ++ 11. YMMV.O que é uma jogada?
A biblioteca padrão C ++ 98 oferece um ponteiro inteligente com semântica de propriedade exclusiva chamada
std::auto_ptr<T>
. Caso você não esteja familiarizadoauto_ptr
, seu objetivo é garantir que um objeto alocado dinamicamente seja sempre liberado, mesmo diante de exceções:O que
auto_ptr
é incomum é o seu comportamento de "cópia":Note como a inicialização
b
coma
que não copiar o triângulo, mas em vez transfere a propriedade do triângulo a partira
deb
. Também dizemos "a
é movido parab
" ou "o triângulo é movido dea
parab
". Isso pode parecer confuso, porque o próprio triângulo sempre permanece no mesmo local da memória.O construtor de cópia de
auto_ptr
provavelmente se parece com isso (um pouco simplificado):Movimentos perigosos e inofensivos
O mais perigoso
auto_ptr
é que o que sintaticamente se parece com uma cópia é realmente uma mudança. Tentar chamar uma função de membro em uma mudança deauto_ptr
invocará um comportamento indefinido, portanto, você deve ter muito cuidado para não usar umaauto_ptr
após a mudança de:Mas
auto_ptr
nem sempre é perigoso. As funções de fábrica são um caso de uso perfeito paraauto_ptr
:Observe como os dois exemplos seguem o mesmo padrão sintático:
E, no entanto, um deles invoca um comportamento indefinido, enquanto o outro não. Então, qual é a diferença entre as expressões
a
emake_triangle()
? Eles não são do mesmo tipo? De fato são, mas têm diferentes categorias de valor .Categorias de valor
Obviamente, deve haver alguma diferença profunda entre a expressão
a
que denota umaauto_ptr
variável e a expressãomake_triangle()
que denota a chamada de uma função que retorna umauto_ptr
valor por, criando assim um novoauto_ptr
objeto temporário toda vez que é chamado.a
é um exemplo de um lvalue , enquanto quemake_triangle()
é um exemplo de um rvalue .Passar de lvalues como
a
é perigoso, porque mais tarde poderíamos tentar chamar uma função de membroa
, invocando um comportamento indefinido. Por outro lado, passar de rvalores comomake_triangle()
é perfeitamente seguro, porque depois que o construtor de cópias fez seu trabalho, não podemos usar o temporário novamente. Não há expressão que denuncie o referido temporário; se simplesmente escrevermosmake_triangle()
novamente, obteremos um temporário diferente . De fato, o temporário movido de já foi para a próxima linha:Observe que as letras
l
er
têm uma origem histórica no lado esquerdo e no lado direito de uma tarefa. Isso não é mais verdade no C ++, porque existem lvalues que não podem aparecer no lado esquerdo de uma atribuição (como matrizes ou tipos definidos pelo usuário sem um operador de atribuição) e existem rvalues que podem (todos os rvalues dos tipos de classe com um operador de atribuição).Referências de valor
Agora entendemos que mudar de valores é potencialmente perigoso, mas mudar de valores é inofensivo. Se o C ++ tivesse suporte de linguagem para distinguir argumentos de lvalue de argumentos de rvalue, poderíamos proibir completamente a mudança de lvalues ou, pelo menos, explicitar a mudança de lvalues no site da chamada, para que não movamos mais por acidente.
A resposta do C ++ 11 para esse problema são as referências de rvalue . Uma referência rvalue é um novo tipo de referência que se liga apenas a rvalues, e a sintaxe é
X&&
. A boa e antiga referênciaX&
agora é conhecida como referência lvalue . (Observe que nãoX&&
é uma referência a uma referência; não existe tal coisa em C ++.)Se jogarmos
const
na mistura, já temos quatro tipos diferentes de referências. A que tipos de expressões do tipoX
eles podem se ligar?Na prática, você pode esquecer
const X&&
. Ser restrito à leitura de rvalues não é muito útil.Conversões implícitas
As referências de valor passaram por várias versões. Desde a versão 2.1, uma referência rvalue
X&&
também se liga a todas as categorias de valor de um tipo diferenteY
, desde que haja uma conversão implícita deY
paraX
. Nesse caso, um temporário do tipoX
é criado e a referência rvalue é vinculada a esse temporário:No exemplo acima,
"hello world"
é um tipo de Ivalueconst char[12]
. Como há uma conversão implícita deconst char[12]
atéconst char*
parastd::string
, um tipo temporáriostd::string
é criado er
é ligada a esse temporária. Este é um dos casos em que a distinção entre rvalues (expressões) e temporários (objetos) é um pouco embaçada.Mover construtores
Um exemplo útil de uma função com um
X&&
parâmetro é o construtor moveX::X(X&& source)
. Seu objetivo é transferir a propriedade do recurso gerenciado da origem para o objeto atual.No C ++ 11,
std::auto_ptr<T>
foi substituído pelostd::unique_ptr<T>
que tira proveito das referências de rvalue. Vou desenvolver e discutir uma versão simplificada dounique_ptr
. Primeiro, encapsulamos um ponteiro bruto e sobrecarregamos os operadores->
e*
, portanto, nossa classe parece um ponteiro:O construtor assume a propriedade do objeto e o destruidor o exclui:
Agora vem a parte interessante, o construtor de movimentação:
Esse construtor de movimentação faz exatamente o que o
auto_ptr
construtor de cópia fez, mas só pode ser fornecido com rvalues:A segunda linha falha ao compilar, porque
a
é um lvalue, mas o parâmetrounique_ptr&& source
só pode ser vinculado a rvalues. Isso é exatamente o que queríamos; movimentos perigosos nunca devem estar implícitos. A terceira linha compila muito bem, porquemake_triangle()
é um rvalue. O construtor de movimentação transferirá a propriedade do temporário parac
. Novamente, é exatamente isso que queríamos.Mover operadores de atribuição
A última peça que falta é o operador de atribuição de movimento. Seu trabalho é liberar o recurso antigo e adquirir o novo recurso a partir de seu argumento:
Observe como essa implementação do operador de atribuição de movimentação duplica a lógica do destruidor e do construtor de movimentação. Você está familiarizado com o idioma de copiar e trocar? Também pode ser aplicado para mover a semântica como o idioma mover-e-trocar:
Agora que
source
é uma variável do tipounique_ptr
, será inicializada pelo construtor move; isto é, o argumento será movido para o parâmetro O argumento ainda é necessário para ser um rvalue, porque o próprio construtor move possui um parâmetro de referência rvalue. Quando o fluxo de controle atinge a chave de fechamento deoperator=
,source
sai do escopo, liberando o recurso antigo automaticamente.Movendo-se de lvalues
Às vezes, queremos passar de lvalues. Ou seja, às vezes queremos que o compilador trate um lvalue como se fosse um rvalue, para que ele possa chamar o construtor move, mesmo que possa ser potencialmente inseguro. Para esse propósito, o C ++ 11 oferece um modelo de função de biblioteca padrão chamado
std::move
dentro do cabeçalho<utility>
. Esse nome é um pouco infeliz, porquestd::move
simplesmente lança um valor lvalue para um valor rvalue; ele não mover qualquer coisa por si só. Apenas permite o movimento. Talvez devesse ter sido nomeadostd::cast_to_rvalue
oustd::enable_move
, mas já estamos presos ao nome.Aqui está como você se move explicitamente de um lvalue:
Observe que após a terceira linha,
a
não possui mais um triângulo. Tudo bem, porque, ao escrever explicitamentestd::move(a)
, deixamos claras nossas intenções: "Caro construtor, faça o que quiser com aa
fim de inicializarc
; não me importoa
mais. Sinta-se à vontade para seguir em frentea
".Xvalues
Observe que, embora
std::move(a)
seja um rvalue, sua avaliação não cria um objeto temporário. Esse dilema forçou o comitê a introduzir uma terceira categoria de valor. Algo que pode ser vinculado a uma referência rvalue, mesmo que não seja um rvalue no sentido tradicional, é chamado de xvalue (valor eXpiring). Os valores tradicionais foram renomeados para valores prévios (valores puros).Prvalues e xvalues são rvalues. Xvalues e lvalues são ambos glvalues (lvalues generalizados). Os relacionamentos são mais fáceis de entender com um diagrama:
Observe que apenas xvalues são realmente novos; o resto é apenas devido à renomeação e agrupamento.
Saindo de funções
Até agora, vimos movimento em variáveis locais e em parâmetros de função. Mas o movimento também é possível na direção oposta. Se uma função retornar por valor, algum objeto no site de chamada (provavelmente uma variável local ou temporária, mas poderia ser qualquer tipo de objeto) será inicializado com a expressão após a
return
instrução como um argumento para o construtor de movimentação:Talvez surpreendentemente, objetos automáticos (variáveis locais que não são declaradas como
static
) também podem ser implicitamente removidos de funções:Como o construtor move aceita o lvalue
result
como argumento? O escopo deresult
está prestes a terminar e será destruído durante o desenrolamento da pilha. Ninguém poderia reclamar depois que issoresult
mudou de alguma maneira; quando o fluxo de controle retorna ao chamador,result
ele não existe mais! Por esse motivo, o C ++ 11 possui uma regra especial que permite retornar objetos automáticos das funções sem precisar escreverstd::move
. Na verdade, você nunca deve usarstd::move
para mover objetos automáticos para fora das funções, pois isso inibe a "otimização do valor de retorno nomeado" (NRVO).Observe que, em ambas as funções de fábrica, o tipo de retorno é um valor, não uma referência de valor nominal. As referências de valor ainda são referências e, como sempre, você nunca deve retornar uma referência a um objeto automático; o chamador acabaria com uma referência pendente se você enganasse o compilador a aceitar seu código, assim:
Mudando para membros
Mais cedo ou mais tarde, você escreverá um código como este:
Basicamente, o compilador reclamará que
parameter
é um valor l. Se você observar seu tipo, verá uma referência rvalue, mas uma referência rvalue significa simplesmente "uma referência vinculada a um rvalue"; isso não significa que a própria referência seja um rvalue! De fato,parameter
é apenas uma variável comum com um nome. Você pode usarparameter
quantas vezes quiser dentro do corpo do construtor, e sempre indica o mesmo objeto. Mover-se implicitamente seria perigoso, por isso a linguagem o proíbe.A solução é ativar manualmente a movimentação:
Você poderia argumentar que
parameter
não é mais usado após a inicialização domember
. Por que não existe uma regra especial para inserir silenciosamente,std::move
assim como nos valores de retorno? Provavelmente porque seria muito pesado para os implementadores do compilador. Por exemplo, e se o corpo do construtor estivesse em outra unidade de tradução? Por outro lado, a regra do valor de retorno simplesmente precisa verificar as tabelas de símbolos para determinar se o identificador após areturn
palavra - chave indica um objeto automático.Você também pode passar o
parameter
valor por. Para tipos somente de movimentaçãounique_ptr
, parece que ainda não existe um idioma estabelecido. Pessoalmente, prefiro passar por valor, pois causa menos confusão na interface.Funções-membro especiais
O C ++ 98 declara implicitamente três funções-membro especiais sob demanda, ou seja, quando são necessárias em algum lugar: o construtor de cópia, o operador de atribuição de cópia e o destruidor.
As referências de valor passaram por várias versões. Desde a versão 3.0, o C ++ 11 declara duas funções-membro especiais adicionais sob demanda: o construtor de movimentação e o operador de atribuição de movimentação. Observe que nem o VC10 nem o VC11 estão em conformidade com a versão 3.0, portanto, você precisará implementá-los você mesmo.
Essas duas novas funções especiais de membro são declaradas implicitamente apenas se nenhuma das funções especiais de membro for declarada manualmente. Além disso, se você declarar seu próprio construtor de movimentação ou operador de atribuição de movimentação, nem o construtor de cópia nem o operador de atribuição de cópia serão declarados implicitamente.
O que essas regras significam na prática?
Observe que o operador de atribuição de cópia e o operador de atribuição de movimentação podem ser fundidos em um único operador de atribuição unificado, assumindo seu argumento por valor:
Dessa forma, o número de funções-membro especiais a serem implementadas cai de cinco para quatro. Há uma troca entre segurança e eficiência de exceção aqui, mas não sou especialista neste assunto.
Referências de encaminhamento ( anteriormente conhecidas como referências universais )
Considere o seguinte modelo de função:
Você pode esperar
T&&
vincular apenas a rvalues, porque, à primeira vista, parece uma referência a rvalue. No entanto,T&&
também se liga a lvalues:Se o argumento é um rvalor do tipo
X
,T
é deduzido como sendoX
, portantoT&&
significaX&&
. Isto é o que alguém esperaria. Mas se o argumento for um valor l de tipoX
, devido a uma regra especial,T
for deduzido como sendoX&
, issoT&&
significaria algo parecidoX& &&
. Mas desde C ++ ainda não tem noção de referências a referências, o tipoX& &&
está em colapso emX&
. Isso pode parecer confuso e inútil no começo, mas o recolhimento de referência é essencial para o encaminhamento perfeito (que não será discutido aqui).Se você deseja restringir um modelo de função a rvalues, é possível combinar SFINAE com características de tipo:
Implementação de mudança
Agora que você entende o recolhimento de referência, eis como
std::move
é implementado:Como você pode ver,
move
aceita qualquer tipo de parâmetro graças à referência de encaminhamentoT&&
e retorna uma referência de rvalue. Astd::remove_reference<T>::type
chamada de meta-função é necessária porque, caso contrário, para lvalues do tipoX
, o tipo de retorno seria oX& &&
qual entraria em colapsoX&
. Comot
sempre é um lvalue (lembre-se de que uma referência nomeada rvalue é um lvalue), mas queremos ligart
a uma referência rvalue, precisamos converter explicitamentet
no tipo de retorno correto. A chamada de uma função que retorna uma referência rvalue é em si um xvalue. Agora você sabe de onde vêm os xvalues;)Observe que retornar por referência rvalue é bom neste exemplo, porque
t
não indica um objeto automático, mas um objeto que foi passado pelo chamador.fonte
A semântica de movimento é baseada em referências de rvalue .
Um rvalue é um objeto temporário, que será destruído no final da expressão. No C ++ atual, os rvalues são vinculados apenas às
const
referências. O C ++ 1x permitiráconst
referências sem valor, ortografadasT&&
, que são referências a objetos rvalue.Como um rvalue vai morrer no final de uma expressão, você pode roubar seus dados . Em vez de copiá- lo para outro objeto, você move seus dados para ele.
No código acima, nos compiladores antigos, o resultado de
f()
é copiado para ox
usoX
do construtor de cópias. Se o seu compilador suportar semântica de movimentação eX
tiver um construtor de movimentação, isso será chamado. Como seurhs
argumento é um rvalor , sabemos que não é mais necessário e podemos roubar seu valor.Portanto, o valor é movido do temporário sem nome retornado de
f()
parax
(enquanto os dados dex
, inicializados para um vazioX
, são movidos para o temporário, que será destruído após a atribuição).fonte
this->swap(std::move(rhs));
porque as referências rvalue nomeados são lvaluesrhs
é um valor l no contexto deX::X(X&& rhs)
. Você precisa ligarstd::move(rhs)
para obter um rvalor, mas isso meio que faz a resposta ser discutível.Suponha que você tenha uma função que retorne um objeto substancial:
Quando você escreve um código como este:
então, um compilador C ++ comum criará um objeto temporário para o resultado de
multiply()
, chame o construtor de cópia para inicializarr
e destrua o valor de retorno temporário. A semântica de movimentação no C ++ 0x permite que o "construtor de movimentação" seja chamado para inicializarr
copiando seu conteúdo e depois descarte o valor temporário sem precisar destruí-lo.Isso é especialmente importante se (como talvez o
Matrix
exemplo acima), o objeto a ser copiado alocar memória extra na pilha para armazenar sua representação interna. Um construtor de cópias teria que fazer uma cópia completa da representação interna ou usar a contagem de referência e a semântica de copiar na gravação de forma interativa. Um construtor de movimentação deixaria a memória da pilha em paz e apenas copiaria o ponteiro dentro doMatrix
objeto.fonte
Se você estiver realmente interessado em uma boa explicação detalhada da semântica de movimentos, recomendo a leitura do artigo original, "Uma proposta para adicionar suporte à semântica de movimentos à linguagem C ++".
É muito acessível e fácil de ler e é um excelente argumento para os benefícios que eles oferecem. Existem outros trabalhos mais recentes e atualizados sobre a semântica de movimentos disponíveis no site do WG21 , mas este é provavelmente o mais direto, pois aborda as coisas de uma visão de nível superior e não se aprofunda nos detalhes da linguagem.
fonte
Mover semântica é transferir recursos, em vez de copiá-los quando ninguém mais precisar do valor de origem.
No C ++ 03, os objetos geralmente são copiados, apenas para serem destruídos ou atribuídos antes que qualquer código use o valor novamente. Por exemplo, quando você retorna por valor de uma função - a menos que o RVO entre em ação - o valor retornado é copiado para o quadro de pilha do chamador e, em seguida, sai do escopo e é destruído. Este é apenas um dos muitos exemplos: veja a passagem por valor quando o objeto de origem é temporário, algoritmos como
sort
esse apenas reorganizam itens, realocamvector
quando seu valorcapacity()
é excedido, etc.Quando esses pares de copiar / destruir são caros, geralmente é porque o objeto possui algum recurso pesado. Por exemplo,
vector<string>
pode possuir um bloco de memória alocado dinamicamente contendo uma matriz destring
objetos, cada um com sua própria memória dinâmica. Copiar esse objeto é caro: você precisa alocar nova memória para cada bloco alocado dinamicamente na fonte e copiar todos os valores. Então você precisa desalocar toda a memória que acabou de copiar. No entanto, mover um grandevector<string>
significa apenas copiar alguns ponteiros (que se referem ao bloco de memória dinâmica) para o destino e zerá-los na fonte.fonte
Em termos fáceis (práticos):
Copiar um objeto significa copiar seus membros "estáticos" e chamar o
new
operador para seus objetos dinâmicos. Direita?No entanto, mover um objeto (repito, do ponto de vista prático) implica apenas copiar os ponteiros de objetos dinâmicos, e não criar novos.
Mas isso não é perigoso? Obviamente, você pode destruir um objeto dinâmico duas vezes (falha de segmentação). Portanto, para evitar isso, você deve "invalidar" os ponteiros de origem para evitar destruí-los duas vezes:
Ok, mas se eu mover um objeto, o objeto de origem se torna inútil, não? Claro, mas em certas situações isso é muito útil. O mais evidente é quando chamo uma função com um objeto anônimo (temporal, objeto rvalue, ..., você pode chamá-lo com nomes diferentes):
Nessa situação, um objeto anônimo é criado, depois copiado para o parâmetro de função e excluído posteriormente. Portanto, aqui é melhor mover o objeto, porque você não precisa do objeto anônimo e pode economizar tempo e memória.
Isso leva ao conceito de uma referência "rvalue". Eles existem no C ++ 11 apenas para detectar se o objeto recebido é anônimo ou não. Eu acho que você já sabe que um "lvalue" é uma entidade atribuível (a parte esquerda do
=
operador), portanto, você precisa de uma referência nomeada a um objeto para poder atuar como um lvalue. Um rvalue é exatamente o oposto, um objeto sem referências nomeadas. Por esse motivo, objeto anônimo e rvalue são sinônimos. Assim:Nesse caso, quando um objeto do tipo
A
deve ser "copiado", o compilador cria uma referência lvalue ou uma referência rvalue de acordo com se o objeto transmitido é nomeado ou não. Quando não, seu construtor de movimentação é chamado e você sabe que o objeto é temporal e pode mover seus objetos dinâmicos em vez de copiá-los, economizando espaço e memória.É importante lembrar que objetos "estáticos" são sempre copiados. Não há maneiras de "mover" um objeto estático (objeto na pilha e não na pilha). Portanto, a distinção "mover" / "copiar" quando um objeto não possui membros dinâmicos (direta ou indiretamente) é irrelevante.
Se o seu objeto for complexo e o destruidor tiver outros efeitos secundários, como chamar a função de uma biblioteca, chamar outras funções globais ou o que quer que seja, talvez seja melhor sinalizar um movimento com uma bandeira:
Portanto, seu código é mais curto (você não precisa fazer uma
nullptr
atribuição para cada membro dinâmico) e mais geral.Outra pergunta típica: qual é a diferença entre
A&&
econst A&&
? Claro, no primeiro caso, você pode modificar o objeto e no segundo não, mas, significado prático? No segundo caso, você não pode modificá-lo, portanto não há como invalidar o objeto (exceto com um sinalizador mutável ou algo parecido) e não há diferença prática para um construtor de cópias.E o que é encaminhamento perfeito ? É importante saber que uma "referência rvalue" é uma referência a um objeto nomeado no "escopo do chamador". Porém, no escopo real, uma referência rvalue é um nome para um objeto e, portanto, atua como um objeto nomeado. Se você passar uma referência rvalue para outra função, estará passando um objeto nomeado, portanto, o objeto não será recebido como um objeto temporal.
O objeto
a
seria copiado para o parâmetro real deother_function
. Se você deseja que o objetoa
continue sendo tratado como um objeto temporário, use astd::move
função:Com esta linha,
std::move
converteráa
em um rvalue eother_function
receberá o objeto como um objeto sem nome. Obviamente, seother_function
não houver sobrecarga específica para trabalhar com objetos não nomeados, essa distinção não será importante.Esse encaminhamento é perfeito? Não, mas estamos muito perto. O encaminhamento perfeito é útil apenas para trabalhar com modelos, com o objetivo de dizer: se eu precisar passar um objeto para outra função, preciso que, se eu receber um objeto nomeado, o objeto seja passado como um objeto nomeado e, quando não, Quero passar como um objeto sem nome:
Essa é a assinatura de uma função prototípica que utiliza o encaminhamento perfeito, implementada no C ++ 11 por meio de
std::forward
. Esta função explora algumas regras de instanciação de modelo:Portanto, se
T
for uma referência de valor l paraA
( T = A &),a
também ( A & && => A &). SeT
é uma referência de valor igual aA
,a
também (A && && => A &&). Nos dois casos,a
é um objeto nomeado no escopo real, masT
contém as informações do seu "tipo de referência" do ponto de vista do escopo do chamador. Esta informação (T
) é passada como parâmetro de modelo paraforward
e 'a' é movido ou não de acordo com o tipo deT
.fonte
É como copiar semântica, mas, em vez de ter que duplicar todos os dados, você rouba os dados do objeto que está sendo "movido".
fonte
Você sabe o que significa semântica de cópia, certo? significa que você tem tipos que podem ser copiados; para tipos definidos pelo usuário, você define isso escrevendo explicitamente um construtor de cópias e um operador de atribuição ou o compilador os gera implicitamente. Isso fará uma cópia.
A semântica de movimentação é basicamente um tipo definido pelo usuário com o construtor que recebe uma referência de valor r (novo tipo de referência usando && (sim dois e comercial)) que não é const, isso é chamado de construtor de movimentação, o mesmo vale para o operador de atribuição. Então, o que um construtor de movimentação faz, bem, em vez de copiar a memória do argumento de origem, ele 'move' a memória da origem para o destino.
Quando você quer fazer isso? well std :: vector é um exemplo, digamos que você criou um std :: vector temporário e você o retorna de uma função, digamos:
Você terá sobrecarga do construtor de cópias quando a função retornar, se (e em C ++ 0x) std :: vector tiver um construtor de movimentação em vez de copiar, basta definir seus ponteiros e 'mover' alocado dinamicamente memória para a nova instância. É como uma semântica de transferência de propriedade com std :: auto_ptr.
fonte
Para ilustrar a necessidade de semântica de movimentação , vamos considerar este exemplo sem semântica de movimentação:
Aqui está uma função que pega um objeto do tipo
T
e retorna um objeto do mesmo tipoT
:A função acima usa chamada por valor, o que significa que, quando essa função é chamada, um objeto deve ser construído para ser usado pela função.
Como a função também retorna por valor , outro novo objeto é construído para o valor de retorno:
Dois novos objetos foram construídos, um dos quais é um objeto temporário usado apenas durante a duração da função.
Quando o novo objeto é criado a partir do valor retornado, o construtor de cópia é chamado para copiar o conteúdo do objeto temporário para o novo objeto b. Depois que a função é concluída, o objeto temporário usado na função sai do escopo e é destruído.
Agora, vamos considerar o que um construtor de cópias faz.
Ele deve primeiro inicializar o objeto e depois copiar todos os dados relevantes do objeto antigo para o novo.
Dependendo da classe, talvez seja um contêiner com muitos dados; isso pode representar muito tempo e uso de memória
Com a semântica de movimentação , agora é possível tornar a maior parte desse trabalho menos desagradável, basta mover os dados em vez de copiar.
Mover os dados envolve associar novamente os dados ao novo objeto. E nenhuma cópia ocorre .
Isso é realizado com uma
rvalue
referência.Uma
rvalue
referência funciona praticamente como umalvalue
referência com uma diferença importante:uma referência rvalue pode ser movida e um lvalue não.
De cppreference.com :
fonte
Estou escrevendo isso para garantir que eu o entenda corretamente.
A semântica de movimento foi criada para evitar a cópia desnecessária de objetos grandes. Bjarne Stroustrup, em seu livro "A linguagem de programação C ++", usa dois exemplos em que a cópia desnecessária ocorre por padrão: um, a troca de dois objetos grandes e dois, o retorno de um objeto grande de um método.
Trocar dois objetos grandes geralmente envolve copiar o primeiro objeto para um objeto temporário, copiar o segundo objeto para o primeiro objeto e copiar o objeto temporário para o segundo objeto. Para um tipo interno, isso é muito rápido, mas para objetos grandes, essas três cópias podem demorar muito tempo. Uma "atribuição de movimentação" permite que o programador substitua o comportamento padrão da cópia e troque as referências aos objetos, o que significa que não há cópia e a operação de troca é muito mais rápida. A atribuição de movimentação pode ser chamada chamando o método std :: move ().
Retornar um objeto de um método por padrão envolve fazer uma cópia do objeto local e de seus dados associados em um local acessível ao chamador (porque o objeto local não é acessível ao chamador e desaparece quando o método termina). Quando um tipo interno está sendo retornado, essa operação é muito rápida, mas se um objeto grande estiver sendo retornado, isso poderá levar muito tempo. O construtor move permite que o programador substitua esse comportamento padrão e, em vez disso, "reutilize" os dados de heap associados ao objeto local, apontando o objeto retornado ao chamador para heap de dados associados ao objeto local. Portanto, nenhuma cópia é necessária.
Em idiomas que não permitem a criação de objetos locais (ou seja, objetos na pilha), esses tipos de problemas não ocorrem porque todos os objetos são alocados no heap e sempre são acessados por referência.
fonte
x
ey
, você não pode simplesmente "trocar referências aos objetos" ; pode ser que os objetos contenham ponteiros que façam referência a outros dados e esses ponteiros possam ser trocados, mas os operadores de movimentação não precisam trocar nada. Eles podem apagar os dados do objeto movido de, em vez de preservar os dados de destino nele.swap()
sem mover a semântica. "A atribuição de movimentação pode ser chamada chamando o método std :: move ()." - às vezes é necessário usarstd::move()
- embora isso não mova nada - apenas permite que o compilador saiba que o argumento é móvel, algumas vezesstd::forward<>()
(com referências de encaminhamento) e outras vezes o compilador sabe que um valor pode ser movido.Aqui está uma resposta do livro "A linguagem de programação C ++", de Bjarne Stroustrup. Se você não quiser ver o vídeo, poderá ver o texto abaixo:
Considere este trecho. Retornar de um operador + envolve copiar o resultado da variável local
res
e em algum lugar onde o chamador possa acessá-lo.Nós realmente não queremos uma cópia; nós apenas queríamos obter o resultado de uma função. Portanto, precisamos mover um vetor em vez de copiá-lo. Podemos definir o construtor de movimentação da seguinte maneira:
O && significa "referência ao valor" e é uma referência à qual podemos vincular um valor. "rvalue" 'visa complementar "lvalue", que significa aproximadamente "algo que pode aparecer no lado esquerdo de uma tarefa". Portanto, um rvalue significa aproximadamente "um valor que você não pode atribuir", como um número inteiro retornado por uma chamada de função e a
res
variável local no operador + () para Vectors.Agora, a declaração
return res;
não será copiada!fonte