O que são elisão de cópia e otimização de valor de retorno?

O que é cópia elision? O que é (nomeado) otimização de valor de retorno? O que eles implicam?

Em que situações eles podem ocorrer? Quais são as limitações?

  • Se você foi referenciado a essa pergunta, provavelmente está procurando pela introdução .
  • Para uma visão geral técnica, consulte a referência padrão .
  • Veja casos comuns aqui .

Introdução

Para uma visão geral técnica – pule para esta resposta .

Para casos comuns onde ocorre a elisão de cópia – pule para esta resposta .

Copiar elision é uma otimização implementada pela maioria dos compiladores para evitar cópias extras (potencialmente caras) em determinadas situações. Isso torna o retorno por valor ou passagem a valor viável na prática (restrições se aplicam).

É a única forma de otimização que elide (ha!) A regra como-se – a elisão de cópia pode ser aplicada mesmo se copiar / mover o object tiver efeitos colaterais .

O exemplo a seguir retirado da Wikipedia :

struct C { C() {} C(const C&) { std::cout << "A copy was made.\n"; } }; C f() { return C(); } int main() { std::cout << "Hello World!\n"; C obj = f(); } 

Dependendo do compilador e das configurações, as seguintes saídas são todas válidas :

Olá Mundo!
Uma cópia foi feita.
Uma cópia foi feita.


Olá Mundo!
Uma cópia foi feita.


Olá Mundo!

Isso também significa que menos objects podem ser criados, então você também não pode confiar em um número específico de destruidores sendo chamados. Você não deve ter uma lógica crítica dentro de construtores de cópia / movimento ou destruidores, pois você não pode confiar que eles sejam chamados.

Se uma chamada para um construtor copy ou move for elidida, esse construtor ainda deverá existir e deverá estar acessível. Isso garante que a elisão de cópia não permita copiar objects que normalmente não podem ser copiados, por exemplo, porque eles têm um construtor de copiar / mover privado ou excluído.

C ++ 17 : A partir do C ++ 17, Copy Elision é garantido quando um object é retornado diretamente:

 struct C { C() {} C(const C&) { std::cout << "A copy was made.\n"; } }; C f() { return C(); //Definitely performs copy elision } C g() { C c; return c; //Maybe performs copy elision } int main() { std::cout << "Hello World!\n"; C obj = f(); //Copy constructor isn't called } 

Referência padrão

Para uma visão menos técnica e introdução – pule para esta resposta .

Para casos comuns onde ocorre a elisão de cópia – pule para esta resposta .

Copiar elision é definida no padrão em:

12.8 Copiando e movendo objects de class [class.copy]

Como

31) Quando determinados critérios são atendidos, uma implementação pode omitir a construção de copiar / mover um object de class, mesmo que o construtor de cópia / movimentação e / ou o destruidor do object tenham efeitos colaterais. Nesses casos, a implementação trata a origem e o destino da operação de cópia / movimentação omitida como simplesmente duas maneiras diferentes de se referir ao mesmo object, e a destruição desse object ocorre no final dos tempos em que os dois objects teriam sido destruído sem a otimização. 123 Essa elisão de operações de cópia / movimentação, chamada de cópia elisão , é permitida nas seguintes circunstâncias (que podem ser combinadas para eliminar várias cópias):

– em uma instrução de retorno em uma function com um tipo de retorno de class, quando a expressão é o nome de um object automático não volátil (diferente de uma function ou parâmetro catch-clause) com o mesmo tipo cvunqualified que o tipo de retorno da function, operação copy / move pode ser omitida através da construção do object automático diretamente no valor de retorno da function

– em uma expressão throw, quando o operando é o nome de um object automático não-volátil (diferente de uma function ou parâmetro catch-clause) cujo escopo não se estende além do final do bloco try mais interno (se houver um), a operação de copiar / mover do operando para o object de exceção (15.1) pode ser omitida pela construção do object automático diretamente no object de exceção

– quando um object de class temporário que não tenha sido vinculado a uma referência (12.2) seria copiado / movido para um object de class com o mesmo tipo cv-unqualified, a operação de copiar / mover pode ser omitida construindo o object temporário diretamente no alvo da cópia / movimentação omitida

– quando a declaração de exceção de um manipulador de exceção (Cláusula 15) declara um object do mesmo tipo (exceto cv-qualification) como o object de exceção (15.1), a operação copy / move pode ser omitida tratando a declaração de exceção como um alias para o object de exceção se o significado do programa for inalterado, exceto pela execução de construtores e destruidores para o object declarado pela declaração de exceção.

123) Como apenas um object é destruído em vez de dois, e um construtor copy / move não é executado, ainda há um object destruído para cada um construído.

O exemplo dado é:

 class Thing { public: Thing(); ~Thing(); Thing(const Thing&); }; Thing f() { Thing t; return t; } Thing t2 = f(); 

e explicou:

Aqui os critérios para elision podem ser combinados para eliminar duas chamadas para o construtor de cópia da class Thing : a cópia do object automático local t no object temporário para o valor de retorno da function f() e a cópia desse object temporário em object t2 . Efetivamente, a construção do object local t pode ser vista como inicializando diretamente o object global t2 , e a destruição desse object ocorrerá na saída do programa. Adicionar um construtor de movimento a Coisa tem o mesmo efeito, mas é a construção de movimento do object temporário para t2 que é elidido.

Formas comuns de cópia elisão

Para uma visão geral técnica – pule para esta resposta .

Para uma visão menos técnica e introdução – pule para esta resposta .

(Nomeado) A otimização do valor de retorno é uma forma comum de elisão de cópia. Refere-se à situação em que um object retornado por valor de um método tem sua cópia eliminada. O exemplo apresentado no padrão ilustra a otimização do valor de retorno nomeado , uma vez que o object é nomeado.

 class Thing { public: Thing(); ~Thing(); Thing(const Thing&); }; Thing f() { Thing t; return t; } Thing t2 = f(); 

A otimização do valor de retorno regular ocorre quando um temporário é retornado:

 class Thing { public: Thing(); ~Thing(); Thing(const Thing&); }; Thing f() { return Thing(); } Thing t2 = f(); 

Outros lugares comuns onde a elisão de cópia ocorre é quando um temporário é passado por valor :

 class Thing { public: Thing(); ~Thing(); Thing(const Thing&); }; void foo(Thing t); foo(Thing()); 

ou quando uma exceção é lançada e capturada por valor :

 struct Thing{ Thing(); Thing(const Thing&); }; void foo() { Thing c; throw c; } int main() { try { foo(); } catch(Thing c) { } } 

Limitações comuns da elisão de cópia são:

  • vários pontos de retorno
  • boot condicional

A maioria dos compiladores de nível comercial suportam cópia elisão & (N) RVO (dependendo das configurações de otimização).

Copiar elision é uma técnica de otimização de compiladores que elimina a cópia / movimentação desnecessária de objects.

Nas seguintes circunstâncias, um compilador pode omitir operações de copiar / mover e, portanto, não chamar o construtor associado:

  1. NRVO (Nomeado Retorno Valor Otimização) : Se uma function retorna um tipo de class por valor e expressão da declaração de retorno é o nome de um object não-volátil com duração de armazenamento automático (que não é um parâmetro de function), então a cópia / movimento que seria realizado por um compilador não otimizador pode ser omitido. Nesse caso, o valor retornado é construído diretamente no armazenamento para o qual o valor de retorno da function seria movido ou copiado.
  2. RVO (Return Value Optimization) : Se a function retornar um object temporário sem nome que seria movido ou copiado para o destino por um compilador ingênuo, a cópia ou movimentação pode ser omitida conforme 1.
 #include  using namespace std; class ABC { public: const char *a; ABC() { cout<<"Constructor"< 

Mesmo quando a elisão de cópia ocorre e o construtor de cópia / movimentação não é chamado, ele deve estar presente e acessível (como se nenhuma otimização tivesse acontecido), caso contrário, o programa está mal formado.

Você deve permitir essa cópia apenas em locais onde isso não afetará o comportamento observável do seu software. Copiar elisão é a única forma de otimização permitida ter (ou seja, elide) efeitos colaterais observáveis. Exemplo:

 #include  int n = 0; class ABC { public: ABC(int) {} ABC(const ABC& a) { ++n; } // the copy constructor has a visible side effect }; // it modifies an object with static storage duration int main() { ABC c1(21); // direct-initialization, calls C::C(42) ABC c2 = ABC(21); // copy-initialization, calls C::C( C(42) ) std::cout << n << std::endl; // prints 0 if the copy was elided, 1 otherwise return 0; } Output without -fno-elide-constructors root@ajay-PC:/home/ayadav# g++ -std=c++11 copy_elision.cpp root@ajay-PC:/home/ayadav# ./a.out 0 Output with -fno-elide-constructors root@ajay-PC:/home/ayadav# g++ -std=c++11 copy_elision.cpp -fno-elide-constructors root@ajay-PC:/home/ayadav# ./a.out 1 

O GCC fornece a opção -fno-elide-constructors para desabilitar a elisão de cópia. Se você quiser evitar a elisão de cópia possível, use -fno-elide-constructors .

Agora, quase todos os compiladores fornecem a elisão de cópia quando a otimização está ativada (e se nenhuma outra opção estiver definida para desativá-la).

Conclusão

Com cada elisão de cópia, uma construção e uma destruição correspondente da cópia são omitidas, economizando tempo de CPU e um object não é criado, economizando espaço no quadro da pilha.