web-dev-qa-db-fra.com

Pourquoi utiliser une méthode d'initialisation au lieu d'un constructeur?

Je viens d'entrer dans une nouvelle entreprise et une grande partie de la base de code utilise des méthodes d'initialisation au lieu de constructeurs.

struct MyFancyClass : theUberClass
{
    MyFancyClass();
    ~MyFancyClass();
    resultType initMyFancyClass(fancyArgument arg1, classyArgument arg2, 
                                redundantArgument arg3=TODO);
    // several fancy methods...
};

Ils m'ont dit que cela avait quelque chose à voir avec le timing. Que certaines choses doivent être faites après construction qui échouerait dans le constructeur. Mais la plupart des constructeurs sont vides et je ne vois vraiment aucune raison de ne pas utiliser de constructeurs.

Je me tourne donc vers vous, oh sorciers du C++: pourquoi utiliseriez-vous une méthode init au lieu d'un constructeur?

46
bastibe

Puisqu'ils disent "timing", je suppose que c'est parce qu'ils veulent que leurs fonctions init puissent appeler des fonctions virtuelles sur l'objet. Cela ne fonctionne pas toujours dans un constructeur, car dans le constructeur de la classe de base, la partie classe dérivée de l'objet "n'existe pas encore", et en particulier vous ne pouvez pas accéder aux fonctions virtuelles définies dans la classe dérivée. Au lieu de cela, la version de classe de base de la fonction est appelée, si elle est définie. S'il n'est pas défini (ce qui implique que la fonction est purement virtuelle), vous obtenez un comportement non défini.

L'autre raison courante des fonctions init est le désir d'éviter les exceptions, mais c'est un style de programmation assez old-school (et si c'est une bonne idée est un argument en soi). Cela n'a rien à voir avec des choses qui ne peuvent pas fonctionner dans un constructeur, mais plutôt avec le fait que les constructeurs ne peuvent pas retourner une valeur d'erreur si quelque chose échoue. Donc, dans la mesure où vos collègues vous ont donné les vraies raisons, je suppose que ce n'est pas ça.

66
Steve Jessop

Oui, j'en pense à plusieurs, mais ce n'est généralement pas une bonne idée.

La plupart du temps, la raison invoquée est que vous ne signalez les erreurs que par le biais d'exceptions dans un constructeur (ce qui est vrai) alors qu'avec une méthode classique, vous pouvez renvoyer un code d'erreur.

Cependant, dans un code OO correctement conçu, le constructeur est responsable de l'établissement des invariants de classe. En autorisant un constructeur par défaut, vous autorisez une classe vide, vous devez donc modifier les invariants pour que soit acceptée à la fois la classe "null" et la classe "significative" ... et chaque utilisation de la classe doit d'abord s'assurer que l'objet a été bien construit ... c'est grossier.

Alors maintenant, démystifions les "raisons":

  • J'ai besoin d'utiliser une méthode virtual: utilisez l'idiome Virtual Constructor.
  • Il y a beaucoup de travail à faire: alors quoi, le travail sera fait de toute façon, il suffit de le faire dans le constructeur
  • La configuration peut échouer: lever une exception
  • Je veux conserver l'objet partiellement initialisé: utilisez un try/catch dans le constructeur et définissez la cause de l'erreur dans un champ d'objet, n'oubliez pas de assert au début de chaque méthode publique pour vous assurer que l'objet est utilisable avant d'essayer de l'utiliser.
  • Je veux réinitialiser mon objet: invoquez la méthode d'initialisation à partir du constructeur, vous éviterez le code en double tout en ayant un objet entièrement initialisé
  • Je veux réinitialiser mon objet (2): utilisez operator= (et l'implémenter en utilisant l'idiome de copie et d'échange si la version générée par le compilateur ne correspond pas à vos besoins).

Comme dit, en général, mauvaise idée. Si vous voulez vraiment avoir un constructeur "void", faites-les private et utilisez les méthodes Builder. C'est aussi efficace avec NRVO ... et vous pouvez retourner boost::optional<FancyObject> en cas d'échec de la construction.

29
Matthieu M.

D'autres ont énuméré de nombreuses raisons possibles (et des explications appropriées pour lesquelles la plupart d'entre elles ne sont généralement pas une bonne idée). Permettez-moi de poster un exemple de ne utilisation (plus ou moins) valide des méthodes init, qui a en fait à voir avec le timing.

Dans un projet précédent, nous avions beaucoup de classes et d'objets Service, chacun faisant partie d'une hiérarchie, et se croisant de différentes manières. Donc, généralement, pour créer un ServiceA, vous aviez besoin d'un objet de service parent, qui à son tour avait besoin d'un conteneur de service, qui dépendait déjà de la présence de certains services spécifiques (y compris éventuellement ServiceA lui-même) au moment de l'initialisation. La raison en était qu'au cours de l'initialisation, la plupart des services se sont enregistrés auprès d'autres services en tant qu'écouteurs d'événements spécifiques et/ou ont notifié d'autres services de l'événement d'initialisation réussie. Si l'autre service n'existait pas au moment de la notification, l'enregistrement n'a pas eu lieu, ce service ne recevrait donc pas de messages importants ultérieurement, lors de l'utilisation de l'application. Afin de briser la chaîne des dépendances circulaires, nous avons dû utiliser des méthodes d'initialisation explicites distinctes des constructeurs, donc efficacement faire de l'initialisation du service global un processus en deux phases.

Ainsi, bien que cet idiome ne devrait pas être suivi en général, à mon humble avis, il a des utilisations valables. Cependant, il est préférable de limiter son utilisation au minimum, en utilisant autant que possible des constructeurs. Dans notre cas, il s'agissait d'un projet hérité, et nous ne comprenions pas encore pleinement son architecture. Au moins, l'utilisation des méthodes init était limitée aux classes de service - les classes régulières étaient initialisées via des constructeurs. Je pense qu'il pourrait y avoir un moyen de refactoriser cette architecture pour éliminer le besoin de méthodes d'initialisation de service, mais au moins je n'ai pas vu comment le faire (et pour être franc, nous avions des problèmes plus urgents à régler à l'époque où j'étais partie du projet).

16
Péter Török

Deux raisons auxquelles je peux penser du haut de ma tête:

  • Dire que créer un objet implique beaucoup, beaucoup de travail fastidieux qui peut échouer de beaucoup de manières horribles et subtiles. Si vous utilisez un constructeur court pour configurer des éléments rudamentaires qui n'échoueront pas, puis que l'utilisateur appelle une méthode d'initialisation pour effectuer le gros travail, vous pouvez au moins être sûr que vous avez créé un objet même si le gros travail échoue. . Peut-être que l'objet contient des informations sur la façon précise dont l'initialisation a échoué, ou peut-être qu'il est important de conserver les objets initialisés sans succès pour d'autres raisons.
  • Parfois, vous souhaiterez peut-être réinitialiser un objet longtemps après sa création. De cette façon, il suffit d'appeler à nouveau la méthode d'initialisation sans détruire et recréer l'objet.
7
gspr

Une autre utilisation d'une telle initialisation peut être dans le pool d'objets. Fondamentalement, vous demandez simplement l'objet à partir du pool. Le pool aura déjà créé N objets qui sont vides. C'est maintenant l'appelant qui peut appeler n'importe quelle méthode qu'il souhaite pour définir les membres. Une fois que l'appelant a fini avec l'objet, il dira au pool de le destoryer. L'avantage est que jusqu'à ce que l'objet soit utilisé, la mémoire sera sauvegardée et l'appelant peut utiliser sa propre méthode membre appropriée pour initialiser l'objet. Un objet peut avoir une grande utilité mais l'appelant peut ne pas avoir besoin de tout, et peut également ne pas avoir besoin d'initialiser tous les membres des objets.

Pensez généralement aux connexions à la base de données. Un pool peut avoir un tas d'objets de connexion, et l'appelant peut remplir le nom d'utilisateur, le mot de passe, etc.

5
Manoj R

la fonction init () est bonne lorsque votre compilateur ne prend pas en charge les exceptions ou que votre application cible ne peut pas utiliser de segment de mémoire (les exceptions sont généralement implémentées à l'aide d'un segment de mémoire pour les créer et les détruire).

les routines init () sont également utiles lorsque l'ordre de construction doit être défini. Autrement dit, si vous allouez globalement des objets, l'ordre dans lequel le constructeur est appelé n'est pas défini. Par exemple:

[file1.cpp]
some_class instance1; //global instance

[file2.cpp]
other_class must_construct_before_instance1; //global instance

La norme ne garantit pas que le constructeur de must_construct_before_instance1 sera invoqué avant le constructeur de instance1. Lorsqu'il est lié au matériel, l'ordre dans lequel les choses s'initialisent peut être crucial.

5
TRISAbits

Plus d'un cas particulier: si vous créez un écouteur, vous voudrez peut-être le faire s'enregistrer quelque part (comme avec un singleton ou une interface graphique). Si vous faites cela pendant son constructeur, il fuit un pointeur/référence vers lui-même qui n'est pas encore sûr, car le constructeur n'est pas terminé (et peut même échouer complètement). Supposons que le singleton qui collecte tous les écouteurs et leur envoie des événements lorsque les choses se produisent, reçoit et événement, puis parcourt sa liste d'écouteurs (l'un d'eux est l'instance dont nous parlons), pour leur envoyer un message à chacun. Mais cette instance est encore à mi-chemin dans son constructeur, donc l'appel peut échouer de toutes sortes de mauvaises manières. Dans ce cas, il est logique d'avoir un enregistrement dans une fonction distincte, ce que vous faites évidemment pas appel du constructeur lui-même (ce qui irait à l'encontre du but), mais de l'objet parent, après la construction terminé.

Mais c'est un cas spécifique, pas le cas général.

1
Kajetan Abt

C'est utile pour faire de la gestion des ressources. Supposons que vous ayez des classes avec des destructeurs pour désallouer automatiquement les ressources lorsque la durée de vie de l'objet est terminée. Supposons que vous ayez également une classe qui contient ces classes de ressources et que vous les initiez dans le constructeur de cette classe supérieure. Que se passe-t-il lorsque vous utilisez l'opérateur d'affectation pour lancer cette classe supérieure? Une fois le contenu copié, l'ancienne classe supérieure devient hors contexte et les destructeurs sont appelés pour toutes les classes de ressources. Si ces classes de ressources ont des pointeurs qui ont été copiés lors de l'affectation, tous ces pointeurs sont désormais de mauvais pointeurs. Si vous lancez à la place les classes de ressources dans une fonction d'initialisation distincte de la classe supérieure, vous évitez complètement que le destructeur de la classe de ressources soit appelé, car l'opérateur d'affectation n'a jamais à créer et supprimer ces classes. Je pense que c'est ce que l'on entendait par l'exigence de "calendrier".

1
Christopher

Et aussi j'aime joindre un exemple de code pour répondre # 1 -

Depuis aussi msdn dit:

Lorsqu'une méthode virtuelle est appelée, le type réel qui exécute la méthode n'est sélectionné qu'au moment de l'exécution. Lorsqu'un constructeur appelle une méthode virtuelle, il est possible que le constructeur de l'instance qui appelle la méthode ne se soit pas exécuté.

Exemple: L'exemple suivant montre l'effet de la violation de cette règle. L'application de test crée une instance de DerivedType, ce qui entraîne l'exécution de son constructeur de classe de base (BadlyConstructedType). Le constructeur de BadlyConstructedType appelle incorrectement la méthode virtuelle DoSomething. Comme le montre la sortie, DerivedType.DoSomething () s'exécute et le fait avant l'exécution du constructeur de DerivedType.

using System;

namespace UsageLibrary
{
    public class BadlyConstructedType
    {
        protected  string initialized = "No";

        public BadlyConstructedType()
        {
            Console.WriteLine("Calling base ctor.");
            // Violates rule: DoNotCallOverridableMethodsInConstructors.
            DoSomething();
        }
        // This will be overridden in the derived type.
        public virtual void DoSomething()
        {
            Console.WriteLine ("Base DoSomething");
        }
    }

    public class DerivedType : BadlyConstructedType
    {
        public DerivedType ()
        {
            Console.WriteLine("Calling derived ctor.");
            initialized = "Yes";
        }
        public override void DoSomething()
        {
            Console.WriteLine("Derived DoSomething is called - initialized ? {0}", initialized);
        }
    }

    public class TestBadlyConstructedType
    {
        public static void Main()
        {
            DerivedType derivedInstance = new DerivedType();
        }
    }
}

Sortie:

Appelant le ctor de la base.

DoSomething dérivé est appelé - initialisé? Non

Appel de ctor dérivé.

1
Tarik