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.
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):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 : Sélectionner tout - Visualiser dans une fenêtre à part
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) viergeEnvoyé 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![]()
A méditer: La solution la plus simple est toujours la moins compliquée
Ce qui se conçoit bien s'énonce clairement, et les mots pour le dire vous viennent aisément. Nicolas Boileau
Compiler Gcc sous windows avec MinGW
Coder efficacement en C++ : dans les bacs le 17 février 2014
mon tout nouveau blog
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.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)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.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]
Blog|FAQ C++|FAQ fclc++|FAQ Comeau|FAQ C++lite|FAQ BS|Bons livres sur le C++
Les MP ne sont pas une hotline. Je ne réponds à aucune question technique par le biais de ce média. Et de toutes façons, ma BAL sur dvpz est pleine...
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)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éralementQue va-t-il se passer si la précondition n'est pas respectée![]()
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 flagrantBen, ca, mon vieux, tant pis pour toi... avec beaucoup de chance, ca passera, sinon...![]()
)
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![]()
A méditer: La solution la plus simple est toujours la moins compliquée
Ce qui se conçoit bien s'énonce clairement, et les mots pour le dire vous viennent aisément. Nicolas Boileau
Compiler Gcc sous windows avec MinGW
Coder efficacement en C++ : dans les bacs le 17 février 2014
mon tout nouveau blog
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 : Sélectionner tout - Visualiser dans une fenêtre à part
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 : Sélectionner tout - Visualiser dans une fenêtre à part
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).
Partager