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.

Foi útil?

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 AnObjects 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;
}
Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top