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

377

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? O que são limitações?

Luchian Grigore
fonte
11
A cópia elision é uma maneira de olhar para ela; elisão de objetos ou fusão de objetos (ou confusão) é outra visão.
precisa
Achei este link útil.
subtleseeker

Respostas:

246

Introdução

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

Para casos comuns em que a cópia é eliminada - pule para esta resposta .

A cópia elision é uma otimização implementada pela maioria dos compiladores para evitar cópias extras (potencialmente caras) em determinadas situações. Torna possível o retorno por valor ou passagem por valor na prática (aplicam-se restrições).

É a única forma de otimização que elimina (ha!) A regra como se - a cópia elision pode ser aplicada mesmo que copiar / mover o objeto tenha 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 objetos podem ser criados; portanto, você também não pode contar com um número específico de destruidores sendo chamados. Você não deve ter lógica crítica dentro de copy / move-constructors ou destructors, pois não pode confiar neles.

Se uma chamada para um construtor de copiar ou mover for elidida, esse construtor ainda deve existir e deve estar acessível. Isso garante que a cópia elision não permita copiar objetos que normalmente não são copiáveis, por exemplo, porque eles têm um construtor de copiar / mover privado ou excluído.

C ++ 17 : A partir do C ++ 17, o Copy Elision é garantido quando um objeto é 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
}
Luchian Grigore
fonte
2
você poderia explicar quando é que a segunda saída acontece e quando a terceira?
Zhangxaochen
3
@zhangxaochen quando e como o compilador decide otimizar dessa maneira.
Luchian Grigore
10
@zhangxaochen, 1ª saída: a cópia 1 é do retorno a uma temperatura e a cópia 2 de temp para obj; 2º é quando um dos itens acima é optimezed, provavelmente a cópia reutnr é elidida; o thris ambos são elidados
victor
2
Hmm, mas na minha opinião, isso DEVE ser um recurso em que possamos confiar. Porque, se não pudermos, isso afetaria gravemente a maneira como implementamos nossas funções no C ++ moderno (RVO vs std :: move). Durante a exibição de alguns dos vídeos do CppCon 2014, tive a impressão de que todos os compiladores modernos sempre fazem RVO. Além disso, li em algum lugar que também sem otimizações, os compiladores a aplicam. Mas, é claro, não tenho certeza. É por isso que estou perguntando.
J00hi
8
@ j00hi: Nunca escreva move em uma declaração de retorno - se rvo não for aplicado, o valor de retorno será movido por padrão de qualquer maneira.
MikeMB
96

Referência padrão

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

Para casos comuns em que a cópia é eliminada - pule para esta resposta .

A cópia elision é definida no padrão em:

12.8 Copiando e movendo objetos de classe [class.copy]

Como

31) Quando certos critérios são atendidos, uma implementação pode omitir a construção de copiar / mover um objeto de classe, mesmo que o construtor de copiar / mover e / ou destruidor do objeto tenha efeitos colaterais. Nesses casos, a implementação trata a origem e o destino da operação de copiar / mover omitida como simplesmente duas maneiras diferentes de se referir ao mesmo objeto, e a destruição desse objeto ocorre mais tarde nos momentos em que os dois objetos teriam sido destruído sem a otimização. 123 Esta elisão de operações de copiar / mover, chamada cópia elision , é permitida nas seguintes circunstâncias (que podem ser combinadas para eliminar várias cópias):

- em uma instrução de retorno em uma função com um tipo de retorno de classe, quando a expressão é o nome de um objeto automático não volátil (que não seja um parâmetro de função ou cláusula catch) com o mesmo tipo cvunqualified que o tipo de retorno da função, o A operação de copiar / mover pode ser omitida através da construção do objeto automático diretamente no valor de retorno da função

- em uma expressão de arremesso, quando o operando é o nome de um objeto automático não volátil (que não seja um parâmetro de função ou de cláusula catch) cujo escopo não se estende além do final do bloco de teste interno mais próximo (se houver um), a operação de copiar / mover do operando para o objeto de exceção (15.1) pode ser omitida através da construção do objeto automático diretamente no objeto de exceção

- quando um objeto de classe temporário que não foi vinculado a uma referência (12.2) seria copiado / movido para um objeto de classe com o mesmo tipo não qualificado de cv, a operação de copiar / mover pode ser omitida através da construção do objeto temporário diretamente no diretório destino da cópia / movimentação omitida

- quando a declaração de exceção de um manipulador de exceções (Cláusula 15) declara um objeto do mesmo tipo (exceto para qualificação cv) que o objeto de exceção (15.1), a operação de copiar / mover pode ser omitida tratando a declaração de exceção como um alias para o objeto de exceção se o significado do programa permanecer inalterado, exceto pela execução de construtores e destruidores do objeto declarado pela declaração de exceção.

123) Como apenas um objeto é destruído em vez de dois, e um construtor de copiar / mover não é executado, ainda há um objeto 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 elisão podem ser combinados para eliminar duas chamadas ao construtor de cópias da classe Thing: a cópia do objeto automático local tno objeto temporário para o valor de retorno da função f() e a cópia desse objeto temporário no objeto t2. Efetivamente, a construção do objeto local t pode ser vista como inicializando diretamente o objeto global t2, e a destruição desse objeto ocorrerá na saída do programa. Adicionar um construtor de movimentação a Thing tem o mesmo efeito, mas é a construção da movimentação do objeto temporário para o t2que é elidido.

Luchian Grigore
fonte
11
Isso é do padrão C ++ 17 ou de uma versão anterior?
Nils
90

Formas comuns de cópia elision

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

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

(Nomeado) A otimização do valor de retorno é uma forma comum de remoção de cópia. Refere-se à situação em que um objeto retornado por valor de um método tem sua cópia exagerada. O exemplo estabelecido no padrão ilustra a otimização do valor de retorno nomeado , uma vez que o objeto é 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 locais comuns onde ocorre a remoção de cópias é quando um valor temporário é passado por valor :

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

foo(Thing());

ou quando um 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) {  
  }             
}

As limitações comuns da remoção de cópias são:

  • vários pontos de retorno
  • inicialização condicional

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

Luchian Grigore
fonte
4
Eu estaria interessado em ver os pontos do marcador "Limitações comuns" explicados um pouco ... o que torna esses fatores limitantes?
phonetagger
@phonetagger eu liguei contra o artigo msdn, espero que isso esclareça algumas coisas.
Luchian Grigore
54

Copy elision é uma técnica de otimização de compilador que elimina a cópia / movimentação desnecessária de objetos.

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

  1. NRVO (Otimização de Valor de Retorno Nomeado) : se uma função retornar um tipo de classe por valor e a expressão da instrução de retorno for o nome de um objeto não volátil com duração de armazenamento automática (que não é um parâmetro de função), copie / mova que seria executado 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 função seria movido ou copiado.
  2. RVO (Return Value Optimization) : se a função retornar um objeto temporário sem nome que seria movido ou copiado no destino por um compilador ingênuo, a cópia ou movimentação poderá ser omitida conforme 1.
#include <iostream>  
using namespace std;

class ABC  
{  
public:   
    const char *a;  
    ABC()  
     { cout<<"Constructor"<<endl; }  
    ABC(const char *ptr)  
     { cout<<"Constructor"<<endl; }  
    ABC(ABC  &obj)  
     { cout<<"copy constructor"<<endl;}  
    ABC(ABC&& obj)  
    { cout<<"Move constructor"<<endl; }  
    ~ABC()  
    { cout<<"Destructor"<<endl; }  
};

ABC fun123()  
{ ABC obj; return obj; }  

ABC xyz123()  
{  return ABC(); }  

int main()  
{  
    ABC abc;  
    ABC obj1(fun123());//NRVO  
    ABC obj2(xyz123());//NRVO  
    ABC xyz = "Stack Overflow";//RVO  
    return 0;  
}

**Output without -fno-elide-constructors**  
root@ajay-PC:/home/ajay/c++# ./a.out   
Constructor    
Constructor  
Constructor  
Constructor  
Destructor  
Destructor  
Destructor  
Destructor  

**Output with -fno-elide-constructors**  
root@ajay-PC:/home/ajay/c++# g++ -std=c++11 copy_elision.cpp -fno-elide-constructors    
root@ajay-PC:/home/ajay/c++# ./a.out   
Constructor  
Constructor  
Move constructor  
Destructor  
Move constructor  
Destructor  
Constructor  
Move constructor  
Destructor  
Move constructor  
Destructor  
Constructor  
Move constructor  
Destructor  
Destructor  
Destructor  
Destructor  
Destructor  

Mesmo quando a elisão da cópia ocorre e o copiador / movedor-construtor não é chamado, ele deve estar presente e acessível (como se não houvesse otimização), caso contrário, o programa não está formado.

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

#include <iostream>     
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 oferece a -fno-elide-constructorsopção de desativar a cópia elision. Se você deseja evitar possíveis elisiones de cópia, use -fno-elide-constructors.

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

Conclusão

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

Ajay yadav
fonte
6
a afirmação ABC obj2(xyz123());é NRVO ou RVO? é não ficar temporária variável / object mesmo que ABC xyz = "Stack Overflow";//RVO
Asif Mushtaq
3
Para ter uma ilustração mais concreta do RVO, você pode consultar o assembly que o compilador gera (altere o sinalizador do compilador -fno-elide-constructors para ver o diff). godbolt.org/g/Y2KcdH
Gabinete