O que é copiar elision e como otimiza o idioma de copiar e trocar?
-
23-09-2019 - |
Pergunta
eu estava lendo Copie e trocar.
Tentei ler alguns links sobre copiar a elisão, mas não consegui descobrir corretamente o que isso significava. Alguém pode explicar o que é essa otimização, e especialmente o que é mau pelo texto a seguir
Isso não é apenas uma questão de conveniência, mas de fato uma otimização. Se o (s) parâmetro (s) se ligar a um LValue (outro objeto que não consiga), uma cópia do objeto será feita automaticamente ao criar o (s) parâmetro (s). No entanto, quando S se liga a um Rvalue (objeto temporário, literal), a cópia é tipicamente Elided, o que salva uma chamada para um construtor de cópias e um destruidor. Na versão anterior do operador de atribuição, onde o parâmetro é aceito como referência const, a cópia da elisão não acontece quando a referência se liga a um RValue. Isso resulta em um objeto adicional que está sendo criado e destruído.
Solução
O construtor de cópias existe para fazer cópias. Em teoria quando você escreve uma linha como:
CLASS c(foo());
O compilador teria que ligar para o construtor de cópias para copiar o retorno de foo()
em c
.
A elisão de copiar é uma técnica para pular chamando o construtor de cópias para não pagar pela sobrecarga.
Por exemplo, o compilador pode organizar isso foo()
construirá diretamente seu valor de retorno em c
.
Aqui está outro exemplo. Digamos que você tenha uma função:
void doit(CLASS c);
Se você chamá -lo com um argumento real, o compilador deve invocar o construtor de cópias para que o parâmetro original não possa ser modificado:
CLASS c1;
doit(c1);
Mas agora considere um exemplo diferente, digamos que você chame sua função assim:
doit(c1 + c1);
operator+
vai ter que criar um objeto temporário (um rvalue). Em vez de invocar o construtor de cópias antes de ligar doit()
, o compilador pode passar o temporário que foi criado por operator+
e passar isso para doit()
em vez de.
Outras dicas
Aqui está um exemplo:
#include <vector>
#include <climits>
class BigCounter {
public:
BigCounter &operator =(BigCounter b) {
swap(b);
return *this;
}
BigCounter next() const;
void swap(BigCounter &b) {
vals_.swap(b);
}
private:
typedef ::std::vector<unsigned int> valvec_t;
valvec_t vals_;
};
BigCounter BigCounter::next() const
{
BigCounter newcounter(*this);
unsigned int carry = 1;
for (valvec_t::iterator i = newcounter.vals_.begin();
carry > 0 && i != newcounter.vals_.end();
++i)
{
if (*i <= (UINT_MAX - carry)) {
*i += carry;
} else {
*i += carry;
carry = 1;
}
}
if (carry > 0) {
newcounter.vals_.push_back(carry);
}
return newcounter;
}
void someFunction()
{
BigCounter loopcount;
while (true) {
loopcount = loopcount.next();
}
}
Dentro somefunction
a linha loopcount = loopcount.next();
beneficia muito da copiar a elisão. Se a cópia não fosse permitida, essa linha exigiria três invocações do construtor de cópias e uma chamada associada a um destruidor. Com a cópia da elisão sendo permitida, pode ser reduzida para 1 chamada do construtor de cópias, o explícito dentro de BigCount::next()
Onde newcounter
é declarado.
Se operator =
foi declarado e definido assim:
BigCounter &BigCounter::operator =(const BigCounter &b) {
BigCounter tmp(b);
swap(tmp);
return *this;
}
Houve que houve 2 invocações do construtor de cópias, mesmo com a cópia Elision. Um para construir newcounter
e o outro para construir tmp
. E sem copiar a elisão, ainda haveria 3. é por isso que declarar operator =
Portanto, seu argumento requer invocar a construção de cópia pode ser uma otimização ao usar o idioma 'cópia e troca' para o operador de atribuição. Quando o construtor de cópias é chamado para a construção de um argumento, sua invocação pode ser eliminada, mas se for invocada para criar uma variável local, pode não ser.