Pourquoi initialiser les variable membre d'une class/struct

Merci pour vos explications.

Le problème exposé dans ce sujet a été résolu.

Bonjour,

Je ne voudrais pas polluer le post de notre ami Florian, aussi je crée celuici, ici.

Dans une réponse du post-source, il est préconisé de faire une initialisation des variables membres dans la définition de la class, c’est-à-dire, si j’ai bien compris :

File HPP:
#ifndef POINT_HPP_INCLUDED
#define POINT_HPP_INCLUDED

struct Point {
    Point ();
    Point (double const x, double const y);
    double x_ = 0.;
    double y_ = 0.;
};

#endif // POINT_HPP_INCLUDED

file CPP:
#include "point.hpp"

Point::Point ()
{}

Point::Point (double const x, double const y) : x_{x},y_{y}
{}

Dans l’objectif de réduire les interfaces, j’aurais plutôt écrit :

File HPP:
#ifndef POINT_HPP_INCLUDED
#define POINT_HPP_INCLUDED

struct Point {
    Point (double const x=0.0, double const y=0.0);
    double x_;
    double y_;
};

#endif // POINT_HPP_INCLUDED

file CPP:
#include "point.hpp"

Point::Point (double const x, double const y) : x_{x},y_{y}
{}

Y aurait-il quelques subtilités que je ne maitrise pas ? (Ou alors c’est sans importance, et je coupe les cheveux en quatre !

Cordialement.

(PS: Lyraty, "Couper les cheveux en quatre", c’est une expression argotique, française de France métropole qui veut dire "asticotè", "pinailler", …)

+0 -0

Effectivement, j’aurais tendance à dire que, moins on a de code, mieux c’est.

+0 -0

Personnellement, je trouve que la première version est légèrement plus explicite. Rien qu’en jetant un coup d’œil au header, on comprend immédiatement que les membres sont initialisés à zéro par défaut, et qu’il existe un constructeur sans arguments.

Un défaut avec la deuxième version, c’est que par la suite on peut écrire :

Point p(12);

Ce qui n’est pas très intuitif pour quelqu’un qui va lire le code. Mais en réalité, je trouve que Point p() n’est pas intuitif non plus, et au final je n’autoriserais donc probablement pas de construction sans argument avec des valeurs par défaut.

Hors sujet (mais important a noter), c’est qu’avoir plusieurs initialisation comme dans le premier code, ça n’a pas d’impact sur les performances. Une variable ne peut être initialisée qu’une fois, il n’y a pas de surcoût à initialiser à la déclaration.

Il y a 2 arguments dont je me souviens pour la première syntaxe. C’est juste des questions de maintenance du code.


  1. le code sera plus solide, si tu dois faire évoluer ton code. Par exemple, si tu écris :
struct Point {
    Point() : x_{0.0}, y_{0.0} {}
    Point(double x, double y) : x_{x}, y_{y} {}
    ... // ca manque de constexpr et d'explicit
};

Et que plus tard, tu ajoutes un nouveau constructeur, tu peux oublier d’initialiser une variable :

struct Point {
    Point() : x_{0.0}, y_{0.0} {}
    Point(double x) : x_{x} {}
    Point(double x, double y) : x_{x}, y_{y} {}
    ... // ca manque de constexpr
};

Bien sûr, sur un code aussi simple, le risque d’oublier une installation est très peu probable, mais sur un code plus complexe, où toutes les variables sont pas directement visibles quand on écrit un constructeur, qu’il y a beaucoup de variables, du code un peu plus complexe et moins lisible (des templates, de la doc, etc), le risque d’oublier une initialisation est plus importante.

Un point important à noter, c’est que comme cela produit un UB (la variable va prendre une valeur aléatoire), ça sera potentiellement un bug silencieux (pas de joli crash à l’endroit qui pose problème) et donc difficile à détecter et corriger.


  1. le second point, c’est la consistance des valeurs par défaut. Si tu initialises une variable par défaut a plusieurs endroit du code, alors tu peux faire l’erreur d’initialiser par défaut avec des valeurs différentes.
struct Point {
    Point() : x_{0.0}, y_{0.0} {}
    Point(double x) : x_{0.0}, y_{1.0} {}
    Point(double x, double y) : x_{x}, y_{y} {}
    ... // ca manque de constexpr
};

Encore une fois, avec un code aussi simple, peu de risque de se tromper. Mais sur un vrai code complexe, c’est facile de se tromper.

Avec une initialisation par défaut faite à un seul endroit du code, tu garanties la consistance des valeurs par défaut.


L’argument de Olybri est valide aussi, mais c’est un argument contre la seconde syntaxe (1 seul constructeur versus 2 constructeurs), pas trop un argument pour les initialisations par défaut.


moins on a de code, mieux c’est.

ache

Il ne faut jamais oublier, quand on parle de règle de bonne pratique comme cela, de pourquoi il y a ces règles.

Moins de code = moins de maintenance = moins de risque de bugs. En théorie. C’est un argument tout à fait valide.

Mais il s’oppose à d’autres arguments, comme avoir un code explicite (comme indiqué par Olybri) ou ceux que j’ai cité. Le design est souvent une question de compromis.


Ce qui n’est pas très intuitif pour quelqu’un qui va lire le code. Mais en réalité, je trouve que Point p() n’est pas intuitif non plus, et au final je n’autoriserais donc probablement pas de construction sans argument avec des valeurs par défaut.

Olybri

L’argument est valide. Je suis d’accord que, par exemple, une fonction Point::origin() serait plus explicite qu’un constructeur par défaut (non nommé).

Mais en pratique, pour des raisons concrètes, je mettrais quand meme ce constructeur par défaut : pour que ce soit defaut-constructible et ne pas avoir de problème avec std::vector par exemple.

+1 -0

Tu as raté mes messages dans le fil.

Pour un tel agrégat dépourvu de tout invariant, il n’y a pas de sens à s’enquiquiner à rajouter des constructeurs (constexpr + noexcept aussi!) en C++11 & suivants qui peuvent vite avoir pour effet de détrivialiser l’objet.

Le mieux, AMA c’est ça et uniquement ça

struct Point2D {
    double x;
    double y;
};

Et ça suffit amplement. On peut rajouter éventuellement des services autour (comparaison), différence qui donne un delta 2D, et addition avec des deltas pour faire des translations…

Coté initialisation cela suffit car le compilateur nous offre copie (ctr + affectation) et les initialisations avec.

Point2D a;
Point2D b{}; // et pas (), n'oublions pas
Point2D c{1, 2}; // plus = {42, 12}
Point2D d(1, 2); // ...
Point2D e{1};
Point2D f(1);

Le cas potentiellement gênant est que y est laissé dans un état indéterminé dans e et f.

Dans le cas a aussi, mais c’est volontaire: tout a est laissé dans un état non déterminé. Il y b qui suffit amplement pour offrir l’initialisation à 0.

Et en fait, c’est là qu’est le principal sujet de discussion: est-il acceptable de permettre d’avoir des instances de Point2D dans un état non initialisé? Mon opinion est que oui car il n’y que comme ça que l’on peut avoir des warnings du compilateur qui nous dira que l’on utilise des données non initialisées: car mis à 0 ne veut pas forcément dire mis à une valeur pertinente!

Si programme défensivement en initialisant les membres avec le =0 dans la définition de la classe, a est garantit initialisé, e et f ne sont pas dans des états bâtards, mais on perd l’assistance du compilo: cf l’entrée de la FAQ de dvpz à la question "où faut-il déclarer nos variables locales?" Tout ça est lié.

Les seuls cas où je vais avoir les constructeurs c’est si j’ai eu besoin d’écrire un quelconque autre constructeur ou si l’objet a vraiment un invariant, ce qui justifie la présence d’un constructeur. Si je peux je n’en mets aucun.

Ou encore comme ça :

    Point2D t{.x= 4, .y=5};

Qui est encore plus explicite.

+1 -0

Pour un tel agrégat dépourvu de tout invariant, il n’y a pas de sens à s’enquiquiner à rajouter des constructeurs (constexpr + noexcept aussi!) en C++11 & suivants qui peuvent vite avoir pour effet de détrivialiser l’objet.

lmghs

Je précise que ma réponse était générique et ne concernait pas que ce cas particulier de Point. Pour un simple point, je suis d’accord avec lmghs, les constructeurs ne sont pas forcément nécessaire (surtout que comme l’a dit lmghs et comme je l’ai indiqué dans les codes, il manque du constexpr, du explicit, du noexcept, etc)

La question de savoir s’il faut default-initialiser les variables membres est en fait une question transversale à cette question de constructeur.

Et en fait, c’est là qu’est le principal sujet de discussion: est-il acceptable de permettre d’avoir des instances de Point2D dans un état non initialisé? Mon opinion est que oui car il n’y que comme ça que l’on peut avoir des warnings du compilateur qui nous dira que l’on utilise des données non initialisées: car mis à 0 ne veut pas forcément dire mis à une valeur pertinente!

lmghs

Pour préciser, cette discussion est très ancienne. Elle date du livre de Stepanov Element of programming (2009), qui définissait justement ce qu’était une valeur régulière. (Lmghs me corrigera si je me trompe. C’est bien lui qui a formaliser en premier ce concept pour le C++, non ? Je ne sais pas si ce concept existait avant dans un autre langage ou en math. EDIT : en fait, ca viendrait a priori de Fundamentals of Generic Programming de Stepanov en 1998. Il faudrait que je relise cet article http://stepanovpapers.com/DeSt98.pdf).

Dans EOP, pour les types réguliers :

[default constructor] takes no arguments and leaves the object in a partially-formed state.

An object is in a partially-formed state if it can be assigned-to or destroyed.

Ce concept revient par exemple dans la move semantic, après qu’un objet soit move.

Il y a pleins d’articles qui aborde cette question. Par exemple (pour citer les auteurs que je connais et dont je suis sur de la qualité) :

Bref, pas une question simple.

Mon opinion est que oui car il n’y que comme ça que l’on peut avoir des warnings du compilateur qui nous dira que l’on utilise des données non initialisées: car mis à 0 ne veut pas forcément dire mis à une valeur pertinente!

lmghs
#include <iostream>

struct Point {
    double x_, y_;
    Point(double x) : x_{x} {}
};

int main() {
    Point p(1);
    std::cout << p.x_ << "," << p.y_;
}

Avec clang et -Weverything :

Start
1,0
0
Finish

Aucun warning pour y_ non initialisé !

C’est bien le problème. Si on était sur d’avoir un warning, ça changerait peut être la discussion. Ce qui n’est pas le cas et pose donc un gros probleme de bugs difficiles à localiser et corriger. Et de toute façon, les autres arguments que j’ai cité (localisation unique de l’initialisation, code plus explicite, meilleure maintenabilité) restent valides.

L’autre argument de lmghs est aussi à prendre en compte. Que faire s’il n’existe pas de valeur par défaut pertinente ? Pour un point, initialiser à (0,0) à un sens, c’est l’origine. Mais dans le cas général ?

Dans ce cas, probablement que le mieux est de ne pas avoir de constructeur par défaut et de ne pas initialiser par défaut les membres qui n’ont pas de valeur par défaut pertinente. Ça arrive, aucune solution marche dans 100% de cas.

Mais a mon sens, on peut considérer qu’avoir un "partially-formed state" est une forme d’optimisation du code et donc doit être fait que si on prouve que cela a un impact pertinent. Dans cette idée, pour moi, la règle doit donc être : on initialize toujours par défaut les membres lors de la déclaration (sauf si on a une très très bonne raison de ne pas le faire).

+1 -0

(Lmghs me corrigera si je me trompe. C’est bien lui qui a formaliser en premier ce concept pour le C++, non ? Je ne sais pas si ce concept existait avant dans un autre langage ou en math. EDIT : en fait, ca viendrait a priori de Fundamentals of Generic Programming de Stepanov en 1998. Il faudrait que je relise cet article http://stepanovpapers.com/DeSt98.pdf).

Dans EOP, pour les types réguliers :

[default constructor] takes no arguments and leaves the object in a partially-formed state.

Zut j’avais raté cette nuance. J’étais déjà à me battre sur comment réconcilier le vocabulaire pour ce soir avec les autres dénominations entre types valeurs/entités, sémantique valeur/référence, interprétation de la sémantique de valeur (juste manipulé comme des valeurs, ou avoir un sens de valeur (Lakos, CppCon 2015), qui rejoint les vieilles interprétations de types valeurs fin 90’s: il y a aussi la notion de porter une valeur).

Avec clang et -Weverything :

Start
1,0
0
Finish

Aucun warning pour y_ non initialisé !

Effectivement, le cas bâtard est vraiment bâtard si on y va à la main. Heureusement, en version 0 constructeurs, le compilo choppe les origines des problèmes (en -Wall -Wextra). https://godbolt.org/z/PG8eP3v89

=> faut vraiment en faire le moins possible

Mais a mon sens, on peut considérer qu’avoir un "partially-formed state" est une forme d’optimisation du code et donc doit être fait que si on prouve que cela a un impact pertinent. Dans cette idée, pour moi, la règle doit donc être : on initialize toujours par défaut les membres lors de la déclaration (sauf si on a une très très bonne raison de ne pas le faire).

Mon feeling est que cela a du sens (de laisser la possibilité d’avoir cet état non initialisé) pour tous ces objets mathématiques qui n’ont aucun invariant.

Avec clang et -Weverything :

Start
1,0
0
Finish

Aucun warning pour y_ non initialisé !

C’est étrange, que se soit avec gcc ou clang, j’ai -Wmissing-field-initializers. Par contre, il me semble que les champs seront quand même initialisés à 0 ou avec une valeur par défaut.

Definitivement aucun warning, meme avec gcc.

Ok que certaines syntaxes initialisent quand même ou produisent un warning, mais en termes de sécurité du code, ça me semble être un problème que la sécurité va dépendre des syntaxes, du bon vouloir des compilateurs, etc.

Mon feeling est que cela a du sens (de laisser la possibilité d’avoir cet état non initialisé) pour tous ces objets mathématiques qui n’ont aucun invariant.

Du sens, ok. Mais le bénéfice ? Comparé à la perte de securite du code ?

En soi, avoir des objets non initialisés, je ne suis pas contre. On en a parfois besoin. Un exemple concret qui me vient en tete, c’est l’implementation de vector::reserve, qui pose "problème" s’il n’est pas possible de creer des objets non initialises.

Mais c’est juste un gain de performances. Ce qui, a mon sens, ne doit être fait qu’après vérification du gain réel, pas en première intention sur des arguments théoriques. (Dit autrement, "ca a du sens" n’est pas un argument assez fort a mon sens pour abandonner cette règle)

+0 -0

Definitivement aucun warning, meme avec gcc.

Parce que tu as un constructeur qui n’a rien à faire là. On en revient à ma précédente petite remarque noyée au milieu de mon blabla: les constructeurs inutiles ont des effets de bords indésirables. Ici (ZdS) je ne parlais que de perte de trivialité, mais il n’y a pas que ça.

Ok que certaines syntaxes initialisent quand même ou produisent un warning, mais en termes de sécurité du code, ça me semble être un problème que la sécurité va dépendre des syntaxes, du bon vouloir des compilateurs, etc.

Et c’est ça la vraie question.

Qu’est-ce qui est préférable?

  • avoir une garantie de 0-initialisation, mais perdre la détection d’initialisations débiles à cause de vieilles règles qualité qui demandent de tout déclarer et initialiser par défaut en début de bloc?
  • prier que compilateur, sanitizeur, valgrind nous diront que l’on manipule des données non initialisées, mais on est sur de ne jamais manipuler de données non initialisées et donc d’avoir des bugs reproductibles (retour à ce que je cherchais à démontrer dans ce que j’ai mis dans la FAQ de dvpz)

En soi, avoir des objets non initialisés, je ne suis pas contre. On en a parfois besoin. Un exemple concret qui me vient en tete, c’est l’implementation de vector::reserve, qui pose "problème" s’il n’est pas possible de creer des objets non initialises.

En espérant ne pas lire de travers. reserve() fait plus que 0-initialiser, il construit. Officiellement, il faudrait appeler le constructeur de placement sur le résultat de malloc(size(int)), même avec des POD. Officiellement. Mais compatibilité oblige, l’UB n’est pas exploité par les compilos.

Mais c’est juste un gain de performances. Ce qui, a mon sens, ne doit être fait qu’après vérification du gain réel, pas en première intention sur des arguments théoriques. (Dit autrement, "ca a du sens" n’est pas un argument assez fort a mon sens pour abandonner cette règle)

C’est là où je ne suis donc pas d’accord. C’est de la programmation défensive qui neutralise des possibilités de détections de bugs. Je ne parle pas du cas bâtard avec constructeur qui fait son boulot à moitié (de ton illustration), mais du cas des types triviaux (?) où le compilo est capable de détecter un read/jump sur valeur non initialisée. C’est aussi un des points du demi-sujet de la présentation que j’avais donnée au Capitole du Libre il y a 2–3 ans.

NB: je ne dis pas qu’il y a une réponse unique. Juste que le sujet n’est pas si trivial que cela aujourd’hui où les compilateurs sont mieux capables de détecter une classe de bugs liés à l’utilisation de données non initialisées. Peut etre que dans 10ans je serai bien plus catégorique car les compilos détecteront tout. Aujourd’hui, c’est perfectible.

Parce que tu as un constructeur qui n’a rien à faire là.

Si on se limite à ce cas très particulier du point (mais même dans ce cas, je pense que c’est discutable, selon le contexte. Par exemple, imaginons Point implémenté avec des double et qu’on initialise avec des floats. La conversion est valide mais moins performante. Avec un constructeur, je vois comment être alerté de ce problème mais sans ?). Mais si on place cette question dans le cas général, en prenant en compte les classes complexes, avec pleins de constructeurs et de membres ?

Et est-ce qu’on devrait faire des règles de bonne pratique qui dépende de la taille de la classe ou de l’approche math utilisée ? Quelle sera la limite pour appliquer cette règle de l’initialisation par défaut des membres ?

C’est de la programmation défensive qui neutralise des possibilités de détections de bugs

Mais les bugs ne sont pas toujours détectés. Ok, mon constructeur est discutable sur un type trivial, mais n’empêche que le problème se présente.

Je reconnais que je suis dans une démarche de priorisation de la sécurité du code en premier, en particulier quand il s’agit de discuter des bonnes pratiques "par défaut", sur un forum public avec un public très varié.

Mais je ne suis pas assez convaincu par les arguments que tu avances pour changer cette règle. Pas tant que les compilateurs ne détecteront pas efficacement ce genre de problèmes.

Très clairement, si on me propose dans un code pro de faire une telle classe Point sans aucune verification, je demanderai des grosses preuves pour accepter cela. Et même avec des preuves, il est fort possible que je n’accepte pas une telle implémentation pour une utilisation par défaut. Je demanderais probablement une classe Point a utiliser par défaut et une classe UnsafePoint a utiliser quand on veut des performances (et que l’on a mesuré l’impact réel).

(Et en vrai, c’est que je fais et ce qui se fait souvent avec les GPU : la classe "point" utilisé dans les vector pour envoyer les données au GPU n’est pas la même que la classe "point" utilisée quand on manipule les points un par un. Parce que dans le premier cas, on sait qu’on a besoin de performances, dans le second d’une bonne API safe)

+0 -0

Avec un constructeur, je vois comment être alerté de ce problème mais sans ?)

Ca ne compile pas avec l’écriture parenthèse… :/

Sinon, je refile ça à vtune amplifier/advisor et à certains outils qui diront pourquoi la vectorisation a foiré — si on est dans une boucle… :( J’ai gagné plein de perfs dernièrement sur de pareilles conversions, qui étaient hors boucle (et hors classes) et que j’ai mis du temps à voir.

Mais un outil de plus, j’admets volontiers que ce n’est pas pratique.

Et est-ce qu’on devrait faire des règles de bonne pratique qui dépende de la taille de la classe ou de l’approche math utilisée ? Quelle sera la limite pour appliquer cette règle de l’initialisation par défaut des membres ?

De mon avis, ce n’est pas une question de taille non plus.

Dans le off du FRUG, ce sujet a été abordé toute à l’heure. Loic a partagé la même critique que celle que j’émets (en espérant ne pas la transposer à un autre contexte): avec l’initialisation par défaut on perd toute opportunité d’analyse statique et on peut se retrouver avec une autre famille de bugs sournois.

Je reconnais que je suis dans une démarche de priorisation de la sécurité du code en premier

Pareillement ici, mais pas de la même façon. Pour faire plaisir à tout le monde il faudrait un

montype v = 0_but_dirty;

Qui dise au compilo de lever des warnings mais qui mette à "zéro" au cas où…

J’insiste que mon objectif n’est pas une question de perfs.

Pouvoir trouver

In function 'std::ostream& operator<<(std::ostream&, const Point2D&)',
    inlined from 'void g()' at <source>:32:18:
<source>:7:29: warning: 'p.Point2D::x' is used uninitialized [-Wuninitialized]
    7 |         return os << "("<<p.x <<","<<p.y<<")";
      |                           ~~^
<source>: In function 'void g()':
<source>:31:13: note: 'p' declared here
   31 |     Point2D p;
      |             ^
In function 'std::ostream& operator<<(std::ostream&, const Point2D&)',
    inlined from 'void g()' at <source>:32:18:
<source>:7:40: warning: 'p.Point2D::y' is used uninitialized [-Wuninitialized]
    7 |         return os << "("<<p.x <<","<<p.y<<")";
      |                                      ~~^
<source>: In function 'void g()':
<source>:31:13: note: 'p' declared here
   31 |     Point2D p;
      |             ^
Compiler returned: 0

Sur

struct Point2D {
    double x;
    double y;
    friend std::ostream& operator<<(std::ostream & os, Point2D const& p) {
        return os << "("<<p.x <<","<<p.y<<")";
    }
};

void g() {
    Point2D p;
    std::cout << p << "\n";
}

Ce n’est quand même pas anodin. OK. Les compilos ne couvrent pas tout, certes, mais valgrind oui. Du coup, c’est le seul moyen que je vois pour être assisté par des outils pour reconnaître des cas comme

Point2D p1;
Point2D p2;
if (toto) {
    p1 = {1, 2}; p2 = ...;
    30 lignes...
} elseif (titi) {
    p2 = ...;
    10 lignes;
    p1 = {2, 3};
    3 lignes;
} .... else {
    p1 = ...;
    4 lignes;
    pas de p2 = ..
}
bli(p2 - p1);

Avec une 0-initialisation par défaut impossible de voir que le code pourri a oublié une initialisation dans un cas.

Je ne peux pas te donner tort. S’il y a débat, c’est que je trouve les arguments valides au fond.

vtune amplifier/advisor valgrind

lmghs

Le problème des analyseurs dynamiques est toujours la même : il faut avoir les tests derrière. Encore aujourd’hui, j’ai eu un retour client qui a trouvé un bug sur une utilisation non prévue d’une lib. C’est pas un use case, on ne testait pas ce scénario, donc on ne pouvait pas le détecter. Mais n’empêche que cela reste un bug et qu’il faut le corriger. Ou envoyer balader le client…

Les compilos ne couvrent pas tout

J’ai testé avec clang, pas de warning. C’est un peu ce qui me gène dans ton argument de "faire confiance aux outils". Pour le moment, il a trop de pièges qui font qu’on va se trouver avec un bug qui n’est pas détecté. (On voit dans cette discussion que la détection va dépendre du compilateur, de si on a des constructeurs ou pas, etc)

On parle des points et de doubles, une valeur non initialisée ne sera pas forcément catastrophique. Mais j’imagine aussi des pointeurs, des classes complexes, etc.

Mais je te promets, je mettrai un lien vers cette conversation a chaque fois que je conseillerais sur les forums ou sur discord de toujours initialiser par défaut :)

+0 -0

Mais n’empêche que cela reste un bug et qu’il faut le corriger. Ou envoyer balader le client…

Et ajouter son scénario dans la base de tests ? ;)

On parle des points et de doubles, une valeur non initialisée ne sera pas forcément catastrophique. Mais j’imagine aussi des pointeurs, des classes complexes, etc.

Bonne question. Pour le coup il y a une différence intéressante. Déférencer un pointeur nul plantera probablement. Certes on peut le tester et être dérouté ailleurs, mais j’ai plus confiance en la détection d’une erreur de logique qu’en la détection d’une erreur de calcul. Une intuition. De plus généralement ce pointeur va régulièrement être absorbé dans un truc plus gros et participera à ses invariants internes. Du coup, je tends à initialiser là. Sur un point, quels sont les invariants?

Du coup, est-ce que NaN ne serait pas une meilleure valeur par défaut? Vu que c’est une valeur typiquement invalide et qui pourra se reconnaître comme telle? 🤔

(je passe les problématiques de dangling pointeurs)

Mais je te promets, je mettrai un lien vers cette conversation a chaque fois que je conseillerais sur les forums ou sur discord de toujours initialiser par défaut

:D :D

Bonjour Messieurs et merci pour vos réponses/discutions.

Je n’ai pas eu le temps de me connecter cette semaine, et je découvre le fil de ce post. Je ne m’attendais pas à ouvrir un sujet si conséquent.

… C’est juste des questions de maintenance du code.

  1. le code sera plus solide, si tu dois faire évoluer ton code…

… sur un code plus complexe, où toutes les variables sont pas directement visibles quand on écrit un constructeur, qu’il y a beaucoup de variables, du code un peu plus complexe et moins lisible (des templates, de la doc, etc), le risque d’oublier une initialisation est plus importante.

  1. le second point, c’est la consistance des valeurs par défaut. Si tu initialises une variable par défaut a plusieurs endroit du code, alors tu peux faire l’erreur d’initialiser par défaut avec des valeurs différentes.

gbdivers

Ok.

Je pense avoir compris les positions de LMGHS et de GBDivers, dans la suite de la discussion. Étant personnellement dans le logiciel critique embarqué, je suis plus sensible aux arguments de GBDivers.

Réflexion faite, je me pose la question : étant donné les puissances des processeurs (L’argument du gain de performance me semble plus très convaincant), pourquoi le compilateur n’initialise pas par défaut les valeurs des variables, à la déclaration ? (Ca n’empêcherai pas qu’il puisse aussi générer des Warnings, si cette valeur n’est pas surcharger par une initialisation explicite, libre aux développeurs de regarder/corriger ou pas ce warning). Bien sûr, ce n’est qu’une initialisation par défaut, qui n’aura peut-être pas de sens, qui pourrait même faire planter le soft, mais il n’y aurait plus de UB. Je sais que les vieux compilateurs C (1980/1990, quand j’ai commencé à travailler) ne le faisait pas. Est-ce pour toujours rester compatible du C K&R, Puis C ANSII, Puis …

Cordialement.

(Ps : Pour la petite histoire, quand j’étais jeune [et beau] développeur, mon premier bug "majeur", à cause d’un compteur de boucle non-initialisé, a été découvert pendant les tests de température du boitier dans une enceinte climatique (Un truc de quelques mètres cube, avec compresseur, bouteilles d’azotes liquide, … un gros frigidaire/four de cuisine, qui génère une température régulée de -55°c à +85°c). En effet, à température ambiante et à chaud la SRAM avait tendance à s’initialiser aléatoirement avec beaucoup de bits à zéro, mais à froid avec beaucoup de bits à 1. Donc la boucle était parcourue quelques fois, à température ambiante, et de l’ordre de 64000 fois à froid, et le boitier ne démarrer pas !)

pourquoi le compilateur n’initialise pas par défaut les valeurs des variables, à la déclaration ? (Ca n’empêcherai pas qu’il puisse aussi générer des Warnings, si cette valeur n’est pas surcharger par une initialisation explicite, libre aux développeurs de regarder/corriger ou pas ce warning). Bien sûr, ce n’est qu’une initialisation par défaut, qui n’aura peut-être pas de sens, qui pourrait même faire planter le soft, mais il n’y aurait plus de UB. Je sais que les vieux compilateurs C (1980/1990, quand j’ai commencé à travailler) ne le faisait pas. Est-ce pour toujours rester compatible du C K&R, Puis C ANSII, Puis …

Source:Dedeun

Parce qu’il y a le problème de "on ne paie pas pour ce qu’on n’a pas besoin" en C++.

Plus le problème de savoir quoi initialiser. Initialiser à 0 peut éviter les UB, mais cela peut provoquer aussi un "bug" (un comportement pas souhaité) qui sera difficile à détecter.

A noter que MSVC initialiser les variables avec des valeurs spécifiques, en débug, justement pour faciliter la détection.

De toute façon, si tu es dans un domaine critique, il faut utiliser un max d’outils de détection.

On pourrait imaginer une option dans les compilos pour forcer l’initialisation par défaut. Mais je n’ai jamais vu ce genre d’option (ou je ne m’en souviens pas).

Aprés, rien n’empêche d’écrire ton propre type qui s’auto initialiserait. C’est que font les smart ptr comparé aux raw ptr, ou vector/array comparés aux C-array.

#ifdef DEBUG
  class Int {
    Int(int i) : m_i(i), m_isInitialised(true) {}
    operator int() const { if(!m_isInitialised) error(); return m_i; }
    int m_i = 0;
    bool m_isInitialised = false;
  };
#else
  using Int = int;
#endif

C’est pas quelque chose que je fais, mais selon le context, pourquoi pas.

(Ps : Pour la petite histoire, quand j’étais jeune [et beau] développeur, mon premier bug "majeur", à cause d’un compteur de boucle non-initialisé, a été découvert pendant les tests de température du boitier dans une enceinte climatique (Un truc de quelques mètres cube, avec compresseur, bouteilles d’azotes liquide, … un gros frigidaire/four de cuisine, qui génère une température régulée de -55°c à +85°c). En effet, à température ambiante et à chaud la SRAM avait tendance à s’initialiser aléatoirement avec beaucoup de bits à zéro, mais à froid avec beaucoup de bits à 1. Donc la boucle était parcourue quelques fois, à température ambiante, et de l’ordre de 64000 fois à froid, et le boitier ne démarrer pas !)

Dedeun

Sympa comme anecdote.

+0 -0

Bonjour Messieurs

Certains d’entre nous ne sont pas des messieurs :(

Je pense effectivement que le C se veut le plus proche de ce que le programmeur écrit. Et si le programme n’initialise pas, alors c’est qu’il n’y en a pas besoin. Même si ça serait utilise, ce n’est pas dans la philosophie du C de se reposer sur le compilateur. Par-contre ça m’étonne que les compilateurs C++ n’aient pas ce genre d’options.

+0 -0

Si j’ai bien compris un bug récent, sous AIX, les pointeurs non initialisés valent quelque chose comme 0x1, tandis que ceux initialisés nuls valent 0x0. Tout mettre à 0x0 par défaut casserai la compatibilité, car un if (pointer) capturerai les pointeurs non initialisés.

+0 -0

Encore moi,

Certains d’entre nous ne sont pas des messieurs :(

ache

Oups! Avec mes excuses, c’était pas fait exprès (Les habitudes du boulot !) Et c’est vrai qu’avec les speudos on peut se gourer.

Parce qu’il y a le problème de "on ne paie pas pour ce qu’on n’a pas besoin" en C++.

Bof, ça coutera pas grand-chose ! (ref à la perte de performance)

Plus le problème de savoir quoi initialiser. Initialiser à 0 peut éviter les UB, mais cela peut provoquer aussi un "bug" (un comportement pas souhaité) qui sera difficile à détecter.

Pour moi le point le plus difficile, c’est le UB. Quand c’est reproductible, c’est plus facile à débuger. (Ref programmation par contrat : les préconditions)

De toute façon, si tu es dans un domaine critique, il faut utiliser un max d’outils de détection.

Oh la la: la qualification des outils … Vaste sujet … (Mais on ne programme pas en C++ le soft critique !)

On pourrait imaginer une option dans les compilos pour forcer l’initialisation par défaut. Mais je n’ai jamais vu ce genre d’option (ou je ne m’en souviens pas).

… Par-contre ça m’étonne que les compilateurs C++ n’aient pas ce genre d’options.

Donc je ne suis pas le seul à me poser la question.

Aprés, rien n’empêche d’écrire ton propre type qui s’auto initialiserait. C’est que font les smart ptr comparé aux raw ptr, ou vector/array comparés aux C-array.

Merci pour l’exemple.

Cordialement

Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte