Comment mettre en œuvre la méthode de motif en C++ correctement

Il y a une chose en C++ ce qui a été fait me sentir mal à l'aise pendant un temps assez long, parce que honnêtement, je ne sais pas comment le faire, même si cela semble simple:

Comment implémenter la Méthode de factorisation dans C++ correctement?

But: permettre au client d'instancier un objet à l'aide de méthodes de fabrique à la place de l'objet constructeurs, sans conséquences inacceptables et un gain de performance.

Par "méthode de Fabrique de la tendance", je veux dire à la fois statique de l'usine de méthodes à l'intérieur d'un objet ou des méthodes définies dans une autre classe, ou des fonctions globales. En général, "le concept de la redirection de la voie normale de l'instanciation de la classe X de n'importe où ailleurs que le constructeur".

Permettez-moi de parcourir quelques réponses possibles à qui j'ai pensé.


0) Ne pas faire d'usines, de faire des constructeurs.

Cela sonne bien (et, en effet, souvent, la meilleure solution), mais n'est pas un remède universel. Tout d'abord, il y a des cas lors de la construction de l'objet est une tâche assez complexe pour justifier son extraction à une autre classe. Mais même en mettant de ce fait de côté, même pour de simples objets en utilisant seulement les constructeurs souvent de ne pas le faire.

L'exemple le plus simple je sais, c'est un 2-D de la classe Vector. Tellement simple, et pourtant délicat. Je veux être en mesure de construire les deux à partir à la fois Cartésien et des coordonnées polaires. Évidemment, je ne peux pas faire:

struct Vec2 {
    Vec2(float x, float y);
    Vec2(float angle, float magnitude); //not a valid overload!
    //...
};

Ma façon naturelle de penser est alors:

struct Vec2 {
    static Vec2 fromLinear(float x, float y);
    static Vec2 fromPolar(float angle, float magnitude);
    //...
};

Qui, au lieu de constructeurs, me conduit à l'utilisation de la statique des méthodes de fabrique... ce qui signifie essentiellement que je suis en œuvre le modèle de fabrique, en quelque sorte ("la classe devient sa propre usine"). Cela a l'air sympa (et le costume de ce cas particulier), mais échoue dans certains cas, que je vais décrire dans le point 2. Ne lisez la suite.

un autre cas: en essayant de surcharge par deux opaque typedefs de certaines API (tels que les Guid des domaines non reliés, ou un GUID et un champ de bits), les types sémantiquement totalement différentes (donc - en théorie - valide les surcharges), mais qui se révèlent être la même chose - comme des entiers non signés ou nulle pointeurs.


1) La Java Façon

Java est simple, car nous n'avons que dynamique-les objets alloués. Faire un factory est aussi trivial que:

class FooFactory {
    public Foo createFooInSomeWay() {
        //can be a static method as well,
        // if we don't need the factory to provide its own object semantics
        // and just serve as a group of methods
        return new Foo(some, args);
    }
}

En C++, cela se traduit par:

class FooFactory {
public:
    Foo* createFooInSomeWay() {
        return new Foo(some, args);
    }
};

Cool? Souvent, en effet. Mais cela oblige l'utilisateur à utiliser l'allocation dynamique. Allocation statique est ce qui fait de C++ complexe, mais est aussi ce qui souvent rend puissant. Aussi, je crois qu'il existe certaines cibles (mot-clé: embedded) qui ne permettent pas l'allocation dynamique. Et cela ne signifie pas que les utilisateurs de ces plates-formes, comme pour écrire nettoyer la POO.

De toute façon, la philosophie de côté: Dans le cas général, je ne veux pas forcer les utilisateurs de l'usine pour être retenue pour l'allocation dynamique.


2) Retour par valeur

OK, donc nous savons que 1) c'est cool quand on veut l'allocation dynamique. Pourquoi ne veut-on pas ajouter allocation statique sur le dessus de qui?

class FooFactory {
public:
    Foo* createFooInSomeWay() {
        return new Foo(some, args);
    }
    Foo createFooInSomeWay() {
        return Foo(some, args);
    }
};

Quoi? Nous ne pouvons pas la surcharge par le type de retour? Oh, bien sûr, nous ne le pouvons pas. Donc, nous allons changer les noms de méthode pour refléter cela. Et oui, j'ai écrit le code non valide exemple ci-dessus, juste à souligner combien je n'aime pas la nécessité de changer le nom de la méthode, par exemple, parce que nous ne pouvons pas mettre en œuvre un indépendant de la langue de conception d'usines correctement maintenant, puisque nous avons un changement de noms et de tous les utilisateurs de ce code vous devez vous rappeler que la différence de la mise en œuvre de la spécification.

class FooFactory {
public:
    Foo* createDynamicFooInSomeWay() {
        return new Foo(some, args);
    }
    Foo createFooObjectInSomeWay() {
        return Foo(some, args);
    }
};

OK... il nous l'avons. C'est moche, comme nous avons besoin de changer le nom de la méthode. C'est imparfait, car nous avons besoin d'écrire le même code deux fois. Mais une fois fait, il fonctionne. Droit?

Eh bien, en général. Mais parfois il ne le fait pas. Lors de la création de Foo, nous avons en fait dépendre du compilateur de faire le retour optimisation de la valeur pour nous, parce que la norme C++ est assez bienveillant pour les éditeurs de compilateurs de ne pas préciser quand l'objet créé en lieu et quand doit-il être copié lors du retour temporaire de l'objet par valeur en C++. Donc, si Toto est cher pour copier, cette approche est risquée.

Et si Toto n'est pas copiables à tous? Eh bien, doh. (Noter qu'en C++17 avec garantie de copie élision, pas-être-copiables est pas un problème de plus pour le code ci-dessus)

Conclusion: Faire une usine en retournant un objet est en effet une solution pour certains cas (comme le 2-D de vecteur mentionné précédemment), mais toujours pas un remplacement pour les constructeurs.


3) en Deux phases de construction

Une autre chose que quelqu'un serait probablement arriver, c'est de séparer la question de l'allocation des objets et de son initialisation. Il en résulte généralement dans le code comme ceci:

class Foo {
public:
    Foo() {
        //empty or almost empty
    }
    //...
};

class FooFactory {
public:
    void createFooInSomeWay(Foo& foo, some, args);
};

void clientCode() {
    Foo staticFoo;
    auto_ptr<Foo> dynamicFoo = new Foo();
    FooFactory factory;
    factory.createFooInSomeWay(&staticFoo);
    factory.createFooInSomeWay(&dynamicFoo.get());
    //...
}

On peut penser qu'il fonctionne comme un charme. Le seul prix que nous payons pour notre code...

Depuis que j'ai écrit tout cela et à gauche ce que la dernière, je n'aime pas trop. 🙂 Pourquoi?

Tout d'abord... sincèrement, je n'aime pas le concept de deux phases de construction et je me sens coupable lorsque je l'utilise. Si je crée mes objets avec l'affirmation que "si elle existe, elle est dans l'état valide", j'ai l'impression que mon code est plus sûr et moins sujette aux erreurs. J'aime cette façon.

Les abandons de la convention ET de l'évolution de la conception de mon objet juste pour le but de l'usine de fabrication est.. bien, lourd.

Je sais que le ci-dessus ne pas convaincre beaucoup de gens, permettez-moi de donner plus d'arguments solides. En utilisant deux phases de construction, vous ne pouvez pas:

  • initialiser const ou membre de référence variables,
  • passer des arguments à la base de constructeurs de classe et d'objet de membre des constructeurs.

Et probablement il pourrait y en avoir plus d'inconvénients que je ne peux pas penser à l'instant, et je ne le sent même pas particulièrement obligés de depuis la au-dessus des points de balle de me convaincre, déjà.

Donc: pas même à proximité d'une bonne solution générale pour la mise en œuvre d'une usine.


Conclusions:

Nous voulons avoir un moyen de l'instanciation d'objets qui:

  • permettre l'uniforme de l'instanciation, indépendamment de la répartition,
  • donner des noms significatifs pour les méthodes de construction (donc pas en s'appuyant sur l'argument de la surcharge),
  • de ne pas introduire de façon significative les performances frappé et, de préférence, une importante augmentation du code frapper, surtout à côté client,
  • être général, comme dans: possible être mis en place pour toute la classe.

Je crois que j'ai prouvé que les solutions que j'ai mentionné ne pas répondre à ces exigences.

Un indice? Merci de me fournir une solution, je ne veux pas penser que cette langue ne me permet pas de mettre en œuvre correctement un tel trivial concept.

  • double de stackoverflow.com/questions/4992307/...
  • bien que le titre est très similaire, les questions sont à mon humble avis différents.
  • Bon en double, mais le texte de cette question est précieux en soi.
  • Voudriez-vous pas un constructeur privé? La réflexion sur ce problème un temps, j'ai pensé que le meilleur moyen d'encapsuler la création de l'objet était d'utiliser un gestionnaire de classe, ce qui était un ami d'un constructeur privé. Ce serait forcer de tout objet extérieur par le biais de l'usine de fonctions que le gestionnaire de la propriété.
  • non, plutôt "allez, vous êtes comme une marque de voiture, comment se fait je ne trouve pas une seule voiture qui n'est pas cassé? ils vendent des bonnes voitures partout ailleurs et j'attends la même chose de vous".
  • Je pense que votre premier exemple est vraiment décrivant le "nommé constructeur" l'idiome pas un modèle de fabrique (en.wikibooks.org/wiki/More_C%2B%2B_Idioms/Named_Constructor)
  • Je suis d'accord que cette question est plus complet, mais il est néanmoins essentiellement la même question: "Comment puis-je utiliser correctement l'usine modèle de conception en C++?"
  • Le point de mon commentaire, c'est que cette question doit rester récupérés et non fusionné, même si elle est fermée comme un doublon.
  • Umm, na pas vous dire: Foo* FooFactory::createDynamicFooInSomeWay(); Foo FooFactory::createStaticFooInSomeWay()
  • J'ai pris la liberté de modifier l'exemple de ce qui question d'origine. J'allais à l'appel de la seconde createStaticFooInSomeWay(), mais ce n'était pas de droite c'est plus comme createDynamicallyAllocatedCFooInsomewayandreturnptr(), et createFooValueAndReturnCopy(), mais ceux-ci sont trop verbeux. (Si il y a une convention de nommage pour ce genre de chose, je voudrais entendre parler d'elle.)
  • Merci pour le modifier, c'est une bonne prise.
  • Deux ans après avoir demandé cela, j'ai quelques points à ajouter: 1) Cette question est pertinente pour plusieurs modèles de conception ([résumé] de l'usine, de constructeur, vous le nom, je n'aime pas fouiller dans leur taxonomie). 2) Le problème réel dont il est ici question est "comment proprement dissocier l'objet d'allocation de stockage de construction de l'objet?".
  • Euh, vous êtes à l'alinéa 2) ne permet pas de décrire static de répartition, c'est l'allocation de pile, alias local, aka auto en C++
  • Ne pas l'allocation dynamique de l'option de vous donner en C++ créer une fuite de mémoire?
  • seulement si vous n'avez pas delete il. Ce genre de méthodes sont parfaitement bien, tant qu'il est "documenté" (code source de la documentation 😉 ) que l'appelant prend possession du pointeur (lire: est responsable de la suppression au moment opportun).
  • vous pourriez aussi le faire très explicite par le retour d'un unique_ptr<T> au lieu de T*.
  • Je ne comprends pas ce qui est le point d'une usine lorsque vous n'êtes pas en utilisant des pointeurs: si vous n'êtes pas en utilisant les pointeurs (et, par conséquent, polymorphisme), alors vous devez connaître le type de béton lorsque vous avez écrit le code. Alors, pourquoi avez-vous besoin d'une usine, alors? Et si vous êtes à la pensée de la farce de la pile () de l'objet en un pointeur, eh bien, c'est un peu impliqué et je ne vois pas où/quand il pourrait être utile (même si, ne jamais dire jamais).
  • Je me trouve dans cette position parce que je veux unittest une classe qui crée des objets à l'intérieur de certaines de ses méthodes, et je n'ai pas tout tas. J'ai donc besoin de l'allocation de pile, et j'ai besoin de pouvoir varier les types selon qu'il s'agit de production ou de test de code.

InformationsquelleAutor Kos | 2011-02-25