Bonjour,
je propose un ajout dans la faq "Quelles sont les différences fondamentales entre le constructeur d'une classe et sa méthode Init() ?"
Il est écrit qu'il existe 2 solutions.
J'en vois une troisième: le scope guard.
Version imprimable
Bonjour,
je propose un ajout dans la faq "Quelles sont les différences fondamentales entre le constructeur d'une classe et sa méthode Init() ?"
Il est écrit qu'il existe 2 solutions.
J'en vois une troisième: le scope guard.
Où donc veux-tu en venir?
Il est écrit:Le scope guard est également une solution qui permet de régler ce problème (bien que ce ne soit pas fait pour ça à l'origine):Citation:
Si init lève une exception, la mémoire (ou autres ressources) allouée pour t n'est jamais libérée
C'est que parfois (souvent même j'ai l'impression) on ne peut pas acquérir tout dès le constructeur.Code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25 class ScopeGuard { public: ScopeGuard () : engaged_ (true) {} ~ScopeGuard () { if (engaged_) { /* libération des ressources ici */} } bool init( /* params */ ) { // initialisation et acquisition ici, avec une gestion correcte des allocations (et libération en cas d'échec) // si tout s'est bien passé engaged_ = true; return true; } private: bool engaged_; };
Si tu ne peux pas tout acquérir dès le constructeur, c'est que tu veux construire une voiture sans avoir fabriqué ou acheté le moteur.
Ce que tu ne peux pas avoir automatiquement, demande le en argument, et tout devient simple.
Si ce n'est pas encore le cas, raiie-le (*).
Si c'est encore impossible, je pense que l'architecture est mal pensée.
Je m'explique:
Si j'ai besoin d'initialiser quelque chose que j'ai déjà construit, dans quel état était-il?
Un objet est toujours dans un état connu et utilisable, il doit être construit ou ne pas être.
* tiens, je l'aime bien, mon néologisme
C'est pas une troisième méthode, c'est une implémentation particulière de init().
Qui peut rapidement poser des problèmes : si tu as plusieurs variables à initialiser, tu devras créer une variable bool pour chaque variable membre que tu veux initialiser, ce qui sera vite ingérable.
La solution, c'est le RAII, c'est à dire initialiser dans le constructeur (et comme le dit leternel, si c'est pas possible, voir s'il n'y a pas un problème de conception).
Et utiliser des pointeurs intelligents (voir pas de pointeur) pour les variables membres.
Par contre, il faudra quand même mettre à jour cette faq (en fait, l'ensemble de la faq) pour le C++11 (auto_ptr...)
@leternel: J'ai du mal à comprendre ton message...
Selon toi, il serait interdit de construire un objet partiellement puis de le remplir ensuite par aggregation? L'idiome scope guard n'a donc pas lieu d'être?
Pas interdit, le C++ est un langage permissif...
Mais non respectueux du RAII, donc moins sur, moins robuste, moins lisible, etc
L'approche C++ : le RAII
Architecture Logicielle & Développement - RAII !
Pas de problème, c'est bien le même monde : je passe beaucoup plus de temps à maintenir de l'existant que de partir d'une feuille (de code) vierge ;)Citation:
Envoyé par r0d
Après, effectivement, si on peut pas modifier un code existant pour mettre en place le RAII, que l'on peut pas utiliser des pointeurs intelligents (fournit pas le C++11, boost, Qt, ou même que l'on aurait créé), cela devient compliqué à faire du code robuste.
Le code que tu présentes posera vite des problèmes à mon sens (peut être d'autres auront un avis différents). Pour les objets complexes, j'aurais tendance à préféré des DP composite, builder, etc. Ou un DP wrapper pour sécuriser/isoler un code non robuste (typiquement une lib C utilisée en C++), etc
Pour revenir plus spécifiquement à la FAQ, il me semble que c'est pas une correction à faire (parce que c'est pas une troisième solution, mais une implémentation spécifique de init ; et qu'il faut bien faire des choix quand au contenu de la faq)
Salut,
En fait, nous pourrions peut etre en effet parler du scope guard, tout en précisant clairement qu'il s'agit d'une manière de travailler qui devrait être réservée au cas "exceptionnel" dans lequel on n'a pas d'autre solution (comme la maintenance d'un code existant), mais qui est symptomatique d'un problème de conception à la base.
Je suis le premier à prôner le fait qu'un objet se doit d'être directement dans un état cohérent dés la fin du constructeur, mais j'ai aussi rencontré suffisamment de code pour savoir qu'il est parfois difficile de faire en sorte que ce soit toujours le cas ;)
Personnellement, au boulot, je refuse systématiquement d'écrire du code qui n'est pas "constructor coherent".
Et chaque fois, c'est accepté, et je fais sauter un millier de lignes de code (oui, le projet est monstrueux…)
D'ailleurs, en général, quand j'en parle tous les développeurs semblent d'accord pour reconnaître que le code est "mal écrit" dans ces portions.
La seule "exception" est les "arguments nommés", que j'utilise pour la construction de requete sql, par exemple.
Encore que, "construction en cours" peut-être vu comme un état valide d'un "RequestBuilder"
Il y a quelque chose qui m'échappe dans tout ça.
Déjà, dans ladite faq, je lis:Selon vous, cette phrase est à bannir? Et donc toute la faq? Puisque si je comprend bien, vous êtes en train de dire que la fonction init() ne devrait pas exister? Répondez-moi s'il vous plait, je ne comprend pas votre position.Citation:
Dans certains cas, on n'a pas assez d'infos à la construction de l'objet pour tout initialiser [...]
Vous n'êtes jamais tombé sur des problèmes où vous n'avez pas ce qu'il faut pour construire un objet complètement au moment où l'on a pourtant besoin de lui?
gbdivers par exemple, je lis que tu es bio-informaticien. Tu dois bien avoir rencontré des problèmes de data-mining où tu dois construire une structure à partir de set de données partiellement complets non? Et encore ça s'est rien, le vrai problème c'est lorsqu'on doit construire un modèle à partir de ces données incomplètes et qu'on ne sait pas à l'avance de quelle façon on va construire cet objet.
Quant à "supprimer des milliers de ligne de code", parfois c'est juste pas possible. Par exemple (et sans parler du cas - pourtant fréquent - du problème de temps), lorsque ces milliers de lignes de code effectuent un traitement tellement complexe qu'il nous faudrait des années pour seulement comprendre ce que fait ce code.
A voir tes questions, je crois que l'on pas été assez précis et exacte sur les explications.
En fait, la construction est différent de l'initialisation. La fonction init est surement mal nommé, son but n'est pas d'initialiser la classe (c'est ce que fait assign pour vector par exemple), mais de la "construire" (selon la définition de koala01, en particulier que l'invariant de classe soit respecté à la fin du constructeur)Citation:
un objet se doit d'être directement dans un état cohérent dés la fin du constructeur
Il n'est pas aberrant de créer un conteneur vide et de le remplir avec des push_back, on sait bien qu'il n'est pas possible dans la plupart des cas de créer le vecteur directement rempli.
Ici on parle bien d'utiliser une fonction init en remplacement du constructeur, donc une fonction qui chargée de mettre la classe dans un étant cohérent. Ce qui implique que l'objet n'est pas dans un étant cohérent entre l'appel du constructeur et l'appel de init (si c'est pas le cas, alors c'est que le constructeur a fait son boulot et que la fonction init n'est pas en remplacement du constructeur, mais une vraie fonction d'initialisation)
Il faut peut être reformuler certaines phrases...
auto_ptr<> est une capsule à la RAII. Aujourd'hui on dirait unique_ptr. C'est un scoped_guard. Un peu particulier certes, mais cela en est un. C'est ici un détail par rapport à comment écrire cette sorte de factory-method qui fait construction + post-init polymorphiqme.
Sinon, bonne idée que de déterrer ce point de FAQ car il n'adresse pas clairement ce qui est vraiment important.
init() est un truc hérité de l'enseignement syntaxique par delta du C++ (j'invente les mots ; mais ils viennent d'une rumination pour une présentation que j'ai faite dans la boite pour inciter de migrer du C++ historique au C++ moderne). Au départ le C dont on fait vite le tour de la syntaxe. Le Pascal omniprésent dans l'éducation (chez nous du moins). Et des profs dans les années fin 80 à 90+ qui veulent présenter le C++.
Donc il partent du C. Voilà une structure, on en fait un premier objet. Souvenez-vous les structures il faut les initialiser. Pouf une fonction init(). Ils continuent à présenter divers trucs et puis à un moment ils reviennent sur l'initialisation des données (notez le soucis au passage : la POO part d'une agrégation de données, et non de services rendus) et disent: ouais mais en fait il y a un truc fait pour initialiser -> les constructeurs.
[notez l'approche itérative, d'où le "delta", qui ne s'intéresse qu'à présenter la syntaxe]
Sauf que c'est déjà trop tard, les élèves ont eu deux semaines de TP entre temps à avoir utilisé des fonctions init() qui semblent remplir le besoin. Ils ont déjà pris la mauvaise habitude et sans même savoir pourquoi c'est une mauvaise habitude, parce que l'on ne leur a jamais parlé de cet invariant dynamique/"structurel" (pas le bon mot)
En effet, la fonction init() oblige à gérer l'invariant "objet utilisable" de façon dynamique : avec un membre isReady/isInitialized, et un test de cette variable en début de chaque fonction. Tandis que l'approche constructeur nous assure un invariant statique : "par construction" (souvenez-vous des démonstrations en maths, c'est le même "par construction"), nous sommes assurés que si l'objet existe alors il est utilisable => pas de test à faire!
Ce qui me fait penser à cette analyse d'une interview de Stroustrup, où il est cité à dire: http://www.theregister.co.uk/2012/12...n_c_plus_plus/
L'invariance. C'est la clé du pourquoi les fonctions init c'est mal.Citation:
Envoyé par Stroustrup
Au final, je vois deux intérêts à init() :
- la post-initialisation polymorphique, dont le point de FAQ parle
- le code pré-exceptions -> je vous renvoie aux règles de codage chez google qui interdisent les exceptions (car base de code sans exceptions trop volumineuse pour changer ça) et qui demandent donc à utiliser des fonctions init().
Mais init() reste le cousin pauvre du constructeur. À bannir par défaut.
[Ce peut valoir le coup d'insister sur cette histoire d'invariance dans le point de FAQ]
Juste une remarque. Si ce test est réellement effectué pour toutes les fonctions, alors a priori, la classe est toujours cohérente (on a bien la validation systématique faite pas le concepteur de la classe et pas par l'utilisateur, simplement l'invariant de classe I devient "si isInitialized alors I, sinon rien" et il est toujours respecté, pas d'états et de comportements indéterminés)Citation:
et un test de cette variable en début de chaque fonction
En pratique, c'est jamais fait comme ça (surcout, oubli), d'ou la nécessité de l'appel d'init par l'utilisateur (et la faq dans ce sens, avec la phrase "l'utilisateur doit l'appeler manuellement (risque d'oubli)" sur le risque d'erreur par l'utilisateur)
En fait, il s'agit plus d'une précondition pour tout usage d'un objet autre que l'appel à init et d'une postcondition pour le fait d'appeler init ;)
Mais la question se pose alors dans ces termes :
La réponse classique à cette question est généralementCitation:
Que va-t-il se passer si la précondition n'est pas respectée :question:
Et on l'en revient à un problème similaire à ce qui fait que l'on préfère les références aux pointeurs "chaque fois que possible" car cela sous entend que toute fonction commence par ... un gros test d'initialisation (le lien entre un if(ptr) et un if (object.initialized() ) est flagrant :D )Citation:
Ben, ca, mon vieux, tant pis pour toi... avec beaucoup de chance, ca passera, sinon...:aie:
Edit Cependant, on ne peut pas prétendre qu'un objet sur lequel init n'aurait pas été appelé soit "cohérent".
La preuve en est que, si init n'a pas été appelée sur un objet, on ne peut absolument pas préjuger du résultat et qu'il faut donc s'assurer de la cohérence de l'objet au travers du test ;)
Il me semble, en effet. D'ailleurs, vous ne répondez pas à mes questions.
Je vais essayer de trouver une formulation avec laquelle nous soyons tous d'accord: l'init c'est comme les pointeurs, à n'utiliser que lorsqu'on ne peut pas faire autrement (formulation qui sous-entend que, parfois, on ne peut peut pas faire autrement).
Après, Luc a raison sur le fait que derrière cette discussion, il y a un problème plus théorique concernant l'état d'un objet. Je suis bien incapable de formuler cela clairement, mais en gros, je pense que ce qui est important c'est l'état de l'objet (à un instant donné), et pas la façon dont l'objet est construit et/ou modifié. Donc l'invariance, mais peut-être pas uniquement (d'ailleurs, en parlant d'invariance, il serait peut-être bien de parler des assertions dans la faq?).
Il y a également un problème de définitions:
- Quand est-ce qu'un objet est "utilisable"? En effet, on peut imaginer un objet construit partiellement, avec une sémantique telle que si certaines partie manquent cela ait du sens (un exemple: sachant que telle donnée est manquante, cela permet de se situer dans l'arbre d'un traitement sur lequel on a pas le contrôle. D'ailleurs, de façon plus globale, l'absence de donnée a en fait parfois du sens.).
- Qu'est-ce que l'invariance en c++? Je n'ai trouvé aucune définition qui me convienne. Une classe (un type en général) n'est pas un invariant en soi, mais elle définit des invariants. Mais comment appréhender ces invariants lorsqu'on a une classe qui évolue (par exemple une machine à état).
- Qu'entendez-vous par "constructor coherent"?
Enfin, un point qui n'est toujours pas clair pour moi: quid des constructeurs pas défaut? On est très souvent obligé de passer par un constructeur par défaut (stockage dans un conteneur template, basiquement). Est-ce que, dans ce cas, une fonction init() n'est pas envisageable? Un exemple de code pour illustrer ma question:
Code:
1
2
3
4
5
6 main() { std::vector<MyObject> v(5); // arrivé là, nos 5 objets sont déjà construits via le default ctor. // Ils ne sont pas forcément utilisables directement pour autant }
S'ils ne sont pas utilisables, c'est parce que le constructeur par défaut est pourri, car il ne fait pas ce qu'il faut.Code:
1
2
3
4
5
6 main() { std::vector<MyObject> v(5); // arrivé là, nos 5 objets sont déjà construits via le default ctor. // Ils ne sont pas forcément utilisables directement pour autant }
Si tu fais un std::vector<char> v(5); ton vector contient cinq char, pleinement utilisables.
un objet est utilisable si toute fonction membre est appelable et n'échoue pas hors interface.
Par exemple, si tu as un std::fstream, il est forcément utilisable. Sinon, le constructeur aurait throwé, et tu n'aurais pas ton fstream.
- Invariant : N'importe quel propriété P que doit vérifier un objet de type T.
- propriété : N'importe quel assertion (vérifiable ? (*)) sur un objet.
- ctor coherent : Dans le contexte, un constructeur qui, si aucune exception n'est lancée, va créer un objet respectant les invariants associés au type (sinon rien n'est crée).
Au final l'intérêt pratique des fonctions init est assez faible je pense :
- Pour le code interdit d'exception
- Pour le besoin de ctor polymorphe comme l'a expliqué Luc, cependant je me demande si il existe autant de cas que ça où ce besoin est bien fondé et pas lié à une conception bancale.
- En fonction à visibilité restreinte pour factoriser le code, inutile en C++11 (delegating ctor).
Pour les exemples de BDD, data mining, etc, c'est quand même aux développeur de fixer les invariants de sa classe (tout en restant cohérant), il a aussi le pouvoir d'introduire des intermédiaires (aux invariants plus faible), au final je suis convaincu que l'on peut aboutir à des designs sur ces éléments sans avoir besoin d'init.
(*) De manire "théorique" le vérifiable ne sert à rien, de manière pratique, le résultat d'une assertion non vérifiable ne doit pas avoir réellement d'impact.
Je craint que cela ne suffise pas, du moins pas dans le domaine du développement logiciel. Prenons l'exemple d'un objet qui a deux états et une fonction doSomething(). Cette fonction fera des choses différentes selon l'état de l'objet; par conséquent, notre objet aura des propriétés différentes selon son état. Pourtant, au niveau du paradigme objet, le type MonObjet::doSomething() sera invariant (une fonction peut être considéré comme un type).
Cela ne suffit peut etre pas, mais, si doSomething réagit différemment en fonction de l'état dans lequel est l'objet tout en fournissant un résultat final similaire, c'est que les deux états peuvent être considérés comme cohérents.
Le problème avec init, c'est que, a priori, l'objet est dans un état incohérent avant d'avoir été initialisé : une personne qui n'aurait pas de nom, ou un objet qui ne dispose pas des "caractéristiques minimales" qui permettent de le considérer comme tel.
Or, a priori, ce n'est pas à l'utilisateur de veiller à ce que l'objet reçu soit dans un état cohérent, mais bien à la classe, qui doit s'assurer que l'objet créé sera dans un état cohérent dés le moment où l'objet est créé :aie:
Ok alors, concrètement, qu'est-ce qu'un état cohérent?
C'est ce que j'essaie de montrer: ce n'est pas aussi simple de déterminer ces notions de "utilisable", "cohérent", etc.
Par exemple, il ne me semble pas absurde d'imaginer qu'une instance de classe puisse être utile même si tous ces champs (variables membres) ne sont pas remplis. Si récupère le champ nom (via l'accesseur getName) et que c'est une chaine vide, ce n'est pas forcément un problème et on peut peut-être continuer nos traitements, dans le cas où l'information n'est pas indispensable. Si maintenant getName() accède a des ressources externes susceptibles de lever une exception, on peut, de la même manière, imaginer récupérer cette exception et continuer nos traitements.
Avant de continuer, y a-t-il une hérésie dans la situation que je décris dans ce paragraphe?
@r0d: Je ne vois pas en quoi ma définition est insuffisante.
Si ta fonction fait des choses différentes selon l'état, alors c'est qu'elle ne travaille pas seulement selon des invariants, rien de plus.
Je ne suis pas chercheur en informatique théorique (même pas chercheur tout court), mais je suis convaincu que tu dois avoir des gens qui ont déjà définies tout ça de manière très rigoureuse (sans trop réfléchir, je dirais théorie des graphes et théorie des ensembles).
Pour ton dernier message, un champs vide peut-être un état valide, c'est à toi, développeur, de choisir l'ensemble de tes états valides. Pas exemple, les pointeurs intelligents et les conteneurs, ont un état valide où ils ne référencent/contiennent rien.
Par contre se retrouver avec un objet dont l'état n'est valide que pour une fonction (init) et ne sera valide qu'après un appel à cette fonction pour les autres fonctions, ca doit inciter à se demander si il n'y a pas une mauvaise répartition invariant/pré-conditions. Typiquement, est-ce que l'introduction d'un type supplémentaire ne permettrait pas de résoudre le problème ?
Après l'idéal serait que BeforeA ai une réalité sémantique, ça reste envisageable selon la situation selon moi.Code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27 //Avec init struct A { //Invariant A void init(/*needed params C*/) { /*Post-Condition B*/ } void foo() { /*Pre-Condition B*/ } } //Avec intérmédiaire struct BeforeA { //Invariant A } struct A { //Invariant A+B A(BeforeA, /*needed params C*/) { } void foo() { } }