Question

Qu'est-ce qu'un bon modèle classe / design existant pour la construction / initialisation à plusieurs étages d'un objet en C ++?

J'ai une classe avec certains membres de données qui doivent être initialisées en différents points dans le flux du programme, de sorte que leur initialisation doit être retardée. Par exemple, un argument peut être lu à partir d'un fichier et un autre du réseau.

Actuellement, je suis en utilisant boost :: en option pour la construction retardée des membres de données, mais il me tracasse que facultatif est sémantiquement différent de retarder décomposés.

Ce que je dois rappeler les caractéristiques de boost :: bind et lambda application de fonction partielle, et l'utilisation de ces bibliothèques, je peux probablement concevoir la construction en plusieurs étapes - mais je préfère utiliser existants, les classes testées. (Ou peut-être il y a un autre modèle de construction en plusieurs étapes que je ne suis pas au courant).

Était-ce utile?

La solution

La question clé est de savoir si oui ou non vous devez distinguer les objets complètement peuplés d'objets incomplètement peuplées au niveau du type . Si vous décidez de ne pas faire une distinction, puis il suffit d'utiliser boost::optional ou similaire que vous faites: cela rend facile d'obtenir rapidement le codage. OTOH vous ne pouvez pas obtenir le compilateur de faire respecter l'exigence selon laquelle une fonction particulière nécessite un objet complètement peuplé; vous devez effectuer l'exécution de la vérification des champs à chaque fois.

Types de paramètres groupe

Si vous faites distinguer les objets complètement peuplés d'objets incomplètement peuplées au niveau du type, vous pouvez appliquer l'exigence qu'une fonction soit passée d'un objet complet. Pour ce faire, je suggère la création d'un XParams de type correspondant pour chaque X de type pertinent. XParams a des membres de boost::optional et les fonctions setter pour chaque paramètre qui peut être réglé après la construction initiale. Ensuite, vous pouvez forcer X avoir un seul constructeur (non-copie), qui prend un XParams comme seul argument et vérifie que chaque paramètre nécessaire a été défini dans cet objet XParams. (Je ne sais pas si ce modèle a un nom - Quelqu'un veut modifier pour nous remplir)

Types "d'objet partiel"

Cela fonctionne à merveille si vous n'avez pas vraiment à faire quoi que ce soit avec l'objet avant qu'il ne soit complètement peuplé (peut-être autre que des choses triviales comme obtenir les valeurs de champ arrière). Si vous devez traiter parfois un X incomplètement peuplé comme un « plein » X place, vous pouvez faire X dériver d'un type XPartial, qui contient toute la logique, plus protected méthodes virtuelles pour effectuer des tests de condition préalable que vérifier si tous les champs nécessaires sont peuplé. Ensuite, si X assure qu'il ne peut jamais être construit dans un état complètement peuplé, il peut passer outre les méthodes protégées avec des contrôles triviales qui reviennent toujours true:

class XPartial {
    optional<string> name_;

public:
    void setName(string x) { name_.reset(x); }  // Can add getters and/or ctors
    string makeGreeting(string title) {
        if (checkMakeGreeting_()) {             // Is it safe?
            return string("Hello, ") + title + " " + *name_;
        } else {
            throw domain_error("ZOINKS");       // Or similar
        }
    }
    bool isComplete() const { return checkMakeGreeting_(); }  // All tests here

protected:
    virtual bool checkMakeGreeting_() const { return name_; }   // Populated?
};

class X : public XPartial {
    X();     // Forbid default-construction; or, you could supply a "full" ctor

public:
    explicit X(XPartial const& x) : XPartial(x) {  // Avoid implicit conversion
        if (!x.isComplete()) throw domain_error("ZOINKS");
    }

    X& operator=(XPartial const& x) {
        if (!x.isComplete()) throw domain_error("ZOINKS");
        return static_cast<X&>(XPartial::operator=(x));
    }

protected:
    virtual bool checkMakeGreeting_() { return true; }   // No checking needed!
};

Bien qu'il puisse sembler l'héritage ici est « retour à l'avant », cette façon de faire signifie qu'un X peut en toute sécurité être fournie partout où un XPartial& est demandé, si cette approche obéit à la Liskov principe de substitution . Cela signifie qu'une fonction peut utiliser un type de paramètre de X& pour indiquer qu'il a besoin d'un objet X complet ou XPartial& pour indiquer qu'il peut manipuler des objets partiellement peuplées -. Dans ce cas, soit un objet XPartial ou une X complète peut être passé

Au départ, j'avais isComplete() comme protected, mais a trouvé cela ne fonctionne pas depuis copie de X de cteur et opérateur d'affectation doit appeler cette fonction sur leur argument XPartial&, et ils n'ont pas un accès suffisant. A la réflexion, il est plus logique d'exposer publiquement cette fonctionnalité.

Autres conseils

Je dois manquer quelque chose - je faire ce genre de chose tout le temps. Il est très fréquent d'avoir des objets qui sont grands et / ou non nécessaire par une classe en toutes circonstances. Alors les créer dynamiquement!

struct Big {
    char a[1000000];
};

class A {
  public: 
    A() : big(0) {}
   ~A() { delete big; }

   void f() {
      makebig();
      big->a[42] = 66;
   }
  private:
    Big * big;
    void makebig() {
      if ( ! big ) {
         big = new Big;
      }
    }
};

Je ne vois pas le besoin de quoi que ce soit que colombophile, sauf que makebig () devrait probablement être const (et peut-être en ligne), et le pointeur Big devrait probablement être mutable. Et bien sûr, A doit être en mesure de construire Big, ce qui peut dans d'autres cas, la mise en cache des paramètres moyenne du constructeur de la classe contenu. Vous devrez également décider d'une politique de copie / cession -. Je serais probablement interdis à la fois pour ce genre de classe

Je ne connais pas de motifs pour faire face à ce problème spécifique. Il est une question de conception délicate, et un peu unique des langages comme C ++. Une autre question est que la réponse à cette question est étroitement liée à votre personne (ou morale) le style de codage.

J'utiliser des pointeurs pour ces membres, et quand ils doivent être construits, de les affecter en même temps. Vous pouvez utiliser auto_ptr pour ceux-ci, et vérifiez contre NULL pour voir si elles sont initialisés. (Je pense que de pointeurs sont un type intégré « option » en C / C ++ / Java, il existe d'autres langues où NULL n'est pas un pointeur valide).

Une question comme une question de style que vous pouvez compter sur votre constructeurs à faire trop de travail. Quand je codage OO, je les constructeurs fonctionnent juste assez pour obtenir l'objet dans un état cohérent. Par exemple, si j'ai une classe Image et je veux lire un fichier, je pouvais faire ceci:

image = new Image("unicorn.jpeg"); /* I'm not fond of this style */

ou, je pourrais faire ceci:

image = new Image(); /* I like this better */
image->read("unicorn.jpeg");

Il peut être difficile de raisonner sur la façon dont un programme C ++ fonctionne si les constructeurs ont beaucoup de code en eux, surtout si vous posez la question, « ce qui se passe si un constructeur échoue? » Ceci est le principal avantage de sortir le code des constructeurs.

J'aurais plus à dire, mais je ne sais pas ce que vous essayez de faire avec la construction retardée.

Edit: je me suis souvenu qu'il ya un moyen de (un peu pervers) appeler un constructeur sur un objet à tout moment arbitraire. Voici un exemple:

class Counter {
public:
    Counter(int &cref) : c(cref) { }
    void incr(int x) { c += x; }
private:
    int &c;
};

void dontTryThisAtHome() {
    int i = 0, j = 0;
    Counter c(i);       // Call constructor first time on c
    c.incr(5);          // now i = 5
    new(&c) Counter(j); // Call the constructor AGAIN on c
    c.incr(3);          // now j = 3
}

Notez que faire quelque chose aussi téméraires que cela pourrait vous faire gagner le mépris de vos programmeurs collègues, à moins que vous avez des raisons solides pour l'utilisation de cette technique. Cela ne retarde pas aussi le constructeur, juste vous permet d'appeler à nouveau plus tard.

L'utilisation boost.optional ressemble à une bonne solution pour certains cas d'utilisation. Je ne l'ai pas beaucoup joué avec lui donc je ne peux pas beaucoup de commentaires. Une chose que je garde à l'esprit lorsqu'ils traitent avec cette fonctionnalité est de savoir si je peux utiliser des constructeurs surchargées au lieu des constructeurs par défaut et copie.

Quand je besoin de cette fonctionnalité, je voudrais simplement utiliser un pointeur sur le type du champ nécessaire comme ceci:

public:
  MyClass() : field_(0) { } // constructor, additional initializers and code omitted
  ~MyClass() {
    if (field_)
      delete field_; // free the constructed object only if initialized
  }
  ...
private:
  ...
  field_type* field_;

Ensuite, au lieu d'utiliser le pointeur j'accéder au champ par la méthode suivante:

private:
  ...
  field_type& field() {
    if (!field_)
      field_ = new field_type(...);
    return field_;
  }

J'ai omis sémantique const-accès

La façon la plus simple que je sais est similaire à la technique proposée par Dietrich Epp, sauf qu'il vous permet de retarder vraiment la construction d'un objet jusqu'à ce qu'un moment de votre choix.

En gros: réserver l'objet en utilisant malloc au lieu de nouveau (contournant ainsi le constructeur), puis appelez le nouvel opérateur lorsque vous vraiment veulent surchargé de construire l'objet via le placement nouveau

.

Exemple:

Object *x = (Object *) malloc(sizeof(Object));
//Use the object member items here. Be careful: no constructors have been called!
//This means you can assign values to ints, structs, etc... but nested objects can wreak havoc!

//Now we want to call the constructor of the object
new(x) Object(params);

//However, you must remember to also manually call the destructor!
x.~Object();
free(x);

//Note: if you're the malloc and new calls in your development stack 
//store in the same heap, you can just call delete(x) instead of the 
//destructor followed by free, but the above is the  correct way of 
//doing it

Personnellement, la seule fois que je l'ai jamais utilisé cette syntaxe était quand je devais utiliser un allocateur basé sur C personnalisé pour les objets C ++. Comme le suggère Dietrich, vous devriez vous demander si vous avez vraiment, vraiment devez retarder l'appel du constructeur. Base constructeur doit effectuer le strict minimum pour obtenir votre objet dans un état réparable, alors que d'autres constructeurs surchargées peuvent effectuer plus de travail au besoin.

Je ne sais pas s'il y a un modèle formel pour cela. Dans les endroits où je l'ai vu, nous l'avons appelé « paresseux », « demande » ou « à la demande ».

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top