Non-Static Data Member Initializers (NSDMI)

Étonnamment, l'initialisation est un sujet complexe en C++. Ce qui suit couvre quelques aspects de cette question, mais vous voudrez sans doute en savoir aussi sur :

Depuis C++ 11, il est possible d'initialiser des attributs d'instance dès leur déclaration. C'est ce qu'on nomme l'initialisation immédiate (acronyme anglais NSDMI, pour Non-Static Data Member Initialization).

Pour un type comme un Point tridimensionnel, si l'on accepte que les valeurs x,y,z n'aient pas d'invariants en propre à respecter, on pourrait imaginer une application de ce mécanisme. Cela donnerait :

Sans initialisation immédiate Avec initialisation immédiate
struct Point {
   float x, y, z;
   constexpr Point() noexcept
      : Point{ 0.0f, 0.0f, 0.0f } {
   }
   constexpr Point(float x, float y, float z)
      : x{ x }, y{ y }, z{ z } {
   }
   // ... etc.
};
struct Point {
   float x {}, y {}, z {};
   Point() = default; // constexpr par défaut!
   constexpr Point(float x, float y, float z)
      : x{ x }, y{ y }, z{ z } {
   }
   // ... etc.
};

Notez que le = default est nécessaire à droite, du fait que le constructeur paramétrique fait disparaître le constructeur par défaut qui aurait autrement été généré de manière implicite. Supposons maintenant une classe Cercle. Auparavant, nous aurions pu initialiser ses attributs comme le montre le code à gauche, alors qu'il est désormais possible de procéder comme le montre l'extrait à droite :

Sans initialisation immédiate Avec initialisation immédiate
class Cercle {
   Point centre_;
   float rayon_;
public:
   constexpr Cercle() noexcept
      : centre_(), rayon_(1.0f) {
   }
   constexpr Cercle(const Point &centre) noexcept 
      : centre_(centre), rayon_(1.0f) {
   }
   constexpr Cercle(const Point &centre, float rayon) noexcept
      : centre_(centre), rayon_(rayon) {
   }
   // ... etc.
};
class Cercle {
   Point centre_{};
   float rayon_{ 1.0f };
public:
   Cercle() = default;
   constexpr Cercle(const Point &centre) noexcept
      : centre_{ centre } {
   }
   constexpr Cercle(const Point &centre, float rayon) noexcept
      : centre_{ centre }, rayon_{ rayon } {
   }
   // ... etc.
};

Ceci permet d'apposer une valeur par défaut dans les attributs d'instance d'un objet. Si le constructeur utilise une initialisation explicite, alors seule cette initialisation aura lieu en pratique; si aucune initialisation explicite n'est faite par un constructeur, alors le NSDMI s'appliquera (s'il y en a un).

Bémol pédagogique

J'ai mes réserves sur ce mécanisme, d'un point de vue pédagogique du moins. C'est un bon mécanisme, mais il faut bien le comprendre pour en profiter pleinement.

Quand j'enseigne à des gens qui débutent dans la programmation, mes étudiant(e)s tendent à mal comprendre les constructeurs et leur rôle clé dans l'encapsulation; je constate qu'ils laissent souvent des attributs dans un état indéfini. Je suis conscient qu'affecter une valeur par défaut à chaque attribut « règle » ce problème, mais je ne suis pas certain que l'initialisation à deux endroits distincts (implicite, dans un constructeur) va entraîner chez eux de saines pratiques d'hygiène de programmation.

Avec des programmeuses et des programmeurs d'expérience, c'est autre chose : ce mécanisme entraîne une simplification appréciable de la programmation. J'utilise les NSDMI dans mon propre code et je les apprécie.

Je suis un partisan des petites classes (et des petites fonctions) qui font une et une seule chose. J'aime bien que ce soit simple, avec peu d'attributs.

Lectures complémentaires

Quelques liens pour enrichir le propos.


Valid XHTML 1.0 Transitional

CSS Valide !