43 votes

Comment appliquer réellement la règle de cinq?

Mise à JOUR au bas

q1: Comment voulez-vous mettre en œuvre la règle de cinq pour une classe qui gère plutôt que des ressources importantes, mais de qui vous voulez être passés par valeur, car qui simplifie grandement et embellit son utilisation? Ou ne sont pas toutes les cinq éléments de la règle, même nécessaire?

Dans la pratique, je commence quelque chose avec l'imagerie 3D où une image est généralement 128*128*128 en double. Pouvoir bien écrire des choses comme cela rendrait les calculs beaucoup plus facile:

Data a = MakeData();
Data c = 5 * a + ( 1 + MakeMoreData() ) / 3;

q2: à l'Aide d'une combinaison de copie élision / RVO / sémantique de déplacement, le compilateur doit être en mesure de de ce de ce avec un minimum de la copie, non?

J'ai essayé de comprendre comment ce faire j'ai donc commencé avec les bases; supposons qu'un objet de la mise en œuvre de la manière traditionnelle de mise en œuvre de la copie et de l'affectation:

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;
};

Maintenant, entrez rvalues et la sémantique de déplacement. Aussi loin que je peux dire que ce serait un travail de mise en œuvre:

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;
}

Cependant, le compilateur (VC++ 2010 SP1) n'est pas trop heureux avec cela, et les compilateurs sont généralement correctes:

AnObject make()
{
  return AnObject();
}

int main()
{
  AnObject a;
  a = make(); //error C2593: 'operator =' is ambiguous
}

q3: Comment résoudre ce problème? Revenir à AnObject& operator = ( const AnObject& rh ) corrige certainement, mais ne pas nous perdre une assez grande optimisation de l'occasion?

En dehors de cela, il est clair que le code du constructeur de déplacement et de cession est plein de duplication. Donc pour l'instant on oublie l'ambiguïté et essayer de le résoudre à l'aide de la copie et de swap, mais maintenant pour les rvalues. Comme expliqué ici, nous n'aurions même pas besoin d'un custom swap, mais plutôt avoir std::swap de faire tout le travail, ce qui semble très prometteur. J'ai donc écrit la suite, en espérant std::swap copie de construire un temporaire en utilisant le constructeur de déplacement, puis remplacez-la par *ce:

AnObject& operator = ( AnObject&& rh )
{
  std::swap( *this, rh );
  return *this;
}

Mais qui ne fonctionne pas et au lieu de cela conduit à un débordement de la pile grâce à une récursion infinie depuis std::swap appelle notre opérateur = ( AnObject&& rh ) à nouveau. q4: quelqu'un Peut-il donner un exemple de ce que l'on entend dans l'exemple alors?

Nous pouvons résoudre ce problème en fournissant une deuxième fonction d'échange:

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;
}

Il y a maintenant presque deux fois la quantité de code, cependant, le transfert d'une partie de paie de par alowing assez bon marché en mouvement; mais d'autre part, l'attribution normale ne peut pas bénéficier de la copie élision plus. À ce point, je suis vraiment confus mais, et ne voyant pas plus ce qui est bien et de mal, donc je suis l'espoir d'obtenir quelques commentaires ici..

Mise à JOUR de Sorte qu'il semble qu'il y a deux camps:

  • l'un disant à ignorer l'opérateur d'assignation de déplacement et de continuer à faire ce que le C++03 nous a enseigné, c'est à dire écrire un seul opérateur d'affectation qui passe l'argument par valeur.
  • l'autre en disant à mettre en œuvre l'opérateur d'assignation de déplacement (après tout, c'est du C++11) et d'avoir la copie opérateur d'affectation de prendre son argument par référence.

(ok et il y a le 3ème camp de me dire d'utiliser un vecteur, mais c'est une sorte de hors de la portée de cet hypothétique classe. Ok dans la vraie vie, je voudrais utiliser un vecteur, et il y aurait aussi d'autres membres, mais depuis le constructeur de déplacement/d'affectation ne sont pas générées automatiquement (encore?) la question serait toujours en attente)

Malheureusement, je ne peut pas tester les deux mises en œuvre dans un scénario réel, puisque ce projet vient de commencer et la façon dont les données seront effectivement flux n'est pas encore connu. J'ai donc tout simplement mis en œuvre à la fois d'entre eux, ajouté des compteurs pour l'allocation etc et a couru un couple d'itérations de l'env. ce code, où T est l'une des implémentations:

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 >() );
  }
}

Ce code n'est pas assez bon pour tester ce que je suis après, ou le compilateur est juste trop intelligent: n'a pas d'importance ce que j'utilise pour arraySize et numIter, les résultats pour les deux camps sont à peu près identiques: même nombre d'allocations, de très légères variations dans le calendrier, mais pas reproduit de différence significative.

Donc à moins que quelqu'un peut pointer vers une meilleure façon de tester cela (étant donné que l'utilisation réelle scnearios ne sont pas encore connus), je vais devoir conclure qu'il n'a pas d'importance et est donc laissée à la goût de le developper. Dans ce cas, je choisirais #2.

17voto

Howard Hinnant Points 59526

Vous avez manqué une optimisation importante dans votre copie opérateur d'affectation. Et par la suite, la situation a obtenu confus.

  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;
  }

Sauf si vous avez vraiment ne jamais penser que vous serez en attribuant AnObjects de la même taille, c'est beaucoup mieux. Ne jamais jeter de ressources si vous pouvez les recycler.

Certains pourraient se plaindre que l' AnObjects'opérateur d'assignation de copie a maintenant la base de l'exception de sécurité au lieu de forte exception de sécurité. Cependant, considérez ceci:

Vos clients peuvent toujours prendre un rapide opérateur d'affectation et de lui donner une forte exception de sécurité. Mais ils ne peuvent pas prendre une lente opérateur d'affectation et de le rendre plus rapide.

template <class T>
T&
strong_assign(T& x, T y)
{
    swap(x, y);
    return x;
}

Votre constructeur de déplacement est très bien, mais votre opérateur d'assignation de déplacement a une fuite de mémoire. Il devrait être:

  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: à l'Aide d'une combinaison de copie élision / RVO / sémantique de déplacement, le compilateur doit être en mesure de de ce de ce avec un minimum de la copie, non?

Vous pouvez avoir besoin de surcharger votre exploitants de bénéficier de ressources dans les rvalues:

Data operator+(Data&& x, const Data& y)
{
   // recycle resources in x!
   x += y;
   return std::move(x);
}

En fin de compte des ressources devrait être créé qu'une seule fois pour chaque Data vous vous souciez. Il ne devrait pas être inutile new/delete seulement pour le but de déplacer des choses autour.

13voto

Anthony Williams Points 28904

Si votre objet est de ressources-lourds, vous voudrez peut-être éviter la copie tout à fait, et il suffit de fournir le constructeur de déplacement et de déménagement opérateur d'affectation. Toutefois, si vous voulez vraiment de la copie de trop, il est facile de fournir toutes les opérations.

Vos opérations de copie regard sensible, mais vos opérations de déplacement de ne pas. Tout d'abord, si une référence rvalue paramètre permet de le lier à une rvalue, au sein de la fonction c'est une lvalue, de sorte que votre constructeur de déplacement doit être:

AnObject( AnObject&& rh ) :
  n( std::move(rh.n) ),
  a( std::move(rh.a) )
{
  rh.n = 0;
  rh.a = nullptr;
}

Bien sûr, pour les types fondamentaux comme vous avez ici, il n'est pas réellement faire une différence, mais c'est ainsi pour obtenir dans l'habitude.

Si vous fournissez un move-constructeur, alors vous n'avez pas besoin d'un move-opérateur d'affectation lorsque vous définissez la copie d'affectation comme vous avez --- parce que vous acceptez le paramètre par valeur, une rvalue sera déplacé dans le paramètre plutôt que de les copier.

Comme vous l'avez trouvé, vous ne pouvez pas utiliser std::swap() sur l'ensemble de l'objet à l'intérieur d'un move-opérateur d'affectation, depuis que répète en arrière dans le mouvement-opérateur d'affectation. Le point du commentaire dans le post que vous avez lié, c'est que vous n'avez pas besoin de mettre en œuvre une coutume swap si vous fournissez des opérations de déplacement, comme std::swap de l'utilisation de vos opérations de déplacement. Malheureusement, si vous ne définissez pas d'un côté opérateur d'affectation cela ne fonctionne pas, et encore de manière récursive. Vous pouvez bien sûr utiliser std::swap pour permuter entre les membres:

AnObject& operator=(AnObject other)
{
    std::swap(n,other.n);
    std::swap(a,other.a);
    return *this;
}

Votre classe finale est donc:

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;
};

4voto

Matthieu M. Points 101624

Permettez-moi de vous aider à:

#include <vector>

class AnObject
{
public:
  AnObject( size_t n = 0 ) : data(n) {}

private:
  std::vector<int> data;
};

Le C++0x FDIS, [la classe.copie] note 9:

Si la définition d'une classe X ne pas déclarer explicitement un constructeur de déplacement, on va être implicitement déclarée en défaut si et seulement si

  • X n'a pas un utilisateur déclaré constructeur de copie,

  • X n'a pas un utilisateur déclaré copie opérateur d'affectation,

  • X n'a pas un utilisateur déclaré déplacer opérateur d'affectation,

  • X n'a pas un utilisateur déclaré destructeur, et

  • le constructeur de déplacement ne serait pas implicitement défini comme étant supprimés.

[ Remarque: Lorsque le constructeur n'est pas déclarée implicitement ou explicitement fournis, expressions qui, autrement, aurait invoqué le constructeur de déplacement peut invoquer un constructeur de copie. -la note de fin ]

Personnellement, je suis beaucoup plus confiant dans std::vector correctement la gestion de ses ressources et l'optimisation de l'copies / se déplace que dans le code que je pourrais écrire.

1voto

ildjarn Points 38377

Puisque je n'ai pas vu quelqu'un d'autre explicitement ce point est souligné par...

Votre copie d'affectation opérateur de prise de son argument par valeur est une importante optimisation occasion, si (et seulement si) il est passé une rvalue, due à la copie élision. Mais dans une classe avec un opérateur d'affectation explicitement que seuls les prend rvalues (c'est à dire, l'une avec un opérateur d'assignation de déplacement), c'est un scénario absurde. Donc, modulo les fuites de mémoire qui ont déjà été souligné dans d'autres réponses, je dirais que votre classe est déjà idéal si vous changez simplement la copie opérateur d'affectation pour prendre son argument par référence const.

1voto

CTMacUser Points 772

t3 de l'Affiche Originale

Je pense que vous (et d'autres intervenants) mal compris ce que le compilateur erreur signifiait, et est venu à des conclusions erronées à cause de cela. Le compilateur pense que l' (déplacer) cession d'appel est ambigu, et c'est à droite! Vous disposez de plusieurs méthodes qui sont aussi qualifiés.

Dans votre version d'origine de l' AnObject classe, votre constructeur de copie prend dans l'ancien objet en const (lvalue) de référence, tandis que l'opérateur d'affectation prend son argument (non qualifié) de la valeur. La valeur de l'argument est initialisé par le transfert de constructeur à partir de tout ce qui était sur le côté droit de l'opérateur. Puisque vous avez seulement un transfert de constructeur, que le constructeur de copie est toujours utilisé, peu importe si l'original de droite de l'expression est une lvalue ou une rvalue. Cela rend l'opérateur d'affectation de la loi sur la copie d'affectation spéciale de la fonction membre.

La situation change une fois qu'un constructeur de déplacement est ajouté. Chaque fois que l'opérateur d'affectation est appelé, il y a deux choix pour le transfert de constructeur. Le constructeur de copie seront toujours utilisés pour lvalue expressions, mais le constructeur de déplacement sera utilisé chaque fois qu'une rvalue expression est donnée à la place! Cela rend l'opérateur d'affectation simultanément agir en tant que mouvement d'affectation spéciale de la fonction membre.

Lorsque vous avez ajouté un traditionnel de déménagement de l'opérateur d'affectation, vous avez donné à la classe deux versions de la même spécial de la fonction membre, ce qui est une erreur. Vous avez déjà ce que vous voulez, il suffit donc de se débarrasser de la traditionnelle déplacez-opérateur d'affectation, et aucune autre modification ne devrait être nécessaire.

Dans les deux camps figurant dans votre mise à jour, je suppose que je suis techniquement dans le premier camp, mais pour des motivations bien différentes. (Ne pas sauter le (traditionnel), déplacez-opérateur d'affectation parce qu'il est "cassé" pour votre classe, mais parce que c'est superflu.)

BTW, je suis nouveau à la lecture des propos de C++11 et StackOverflow. Je suis venu avec cette réponse de la navigation sur un autre S. O. question avant de voir celui-ci. (Mise à jour: en Fait, j'ai toujours eu de la page ouverte. Le lien renvoie à la réponse spécifique de FredOverflow qui montre la technique.)

À propos de l'2011-Mai-12 Réponse par Howard Hinnant

(Je suis trop débutant pour directement de commentaires sur les réponses.)

Vous n'avez pas besoin explicitement de vérification pour l'auto-affectation si un autre test serait déjà à l'abattage de il. Dans ce cas, n != rh.n serait déjà prendre soin de la plupart. Cependant, l' std::copy appel est en dehors de ce (actuellement) intérieure if, de sorte que nous aurions n de niveau composant auto-affectations. C'est à vous de décider si ces missions serait trop anti-optimal, même si l'auto-affectation doit être rare.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X