Como, na verdade, aplicar a regra dos cinco?
-
12-11-2019 - |
Pergunta
ATUALIZAÇÃO na parte inferior
q1: Como você implementar o regra de cinco para uma classe que gerencia, ao invés de pesados recursos, mas o que você quer ser passado por valor, pois, que simplifica e embeleza a sua utilização?Ou não são todos os cinco itens da regra, mesmo necessário?
Na prática, eu estou começando algo com imagens em 3D onde uma imagem é geralmente 128*128*128 duplos.Ser capaz de que para escrever coisas como esta faria a matemática muito mais fácil:
Data a = MakeData();
Data c = 5 * a + ( 1 + MakeMoreData() ) / 3;
q2: Usando uma combinação de cópia elision / RVO / mover semântica o compilador deve ser capaz de esta esta com um mínimo de copiar, não?
Eu tentava descobrir como fazer isso, então eu comecei com o básico;suponha que um objeto de execução, a forma tradicional de execução de cópia e distribuição:
class AnObject
{
public:
AnObject( size_t n = 0 ) :
n( n ),
a( new int[ n ] )
{}
AnObject( const AnObject& rh ) :
n( rh.n ),
a( new int[ rh.n ] )
{
std::copy( rh.a, rh.a + n, a );
}
AnObject& operator = ( AnObject rh )
{
swap( *this, rh );
return *this;
}
friend void swap( AnObject& first, AnObject& second )
{
std::swap( first.n, second.n );
std::swap( first.a, second.a );
}
~AnObject()
{
delete [] a;
}
private:
size_t n;
int* a;
};
Agora digite rvalues e mover semântica.Tanto quanto eu posso dizer que este seria um trabalho de implementação:
AnObject( AnObject&& rh ) :
n( rh.n ),
a( rh.a )
{
rh.n = 0;
rh.a = nullptr;
}
AnObject& operator = ( AnObject&& rh )
{
n = rh.n;
a = rh.a;
rh.n = 0;
rh.a = nullptr;
return *this;
}
No entanto, o compilador VC++ 2010 SP1) não está muito feliz com isso, e compiladores são geralmente correto:
AnObject make()
{
return AnObject();
}
int main()
{
AnObject a;
a = make(); //error C2593: 'operator =' is ambiguous
}
q3: Como resolver isso?Voltando ao AnObject& operator = ( const AnObject& rh ), certamente, corrige-o mas não perdemos um, ao invés de otimização importante oportunidade?
Além de que, é claro, que o código para o mover de construtor e de atribuição é cheio de duplicação.Então agora nós esquecemos sobre a ambiguidade e tentar resolver isso usando copiar e swap, mas agora para rvalues.Como explicado aqui nós nem sequer precisa de um personalizado de permuta, mas em vez disso std::swap de fazer todo o trabalho, que parece ser muito promissor.Então, eu escrevi o seguinte, na esperança de std::swap de copiar construir uma temporário usando a mover-se construtor, em seguida, troque-a com *esta:
AnObject& operator = ( AnObject&& rh )
{
std::swap( *this, rh );
return *this;
}
Mas isso não trabalhar fora e, em vez disso, leva a uma sobrecarga de pilha devido a recursão infinita desde std::swap de chamadas, o nosso operador = ( AnObject&& rh ) novamente. q4: Alguém pode dar um exemplo de o que quer dizer o exemplo, então?
Podemos resolver isso fornecendo uma segunda função de comutação:
AnObject( AnObject&& rh )
{
swap( *this, std::move( rh ) );
}
AnObject& operator = ( AnObject&& rh )
{
swap( *this, std::move( rh ) );
return *this;
}
friend void swap( AnObject& first, AnObject&& second )
{
first.n = second.n;
first.a = second.a;
second.n = 0;
second.a = nullptr;
}
Agora, há quase duas vezes a quantidade de código, no entanto, o movimento parte do pagamento de pelo aguardando a bem barato móveis;mas, por outro lado, o normal de atribuição não pode beneficiar de cópia elision mais.Nesse ponto, eu estou realmente confuso ainda, e não ver mais o que é certo e errado, então eu estou esperando para fazer alguns comentários aqui..
ATUALIZAÇÃO Assim, parece que existem dois campos:
- um dizendo para passar a ser o operador de atribuição e continuar a fazer o que C++03 nos ensinou, ou seja, escrever um único operador de atribuição que passa o argumento por valor.
- o outro dizendo para implementar a mover o operador de atribuição (afinal, é C++11) e ter a cópia operador de atribuição levar seu argumento por referência.
(ok, e há o 3º acampamento me dizendo para usar um vetor, mas o que é a classificação de fora do escopo deste hipotético de classe.Ok, na vida real, eu iria usar um vetor, e haveria também outros membros, mas desde que o mover do construtor/atribuição não são gerados automaticamente (ainda?) a questão seria ainda realizar -)
Infelizmente eu não posso testar ambas as implementações em um cenário do mundo real, pois este projeto está apenas no início e a forma como os dados serão, na verdade, o fluxo ainda não é conhecida.Então, eu simplesmente implementadas tanto de-los, adicionar contadores para alocação etc e executou um par de iterações de aprox.este código, onde T é uma das implementações:
template< class T >
T make() { return T( narraySize ); }
template< class T >
void assign( T& r ) { r = make< T >(); }
template< class T >
void Test()
{
T a;
T b;
for( size_t i = 0 ; i < numIter ; ++i )
{
assign( a );
assign( b );
T d( a );
T e( b );
T f( make< T >() );
T g( make< T >() + make< T >() );
}
}
Este código não é bom o suficiente para testar o que eu estou atrás, ou o compilador é muito inteligente:não importa o que eu uso para arraySize e numIter, os resultados para ambos os campos são praticamente idênticas:mesmo número de alocações, muito ligeiras variações no tempo, mas não reproducable diferença significativa.
Assim, a menos que alguém pode apontar para uma melhor forma de fazer o teste (dado que o uso real scnearios ainda não são conhecidos), eu vou ter que concluir que não importa e, portanto, é deixado ao sabor da developper.Nesse caso eu escolheria #2.
Solução
Você já perdeu uma otimização significativa em sua cópia operador de atribuição.E, posteriormente, a situação ficou confuso.
AnObject& operator = ( const AnObject& rh )
{
if (this != &rh)
{
if (n != rh.n)
{
delete [] a;
n = 0;
a = new int [ rh.n ];
n = rh.n;
}
std::copy(rh.a, rh.a+n, a);
}
return *this;
}
A menos que você realmente nunca acho que você vai ser a atribuição de AnObject
s do mesmo tamanho, isso é muito melhor.Nunca jogue fora os recursos, se você pode reciclá-los.
Alguns podem reclamar que o AnObject
's cópia operador de atribuição tem agora básicos de segurança de exceção em vez de forte segurança de exceção.No entanto, considerar esta:
Seus clientes podem sempre fazer uma rápida o operador de atribuição e dar-lhe forte exceção de segurança.Mas eles não podem tirar a um lento operador de atribuição e torná-lo mais rápido.
template <class T> T& strong_assign(T& x, T y) { swap(x, y); return x; }
O seu movimento construtor é bom, mas o seu movimento operador de atribuição tem uma fuga de memória.Ele deve ser:
AnObject& operator = ( AnObject&& rh )
{
delete [] a;
n = rh.n;
a = rh.a;
rh.n = 0;
rh.a = nullptr;
return *this;
}
...
Data a = MakeData(); Data c = 5 * a + ( 1 + MakeMoreData() ) / 3;
q2: Usando uma combinação de cópia elision / RVO / mover semântica compilador deve ser capaz de este com um mínimo de copiar, não?
Você pode precisar de sobrecarga de operadores para tirar proveito dos recursos de rvalues:
Data operator+(Data&& x, const Data& y)
{
// recycle resources in x!
x += y;
return std::move(x);
}
Em última instância, os recursos devem ser criadas apenas uma vez para cada Data
você se preocupa.Não deve haver nenhum desnecessário new/delete
apenas com o propósito de mover as coisas ao redor.
Outras dicas
Se o objeto for mais pesados, você pode querer evitar a cópia de todo, e apenas fornecem a movimentação construtor e mover o operador de atribuição.No entanto, se você realmente deseja copiar muito, é fácil fornecer todas as operações.
Suas operações de cópia olhar sensível, mas o seu operações de movimentação não.Em primeiro lugar, embora um rvalue parâmetro de referência será bind para um rvalue, dentro da função é um lvalue, assim , seu mover construtor deve ser:
AnObject( AnObject&& rh ) :
n( std::move(rh.n) ),
a( std::move(rh.a) )
{
rh.n = 0;
rh.a = nullptr;
}
É claro que, para tipos fundamentais como você tem aqui é, na verdade, não fazem diferença, mas é bem para ter o hábito.
Se você fornecer um mover-construtor, então você não precisa de um mover-operador de atribuição quando você define copia-atribuição de como você --- porque você aceitar o parâmetro valor, um rvalue será movido para o parâmetro em vez de copiados.
Como você encontrou, você não pode usar std::swap()
em todo o objeto dentro de um mover-operador de atribuição, desde que recurse de volta para a mover-operador de atribuição.O ponto do comentário no post que você linkou para é que você não precisa implementar um swap
se você fornecer operações de movimentação, como std::swap
vai usar o seu operações de movimentação.Infelizmente, se você não definir um separado mover o operador de atribuição isso não funciona, e ainda recurse.Você pode, claro, usar std::swap
para trocar os membros:
AnObject& operator=(AnObject other)
{
std::swap(n,other.n);
std::swap(a,other.a);
return *this;
}
Sua última classe fica assim:
class AnObject
{
public:
AnObject( size_t n = 0 ) :
n( n ),
a( new int[ n ] )
{}
AnObject( const AnObject& rh ) :
n( rh.n ),
a( new int[ rh.n ] )
{
std::copy( rh.a, rh.a + n, a );
}
AnObject( AnObject&& rh ) :
n( std::move(rh.n) ),
a( std::move(rh.a) )
{
rh.n = 0;
rh.a = nullptr;
}
AnObject& operator = ( AnObject rh )
{
std::swap(n,rh.n);
std::swap(a,rh.a);
return *this;
}
~AnObject()
{
delete [] a;
}
private:
size_t n;
int* a;
};
Deixe-me ajudar você a:
#include <vector>
class AnObject
{
public:
AnObject( size_t n = 0 ) : data(n) {}
private:
std::vector<int> data;
};
A partir do C++0x FDIS, [classe.copiar] nota 9:
Se a definição de uma classe X não declarar explicitamente um movimento construtor, será declarado implicitamente como incumprimento, se, e somente se,
X não tem um usuário declarou construtor de cópia,
X não tem um usuário declarou copiar o operador de atribuição,
X não tem um usuário declarou mover o operador de atribuição,
X não tem um usuário declarou processo de destruição, e
o movimento construtor não seria implicitamente definidos como excluídos.
[ Nota:Quando o movimento construtor não é declarado implicitamente ou explicitamente fornecido, expressões que, de outra forma teria chamado o movimento construtor em vez disso, pode invocar um construtor de cópia.—nota final ]
Pessoalmente, estou muito mais confiante em std::vector
gerir corretamente os seus recursos e otimizar as cópias / move-se que, em qualquer código que eu poderia escrever.
Uma vez que eu não vi ninguém explicitamente ponto isso...
Sua cópia operador de atribuição, tendo seu argumento por valor é uma importante otimização oportunidade de se (e apenas se) é passado um rvalue, devido à cópia elision.Mas em uma classe com um operador de atribuição que explicitamente apenas leva rvalues (i.é., um com um movimento operador de atribuição), isso é um absurdo cenário.Assim, o modulo os vazamentos de memória que já foi apontado em outras respostas, eu diria que a classe já é ideal se você simplesmente mudar a cópia operador de atribuição para tomar o seu argumento constante de referência.
q3 do Poster Original
Eu acho que você (e alguns outros respondentes) entendido mal o que o erro de compilador significava, e chegou à conclusões erradas por causa disso.O compilador acha que a (mover) atribuição de chamada é ambíguo, e ele está certo!Você tem vários métodos que são igualmente qualificado.
Em sua versão original dos AnObject
classe, o construtor de cópia demora no antigo objeto de const
(lvalue) de referência, enquanto o operador de atribuição leva o seu argumento (não qualificado) do valor.O argumento valor é inicializado por uma adequada transferência de construtor de tudo o que estava no lado direito do operador.Já que você tem apenas uma transferência de construtor, que o construtor de cópia é sempre usada, não importa se o original do lado direito de expressão foi um lvalue ou um rvalue.Isso faz com que o operador de atribuição atuar como a cópia de atribuição de função de membro especial.
A situação altera-se assim um movimento construtor é adicionado.Sempre que o operador de atribuição é chamado, há duas opções para a transferência de construtor.O construtor de cópia continua a ser utilizada para lvalue expressões, mas o movimento construtor será usado sempre que um rvalue expressão é dada em vez disso!Isso faz com que o operador de atribuição simultaneamente agir como o movimento de atribuição de função de membro especial.
Quando você tiver adicionado um tradicional mover-operador de atribuição, você deu duas versões da mesma função de membro especial, o que é um erro.Você já tem o que queria, de modo que apenas livrar-se do tradicional mover-operador de atribuição, e outras alterações não deve ser necessária.
Nos dois campos listados em sua atualização, eu acho que eu estou tecnicamente, no primeiro acampamento, mas inteiramente diferentes razões.(Não ignore a (tradicional) mover-operador de atribuição, porque é "quebrado" para a sua classe, mas porque é supérfluo.)
BTW, eu sou novo para ler sobre C++11 e StackOverflow.Eu vim com esta resposta de navegação outro S. O.pergunta antes de ver este.(Atualização:Na verdade, eu ainda tinha a página abrir.O link vai para a resposta específica por FredOverflow que mostra a técnica.)
Sobre o de 2011-Maio-12 Resposta por Howard Hinnant
(Eu sou muito novato para comentar diretamente sobre as respostas.)
Você não precisa explicitamente de seleção para a auto-atribuição-se um teste posterior já abate-lo.Neste caso, n != rh.n
já deveria cuidar mais do mesmo.No entanto, o std::copy
chamada está fora do que (atualmente) interior if
, assim que chegar n
componente de nível de auto-atribuições.É até você para decidir se as designações seria muito anti-ideal, mesmo que a auto-atribuição deve ser raro.
Com a ajuda de delegação de construtor, você só precisa implementar cada conceito de uma só vez;
- padrão de inicialização
- excluir de recursos
- swap
- cópia
o resto só usa aqueles.
Também não se esqueça de fazer mover-atribuição (e troca) noexcept
,
se ajuda um desempenho muito se você, por exemplo, colocar sua classe
em um vector
#include <utility>
// header
class T
{
public:
T();
T(const T&);
T(T&&);
T& operator=(const T&);
T& operator=(T&&) noexcept;
~T();
void swap(T&) noexcept;
private:
void copy_from(const T&);
};
// implementation
T::T()
{
// here (and only here) you implement default init
}
T::~T()
{
// here (and only here) you implement resource delete
}
void T::swap(T&) noexcept
{
using std::swap; // enable ADL
// here (and only here) you implement swap, typically memberwise swap
}
void T::copy_from(const T& t)
{
if( this == &t ) return; // don't forget to protect against self assign
// here (and only here) you implement copy
}
// the rest is generic:
T::T(const T& t)
: T()
{
copy_from(t);
}
T::T(T&& t)
: T()
{
swap(t);
}
auto T::operator=(const T& t) -> T&
{
copy_from(t);
return *this;
}
auto T::operator=(T&& t) noexcept -> T&
{
swap(t);
return *this;
}