Recentemente, li sobre construtores de movimento em C ++ (veja, por exemplo, aqui ) e estou tentando entender como eles funcionam e quando devo usá-los.
Tanto quanto eu entendo, um construtor de movimentação é usado para aliviar os problemas de desempenho causados pela cópia de objetos grandes. A página da wikipedia diz: "Um problema de desempenho crônico com o C ++ 03 são as cópias profundas caras e desnecessárias que podem acontecer implicitamente quando objetos são passados por valor".
Eu costumo abordar essas situações
- passando os objetos por referência ou
- usando ponteiros inteligentes (por exemplo, boost :: shared_ptr) para passar pelo objeto (os ponteiros inteligentes são copiados em vez do objeto).
Quais são as situações em que as duas técnicas acima não são suficientes e o uso de um construtor de movimentação é mais conveniente?
c++
programming-practices
Giorgio
fonte
fonte
shared_ptr
apenas por uma cópia rápida) e se a semântica de movimento pode conseguir o mesmo com quase nenhuma penalidade de codificação, semântica e limpeza.Respostas:
A semântica de movimentação introduz uma dimensão inteira ao C ++ - não está lá apenas para permitir que você retorne valores mais baratos.
Por exemplo, sem mover-semântica
std::unique_ptr
não funciona - vejastd::auto_ptr
, que foi preterido com a introdução da movimentação-semântica e removido no C ++ 17. Mover um recurso é muito diferente de copiá-lo. Permite a transferência da propriedade de um item exclusivo.Por exemplo, não vamos olhar
std::unique_ptr
, pois é bastante bem discutido. Vejamos, digamos, um objeto de buffer do vértice no OpenGL. Um buffer de vértice representa memória na GPU - ele precisa ser alocado e desalocado usando funções especiais, possivelmente com restrições rígidas quanto tempo pode durar. Também é importante que apenas um proprietário o use.Agora, isso pode ser feito com um
std::shared_ptr
- mas esse recurso não deve ser compartilhado. Isso torna confuso o uso de um ponteiro compartilhado. Você poderia usarstd::unique_ptr
, mas isso ainda requer semântica de movimentação.Obviamente, eu não implementei um construtor de movimentos, mas você entendeu.
O importante aqui é que alguns recursos não são copiáveis . Você pode passar os ponteiros em vez de se mover, mas, a menos que você use unique_ptr, há um problema de propriedade. Vale a pena ser o mais claro possível sobre qual é a intenção do código; portanto, um construtor de movimentos é provavelmente a melhor abordagem.
fonte
A semântica de movimento não é necessariamente uma melhoria tão grande quando você retorna um valor - e quando / se você usa um
shared_ptr
(ou algo semelhante) provavelmente está pessimizando prematuramente. Na realidade, quase todos os compiladores razoavelmente modernos fazem o que chamamos de Return Value Optimization (RVO) e Node Return Value Optimization (NRVO). Isto significa que quando você está retornando um valor, em vez de realmente copiar o valor em tudo, eles simplesmente passam um ponteiro / referência oculta para onde o valor será atribuído após o retorno, e a função usa isso para criar o valor para o final. O padrão C ++ inclui disposições especiais para permitir isso, portanto, mesmo que (por exemplo) seu construtor de cópias tenha efeitos colaterais visíveis, não é necessário usar o construtor de cópias para retornar o valor. Por exemplo:A idéia básica aqui é bastante simples: crie uma classe com conteúdo suficiente, e preferimos evitar copiá-la, se possível (
std::vector
preencheremos com 32767 ints aleatórios). Temos um copiador explícito que nos mostrará quando / se for copiado. Também temos um pouco mais de código para fazer algo com os valores aleatórios no objeto, para que o otimizador não elimine (pelo menos facilmente) tudo sobre a classe apenas porque ela não faz nada.Temos então algum código para retornar um desses objetos de uma função e, em seguida, usamos a soma para garantir que o objeto seja realmente criado, e não apenas ignorado completamente. Quando executamos, pelo menos com os compiladores mais recentes / modernos, descobrimos que o construtor de cópias que escrevemos nunca é executado - e sim, tenho certeza de que mesmo uma cópia rápida com a
shared_ptr
ainda é mais lenta do que não copiar. em absoluto.Mover permite que você faça um bom número de coisas que você simplesmente não poderia fazer (diretamente) sem elas. Considere a parte "mesclar" de uma classificação de mesclagem externa - você tem, digamos, 8 arquivos que serão mesclados. Idealmente, você gostaria de colocar todos os 8 desses arquivos em um
vector
- mas comovector
(a partir do C ++ 03) precisa copiar elementos eifstream
não pode ser copiado, você está preso a algunsunique_ptr
/shared_ptr
, ou algo nessa ordem para poder colocá-los em um vetor. Observe que, mesmo que (por exemplo) espaçemos oreserve
espaçovector
para que tenhamos certeza de que nossosifstream
s nunca serão realmente copiados, o compilador não saberá disso, portanto o código não será compilado, mesmo sabendo que o construtor de cópias nunca será usado de qualquer maneira.Mesmo que ainda não possa ser copiado, no C ++ 11 um
ifstream
pode ser movido. Neste caso, os objetos provavelmente não vai nunca ser movido, mas o fato de que eles poderiam ser, se necessário, mantém o feliz compilador, para que possamos colocar nossosifstream
objetos em umvector
diretamente, sem qualquer hacks ponteiro inteligente.Um vetor que se expande é um exemplo bastante decente de um tempo em que a semântica de movimento realmente pode ser / é útil. Nesse caso, o RVO / NRVO não ajudará, porque não estamos lidando com o valor de retorno de uma função (ou qualquer coisa muito semelhante). Temos um vetor que contém alguns objetos e queremos movê-los para um novo pedaço maior de memória.
No C ++ 03, isso foi feito criando cópias dos objetos na nova memória e destruindo os objetos antigos na memória antiga. Fazer todas essas cópias apenas para jogar fora as antigas, no entanto, era uma perda de tempo. No C ++ 11, você pode esperar que eles sejam movidos. Isso normalmente nos permite, em essência, fazer uma cópia superficial em vez de uma cópia profunda (geralmente muito mais lenta). Em outras palavras, com uma string ou vetor (por apenas alguns exemplos), apenas copiamos o (s) ponteiro (s) nos objetos, em vez de fazer cópias de todos os dados a que esses ponteiros se referem.
fonte
Considerar:
Ao incluir cadeias de caracteres em v, ele será expandido conforme necessário e, a cada realocação, as cadeias de caracteres deverão ser copiadas. Com os construtores de movimentação, isso é basicamente um problema.
Claro, você também pode fazer algo como:
Mas isso funcionará bem apenas porque os
std::unique_ptr
implementadores movem o construtor.O uso
std::shared_ptr
faz sentido apenas em situações (raras) quando você realmente possui uma propriedade compartilhada.fonte
string
termos uma instância deFoo
30 membros de dados? Aunique_ptr
versão não seria mais eficiente?Os valores de retorno são onde eu mais gostaria de passar por valor em vez de algum tipo de referência. Ser capaz de retornar rapidamente um objeto 'na pilha' sem uma penalidade de desempenho maciça seria bom. Por outro lado, não é particularmente difícil contornar isso (ponteiros compartilhados são tão fáceis de usar ...), por isso não tenho certeza de que vale a pena fazer um trabalho extra em meus objetos apenas para poder fazer isso.
fonte