IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
Navigation

Inscrivez-vous gratuitement
pour pouvoir participer, suivre les réponses en temps réel, voter pour les messages, poser vos propres questions et recevoir la newsletter

Langage C++ Discussion :

Apprendre la construction de classes en C++ avec les types et les objets de Stepanov


Sujet :

Langage C++

  1. #1
    Community Manager

    Profil pro
    Inscrit en
    Avril 2014
    Messages
    4 207
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Avril 2014
    Messages : 4 207
    Points : 13 061
    Points
    13 061
    Par défaut Apprendre la construction de classes en C++ avec les types et les objets de Stepanov
    Chers membres du club,

    J'ai le plaisir de vous présenter ce tutoriel de KDAB pour apprendre la construction de classes en C++ avec les types réguliers et les objets partiellement formés de Stepanov.

    Dans ce tutoriel, j'aborderai un des concepts fondamentaux introduits par Alex Stepanov et Paul McJones dans leur ouvrage de référence Elements of Programming : celui de type régulier (ou semi-régulier) (regular type) et d'état partiellement formé (partially-formed state).
    À partir de ces concepts, j'essaierai d'en déduire des règles d'implémentation en C++ de ce que l'on appelle d'habitude des « types valeur » (value types), en me concentrant sur l'essentiel, qui me semble n'avoir pas été traité suffisamment en profondeur jusqu'à présent : les fonctions membres spéciales.

    Bonne lecture et n'hésitez pas à apporter vos commentaires.


    Retrouvez les autres cours et tutoriels proposés par KDAB


    Retrouvez les meilleurs cours et tutoriels pour apprendre C++
    Pour contacter les différents services du club (publications, partenariats, publicité, ...) : Contacts

  2. #2
    Expert éminent
    Avatar de Pyramidev
    Homme Profil pro
    Développeur
    Inscrit en
    Avril 2016
    Messages
    1 471
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Haute Garonne (Midi Pyrénées)

    Informations professionnelles :
    Activité : Développeur

    Informations forums :
    Inscription : Avril 2016
    Messages : 1 471
    Points : 6 109
    Points
    6 109
    Par défaut
    Personnellement, je suis d'accord avec le premier commentaire de l'article original quand il dit :
    Regarding default-initialization for if/else vs ?: — I think that’s not a very strong argument. If it was the sole argument for a partially-formed state, then the safety concerns (use after lack of proper initialization) would far outweigh this minor benefit IMHO. However, it is not the sole argument. It is far easier to perform aggregation if you have a partially constructed state, since the class might not have a default value to provide to its data member.
    En particulier, quand on manipule std::array<T, N>, il est très pratique que T ait un constructeur par défaut sans le coût de l'initialisation. Exemple :
    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
    #include <array>
     
    class Rect {
    public:
    	Rect() noexcept = default;
    	constexpr Rect(int x1, int y1, int x2, int y2) noexcept :
    		m_x1(x1), m_y1(y1), m_x2(x2), m_y2(y2) {}
    private:
    	int m_x1, m_y1, m_x2, m_y2;
    };
     
    template<size_t N>
    std::array<Rect, N> foo()
    {
    	std::array<Rect, N> result;
    	for(size_t k = 0; k < N; ++k)
    		result[k] = Rect(k, k, k+1, k+1);
    	return result;
    }
    Mais l'absence d'initialisation est dangereuse car source de bogues non reproductibles. Pour pallier cela, on pourrait envisager que l'absence d'initialisation soit explicite :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    struct no_init_t {} no_init;
     
    class Rect2 {
    public:
    	Rect2(no_init_t) noexcept : Rect2() {}
    	constexpr Rect2(int x1, int y1, int x2, int y2) noexcept :
    		m_x1(x1), m_y1(y1), m_x2(x2), m_y2(y2) {}
    private:
    	Rect2() noexcept = default;
    	int m_x1, m_y1, m_x2, m_y2;
    };
    Cependant, d'un autre côté, l'utilisation serait parfois plus complexe. Par exemple, réécrire la fonction foo nécessiterait du code préliminaire :
    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
    namespace detail
    {
    	template<class T, size_t...iseq>
    	std::array<T, sizeof...(iseq)> make_std_array_no_init_impl(std::index_sequence<iseq...>)
    	{
    		return {{(static_cast<void>(iseq), T(no_init))...}};
    	}
    }
     
    //! Pour chaque élément, appelle le constructeur avec en argument no_init.
    template<class T, size_t N>
    std::array<T, N> make_std_array_no_init()
    {
    	return detail::make_std_array_no_init_impl<T>(std::make_index_sequence<N>());
    }
     
    template<size_t N>
    std::array<Rect2, N> foo2()
    {
    	auto result = make_std_array_no_init<Rect2, N>();
    	for(size_t k = 0; k < N; ++k)
    		result[k] = Rect2(k, k, k+1, k+1);
    	return result;
    }

  3. #3
    Expert éminent sénior
    Avatar de Luc Hermitte
    Homme Profil pro
    Développeur informatique
    Inscrit en
    Août 2003
    Messages
    5 275
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Haute Garonne (Midi Pyrénées)

    Informations professionnelles :
    Activité : Développeur informatique
    Secteur : Aéronautique - Marine - Espace - Armement

    Informations forums :
    Inscription : Août 2003
    Messages : 5 275
    Points : 10 985
    Points
    10 985
    Par défaut
    (Je reposte ici un commentaire posé dans la zone de rédaction)

    La discussion de l'auteur sur le partiellement formé n'est pas anodine. C'est un sujet qui a fortement impacté le redesign des variants, d'optional et d'autres trucs encore comparativement au design initial chez boost. Je pense que c'est un sujet sur lequel nous nous cherchons encore.

    Pour l'instant, mettre l'objet dans un état destructible (voire affectable), mais non valide (UB en cas de tentative d'exécution de certaines opérations) ne me parait absolument pas aberrant. On rejoint des problématiques de Programmation par Contrat VS programmation Défensive (quand on considère l'utilisation d'exceptions). C'est exactement la même problématique que " auto p = make_unique<T>(...); f(move(p)); p->g();". Il y a une UB sans que cela ne choque personne, non?

    Jusqu'au C++11, j'en étais arrivé à la conclusion que les constructeurs par défaut ne sont pas nos amis sur les types entités. Que sur les valeurs pourquoi pas, mais que s'il l'on peut éviter c'est aussi bien. Si maintenant on considère que l'on peut déplacer des valeurs, il y a ce problème d'état valide (qui nous offre toujours la garantie basique (de destructabilité)) mais non exploitable.
    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...

  4. #4
    Expert éminent
    Avatar de Pyramidev
    Homme Profil pro
    Développeur
    Inscrit en
    Avril 2016
    Messages
    1 471
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Haute Garonne (Midi Pyrénées)

    Informations professionnelles :
    Activité : Développeur

    Informations forums :
    Inscription : Avril 2016
    Messages : 1 471
    Points : 6 109
    Points
    6 109
    Par défaut
    Petit up.

    Rappel sur l'objet du fil : L'article parle d'une manière de concevoir des classes de telle sorte que le constructeur par défaut génère un objet dans un « état partiellement formé », c'est-à-dire un état dans lequel les seules fonctions qui ont un comportement déterminé sont le destructeur et l'affectation.

    Je suis récemment tombé sur un autre exemple où un état partiellement formé aurait été utile : construire un objet à partir d'un visiteur.

    Soit le code suivant :
    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
    class BuildBarFromFooAndConfig final : private FooConstVisitor
    {
    public:
    	BuildBarFromFooAndConfig(const FooBase& foo, const Config& config) :
    		m_config{config}, m_builtResult{}
    	{
    		foo.accept(*this);
    	}
    	const Bar& getResult() const {return m_builtResult;}
    private:
    	void visit(const FooDeriv1& visited) override {m_builtResult = /* code qui dépend de visited et de m_config */;}
    	void visit(const FooDeriv2& visited) override {m_builtResult = /* code qui dépend de visited et de m_config */;}
    	void visit(const FooDeriv3& visited) override {m_builtResult = /* code qui dépend de visited et de m_config */;}
    	const Config& m_config;
    	Bar           m_builtResult;
    };
    Dans ce code, m_builtResult est d'abord construit par défaut puis est assigné à la bonne valeur. On ne peut pas le construire directement avec la bonne valeur.
    Mais, si Bar n'a pas de constructeur par défaut, on est contraint de changer le code, par exemple comme ceci :
    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
    class BuildBarFromFooAndConfig final : private FooConstVisitor
    {
    public:
    	BuildBarFromFooAndConfig(const FooBase& foo, const Config& config) :
    		m_config{config}, m_builtResult{}
    	{
    		foo.accept(*this);
    		assert(m_builtResult);
    	}
    	const Bar& getResult() const {return *m_builtResult;}
    private:
    	void visit(const FooDeriv1& visited) override {m_builtResult = /* code qui dépend de visited et de m_config */;}
    	void visit(const FooDeriv2& visited) override {m_builtResult = /* code qui dépend de visited et de m_config */;}
    	void visit(const FooDeriv3& visited) override {m_builtResult = /* code qui dépend de visited et de m_config */;}
    	const Config& m_config;
    	//! \remark std::optional because the result is built lately in the constructor.
    	std::optional<Bar> m_builtResult;
    };
    On se trimballe un invariant de classe qui dit que l'objet optionnel m_builtResult n'est pas vraiment optionnel, du moins pas depuis que le corps du constructeur s'est terminé.

  5. #5
    Expert éminent sénior
    Avatar de koala01
    Homme Profil pro
    aucun
    Inscrit en
    Octobre 2004
    Messages
    11 614
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 52
    Localisation : Belgique

    Informations professionnelles :
    Activité : aucun

    Informations forums :
    Inscription : Octobre 2004
    Messages : 11 614
    Points : 30 626
    Points
    30 626
    Par défaut
    Salut,

    Personnellement, mon problème avec les objets partiellement formé serait sans doute double:

    D'un coté, je suis ancré à l'idée qu'un constructeur doit permettre d'obtenir un objet utilisable comme un mollusque à la coque d'un navire. Si un objet n'est que partiellement formé (et donc partiellement (in)utilisable) au moment où l'on y a accès, c'est "pas bon", mais, alors là, "pas bon du tout".

    Le truc, c'est que je serais bien en peine de justifier ce point de vue, peut-être parce que c'est que que l'on m'a toujours appris, peut-être parce qu'il est plus instinctif qu'autre chose.

    D'un autre coté, je peux aussi concevoir le fait que l'on puisse vouloir "subdiviser" la formation d'un élément complexe en plusieurs étapes, ne serait-ce que parce qu'il se peut que l'on ne dispose pas forcément de toutes les informations nécessaires à la formation totale de cet élément complexe.

    Le truc, c'est qu'un autre point de vue auquel je suis viscéralement accroché est que "l'utilisateur est un imbécile distrait", qui saisira toutes les opportunités de faire une connerie que l'on pourrait lui laisser, et que -- au risque d'en choquer certains -- les développeurs ne font pas exception à la règle.

    Si bien que je ne peux m'empêcher de penser que le gros problème posé par le fait d'avoir un élément partiellement formé concerne le choix qui devra être fait quant à l'accessibilité de cet objet, dans l'état son état actuel, car, pour pouvoir terminer la formation de l'objet, il faut être en mesure de manipuler cet objet.

    Mais, d'un autre coté, si on laisse au développeur la possibilité de faire la moindre connerie en manipulant cet objet partiellement formé, il me semble évident qu'il ... la fera tôt ou tard (sans doute plus tôt que tard, d'ailleurs), mais, très certainement, toujours "au pire moment qui soit".

    Le gros problème à résoudre est donc qu'il faut laisser "suffisamment de latitude" à l'utilisateur afin de lui permettre de terminer la formation de l'élément tout en l'empêchant systématiquement de manipuler cet objet d'une manière qui mènerait à la catastrophe (conséquence logique d'un comportement indéfini).

    S'il y a moyen de concilier "la chèvre et le choux" à ce point de vue -- entre autres au travers des techniques propres à la programmation par contrat -- je ne vois aucune objection majeure à la mise en place d'objets partiellement formé. Mais n'est-ce pas un peu utopique que d'espérer concilier deux aspects si fondamentalement différents
    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

  6. #6
    Expert éminent sénior
    Avatar de Luc Hermitte
    Homme Profil pro
    Développeur informatique
    Inscrit en
    Août 2003
    Messages
    5 275
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Haute Garonne (Midi Pyrénées)

    Informations professionnelles :
    Activité : Développeur informatique
    Secteur : Aéronautique - Marine - Espace - Armement

    Informations forums :
    Inscription : Août 2003
    Messages : 5 275
    Points : 10 985
    Points
    10 985
    Par défaut
    La construction via visiteur, j'ai l'impression que c'est souvent : on a un blog de données duquel on extrait des informations. C'est un filtre sur un blob, ou un nouveau blob filtré.

    L'invariant d'exploitabilité ne peut pas être positionné tant que l'on n'a pas fini de visiter.
    D'autres invariants ? Pas sûr qu'il y en ait.

    Ce qui me fait penser que j'ai tendance à percevoir les types de objets de la sorte maintenant:
    - agrégat de données, par vraiment d'invariant, nul besoin de "private" ni autre capsule de protection
    - truc qui a un invariant
    - valeur
    - entité
    - autres machines hybrides comme les exceptions
    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...

Discussions similaires

  1. [VxiR2] "Exporter avec l'univers" dans les propriétés d'un objet
    Par Geo55 dans le forum Designer
    Réponses: 4
    Dernier message: 27/05/2011, 11h10
  2. les methodes et les associations entre les classes
    Par zin_rbt dans le forum Diagrammes de Classes
    Réponses: 1
    Dernier message: 24/05/2010, 14h41
  3. Réponses: 5
    Dernier message: 14/01/2010, 18h11
  4. Réponses: 5
    Dernier message: 08/12/2008, 12h19

Partager

Partager
  • Envoyer la discussion sur Viadeo
  • Envoyer la discussion sur Twitter
  • Envoyer la discussion sur Google
  • Envoyer la discussion sur Facebook
  • Envoyer la discussion sur Digg
  • Envoyer la discussion sur Delicious
  • Envoyer la discussion sur MySpace
  • Envoyer la discussion sur Yahoo