205 votes

Pourquoi utiliser des classes imbriquées en C++ ?

Quelqu'un peut-il m'indiquer des ressources utiles pour comprendre et utiliser les classes imbriquées ? J'ai du matériel comme les principes de programmation et des choses comme ça. Centre de connaissances IBM - Classes imbriquées

Mais j'ai toujours du mal à comprendre leur but. Quelqu'un pourrait-il m'aider ?

0 votes

Difficile de répondre quand il n'y a pas de questions. Et vous avez déjà une description assez bien faite - qu'attendez-vous spécifiquement des réponses ici ?

16 votes

Mon conseil pour les classes imbriquées en C++ est simplement de ne pas utiliser de classes imbriquées.

9 votes

Elles sont exactement comme les classes ordinaires... sauf qu'elles sont imbriquées. Utilisez-les lorsque l'implémentation interne d'une classe est si complexe qu'elle peut être facilement modélisée par plusieurs petites classes.

249voto

Loki Astari Points 116129

Les classes imbriquées permettent de masquer les détails de mise en œuvre.

Liste :

class List
{
    public:
        List(): head(nullptr), tail(nullptr) {}
    private:
        class Node
        {
              public:
                  int   data;
                  Node* next;
                  Node* prev;
        };
    private:
        Node*     head;
        Node*     tail;
};

Ici, je ne veux pas exposer Node car d'autres personnes peuvent décider d'utiliser la classe et cela m'empêcherait de mettre à jour ma classe car tout ce qui est exposé fait partie de l'API publique et doit être maintenu. pour toujours . En rendant la classe privée, non seulement je cache l'implémentation, mais je dis aussi que c'est la mienne et que je peux la changer à tout moment, donc vous ne pouvez pas l'utiliser.

Regardez std::list o std::map ils contiennent tous des classes cachées (ou pas ?). Le fait est qu'ils peuvent ou non, mais parce que l'implémentation est privée et cachée, les constructeurs de la STL ont été en mesure de mettre à jour le code sans affecter la façon dont vous l'utilisiez, ou en laissant beaucoup de vieux bagages autour de la STL parce qu'ils ont besoin de maintenir une compatibilité descendante avec un idiot qui a décidé qu'il voulait utiliser la classe Node qui était cachée à l'intérieur list .

10 votes

Si vous faites ça, alors Node ne devrait pas du tout être exposé dans le fichier d'en-tête.

6 votes

@Billy ONeal : Que se passe-t-il si je fais une implémentation de fichier d'en-tête comme le STL ou boost.

0 votes

@Martin : Apparemment, les implémentations de fichiers d'en-tête sont également "mauvaises" :/

147voto

Kos Points 29125

Les classes imbriquées sont tout comme les classes ordinaires, mais :

  • elles ont une restriction d'accès supplémentaire (comme toutes les définitions à l'intérieur d'une définition de classe),
  • ils ne pas polluer l'espace de nom donné par exemple, l'espace de noms global. Si vous avez l'impression que la classe B est profondément liée à la classe A, mais que les objets de A et B ne sont pas nécessairement liés, vous pourriez vouloir que la classe B ne soit accessible que par le biais de la classe A (elle serait désignée par A::Class).

Quelques exemples :

imbrication publique de classes pour les placer dans une portée de classe pertinente


Supposons que vous voulez avoir une classe SomeSpecificCollection qui regrouperait les objets de la classe Element . Vous pouvez alors soit :

  1. déclarer deux classes : SomeSpecificCollection y Element - mauvais, car le nom "Element" est suffisamment général pour provoquer un éventuel conflit de noms

  2. introduire un espace de nom someSpecificCollection et déclarer des classes someSpecificCollection::Collection y someSpecificCollection::Element . Il n'y a pas de risque de conflit de noms, mais peut-il être plus verbeux ?

  3. déclarer deux classes globales SomeSpecificCollection y SomeSpecificCollectionElement - qui présente des inconvénients mineurs, mais qui est probablement acceptable.

  4. déclarer une classe globale SomeSpecificCollection et la classe Element comme sa classe imbriquée. Alors :

    • vous ne risquez pas de conflit de noms, car Element ne se trouve pas dans l'espace de noms global,
    • dans la mise en œuvre de SomeSpecificCollection vous faites référence à juste Element et partout ailleurs comme SomeSpecificCollection::Element - qui ressemble +- à la même chose que 3., mais plus clair
    • il est clair que c'est "un élément d'une collection spécifique", et non "un élément spécifique d'une collection".
    • il est visible que SomeSpecificCollection est également une classe.

À mon avis, la dernière variante est certainement la plus intuitive et donc le meilleur design.

Permettez-moi d'insister - Ce n'est pas une grande différence de faire deux classes globales avec des noms plus verbeux. C'est juste un tout petit détail, mais il rend le code plus clair.

Introduction d'une autre portée à l'intérieur d'une portée de classe


Ceci est particulièrement utile pour introduire des typedefs ou des enums. Je vais juste poster un exemple de code ici :

class Product {
public:
    enum ProductType {
        FANCY, AWESOME, USEFUL
    };
    enum ProductBoxType {
        BOX, BAG, CRATE
    };
    Product(ProductType t, ProductBoxType b, String name);

    // the rest of the class: fields, methods
};

On appelle alors :

Product p(Product::FANCY, Product::BOX);

Mais en examinant les propositions de complétion de code pour Product:: Dans le cas d'un enum, on obtiendra souvent toutes les valeurs possibles de l'enum (BOX, FANCY, CRATE) et il est facile de faire une erreur ici (les enums fortement typés de C++0x résolvent en quelque sorte ce problème, mais passons).

Mais si vous introduisez une portée supplémentaire pour ces enums en utilisant des classes imbriquées, les choses pourraient ressembler à ceci :

class Product {
public:
    struct ProductType {
        enum Enum { FANCY, AWESOME, USEFUL };
    };
    struct ProductBoxType {
        enum Enum { BOX, BAG, CRATE };
    };
    Product(ProductType::Enum t, ProductBoxType::Enum b, String name);

    // the rest of the class: fields, methods
};

Alors l'appel ressemble à :

Product p(Product::ProductType::FANCY, Product::ProductBoxType::BOX);

Puis en tapant Product::ProductType:: dans un IDE, on obtiendra uniquement les enums de la portée souhaitée suggérée. Cela réduit également le risque de faire une erreur.

Bien sûr, cela peut ne pas être nécessaire pour les petites classes, mais si l'on a beaucoup d'enums, cela facilite les choses pour les programmeurs clients.

De la même manière, vous pourriez "organiser" un grand nombre de typedefs dans un template, si vous en aviez besoin. C'est un modèle utile parfois.

L'idiome PIMPL


Le PIMPL (abréviation de Pointer to IMPLementation) est un idiome utile pour supprimer les détails d'implémentation d'une classe dans l'en-tête. Cela réduit la nécessité de recompiler les classes en fonction de l'en-tête de la classe chaque fois que la partie "implémentation" de l'en-tête change.

Elle est généralement mise en œuvre à l'aide d'une classe imbriquée :

X.h :

class X {
public:
    X();
    virtual ~X();
    void publicInterface();
    void publicInterface2();
private:
    struct Impl;
    std::unique_ptr<Impl> impl;
}

X.cpp :

#include "X.h"
#include <windows.h>

struct X::Impl {
    HWND hWnd; // this field is a part of the class, but no need to include windows.h in header
    // all private fields, methods go here

    void privateMethod(HWND wnd);
    void privateMethod();
};

X::X() : impl(new Impl()) {
    // ...
}

// and the rest of definitions go here

Ceci est particulièrement utile si la définition de la classe complète nécessite la définition de types provenant d'une bibliothèque externe qui a un fichier d'en-tête lourd ou simplement laid (prenez WinAPI). Si vous utilisez PIMPL, alors vous pouvez inclure toute fonctionnalité spécifique à WinAPI uniquement dans le fichier .cpp et ne jamais l'inclure dans .h .

3 votes

struct Impl; std::auto_ptr<Impl> impl; Cette erreur a été popularisée par Herb Sutter. N'utilisez pas auto_ptr sur des types incomplets, ou au moins prenez des précautions pour éviter que du code erroné soit généré.

0 votes

@Gene : Que voulez-vous dire par "mauvais code" ? Je n'étais pas au courant auto_ptr n'a pas fonctionné sur les types incomplets...

2 votes

@Billy ONeal : Autant que je sache, vous pouvez déclarer un auto_ptr de type incomplet dans la plupart des implémentations mais techniquement c'est un UB contrairement à certains des templates en C++0x (ex. unique_ptr ) où il a été explicité que le paramètre du modèle peut être un type incomplet et où exactement le type doit être complet. (par exemple, l'utilisation de ~unique_ptr )

22voto

John Dibling Points 56814

Je n'utilise pas beaucoup les classes imbriquées, mais je les utilise de temps en temps. Surtout quand je définis un type de données et que je veux ensuite définir un foncteur STL conçu pour ce type de données.

Par exemple, considérons un générique Field qui possède un numéro d'identification, un code de type et un nom de champ. Si je veux rechercher un vector de ces Field par le numéro d'identification ou le nom, je pourrais construire un foncteur pour le faire :

class Field
{
public:
  unsigned id_;
  string name_;
  unsigned type_;

  class match : public std::unary_function<bool, Field>
  {
  public:
    match(const string& name) : name_(name), has_name_(true) {};
    match(unsigned id) : id_(id), has_id_(true) {};
    bool operator()(const Field& rhs) const
    {
      bool ret = true;
      if( ret && has_id_ ) ret = id_ == rhs.id_;
      if( ret && has_name_ ) ret = name_ == rhs.name_;
      return ret;
    };
    private:
      unsigned id_;
      bool has_id_;
      string name_;
      bool has_name_;
  };
};

Ensuite, le code qui doit rechercher ces Field peuvent utiliser le match dans le cadre de la Field La classe elle-même :

vector<Field>::const_iterator it = find_if(fields.begin(), fields.end(), Field::match("FieldName"));

0 votes

Merci pour cet excellent exemple et ces commentaires, même si je ne suis pas très au fait des fonctions STL. Je remarque que les constructeurs de match() sont publics. Je suppose que les constructeurs ne doivent pas toujours être publics, auquel cas ils ne peuvent pas être instanciés en dehors de la classe.

1 votes

@user : Dans le cas d'un foncteur STL, le constructeur doit être public.

1 votes

@Billy : I toujours Je n'ai encore vu aucun raisonnement concret expliquant pourquoi les classes imbriquées sont mauvaises.

16voto

Yeo Points 938

On peut mettre en œuvre un modèle Builder avec des classes imbriquées. . En particulier en C++, je trouve personnellement que c'est sémantiquement plus propre. Par exemple :

class Product{
    public:
        class Builder;
}
class Product::Builder {
    // Builder Implementation
}

Plutôt que :

class Product {}
class ProductBuilder {}

0voto

Je pense que le but principal de faire une classe imbriquée au lieu d'une simple classe amie est la possibilité d'hériter d'une classe imbriquée dans une classe dérivée. L'amitié n'est pas héritée en C++.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X