web-dev-qa-db-fra.com

Manière idiomatique de déclarer des classes immuables C ++

J'ai donc un code fonctionnel assez étendu où le type de données principal est des structures/classes immuables. La façon dont j'ai déclaré l'immuabilité est "pratiquement immuable" en rendant constantes les variables membres et toutes les méthodes.

struct RockSolid {
   const float x;
   const float y;
   float MakeHarderConcrete() const { return x + y; }
}

Est-ce en fait la façon dont "nous devrions le faire" en C++? Ou existe-t-il une meilleure façon?

33
BlamKiwi

La façon dont vous avez proposé est parfaitement correcte, sauf si dans votre code vous devez effectuer l'affectation des variables RockSolid, comme ceci:

RockSolid a(0,1);
RockSolid b(0,1);
a = b;

Cela ne fonctionnerait pas car l'opérateur d'affectation de copie aurait été supprimé par le compilateur.

Une alternative est donc de réécrire votre structure en tant que classe avec des membres de données privés et uniquement des fonctions const publiques.

class RockSolid {
  private:
    float x;
    float y;

  public:
    RockSolid(float _x, float _y) : x(_x), y(_y) {
    }
    float MakeHarderConcrete() const { return x + y; }
    float getX() const { return x; }
    float getY() const { return y; }
 }

De cette façon, vos objets RockSolid sont des (pseudo) immuables, mais vous pouvez toujours effectuer des affectations.

32
chrphb

Je suppose que votre objectif est la véritable immuabilité - chaque objet, une fois construit, ne peut pas être modifié. Vous ne pouvez pas affecter un objet à un autre.

Le plus gros inconvénient de votre conception est qu'elle n'est pas compatible avec la sémantique de déplacement, ce qui peut rendre les fonctions renvoyant de tels objets plus pratiques.

Par exemple:

struct RockSolidLayers {
  const std::vector<RockSolid> layers;
};

nous pouvons en créer un, mais si nous avons une fonction pour le créer:

RockSolidLayers make_layers();

il doit (logiquement) copier son contenu vers la valeur de retour, ou utiliser la syntaxe return {} pour le construire directement. Dehors, vous devez soit faire:

RockSolidLayers&& layers = make_layers();

ou encore (logiquement) copier-construire. L'incapacité de déplacer-construire entravera un certain nombre de façons simples d'avoir un code optimal.

Maintenant, ces deux constructions de copie sont élidées, mais le cas le plus général est vrai - vous ne pouvez pas déplacer vos données d'un objet nommé à un autre, car C++ n'a pas d'opération "détruire et déplacer" qui à la fois prend une variable hors de portée et l'utilise pour construire autre chose.

Et les cas où C++ déplaceront implicitement votre objet (return local_variable; Par exemple) avant la destruction sont bloqués par vos membres de données const.

Dans un langage conçu autour de données immuables, il saurait qu'il peut "déplacer" vos données malgré son immuabilité (logique).

Une façon de résoudre ce problème consiste à utiliser le tas et à stocker vos données dans std::shared_ptr<const Foo>. Maintenant, le constness n'est pas dans les données des membres, mais plutôt dans la variable. Vous pouvez également exposer uniquement les fonctions d'usine pour chacun de vos types qui renvoient le shared_ptr<const Foo> Ci-dessus, bloquant toute autre construction.

De tels objets peuvent être composés, avec Bar stockant std::shared_ptr<const Foo> Membres.

Une fonction renvoyant un std::shared_ptr<const X> Peut déplacer efficacement les données, et une variable locale peut avoir son état déplacé dans une autre fonction une fois que vous en avez fini sans être en mesure de jouer avec les "vraies" données.

Pour une technique connexe, il est idomatique en C++ moins contraint de prendre de tels shared_ptr<const X> Et de les stocker dans un type wrapper qui prétend qu'ils ne sont pas immuables. Lorsque vous effectuez une opération de mutation, le shared_ptr<const X> Est cloné et modifié, puis stocké. Une optimisation "sait" que le shared_ptr<const X> Est "vraiment" un shared_ptr<X> (Remarque: assurez-vous que les fonctions d'usine renvoient une fonte shared_ptr<X> À un shared_ptr<const X> Ou ceci n'est pas réellement vrai), et lorsque la fonction use_count() vaut 1, elle remplace const et la modifie directement. Il s'agit d'une implémentation de la technique connue sous le nom de "copie sur écriture".

9