Pourquoi utiliserait-on des classes imbriquées en C ++?

188

Quelqu'un peut-il s'il vous plaît me diriger vers de belles ressources pour comprendre et utiliser des classes imbriquées? J'ai du matériel comme les principes de programmation et des choses comme ce centre de connaissances IBM - Classes imbriquées

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

à lunettes
la source
15
Mon conseil pour les classes imbriquées en C ++ est simplement de ne pas utiliser de classes imbriquées.
Billy ONeal
7
Ils sont exactement comme des classes régulières ... sauf imbriquées. Utilisez-les lorsque l'implémentation interne d'une classe est si complexe qu'elle peut plus facilement être modélisée par plusieurs classes plus petites.
meagar
12
@Billy: Pourquoi? Cela me semble trop large.
John Dibling
30
Je n'ai toujours pas vu d'argument pour lequel les classes imbriquées sont mauvaises de par leur nature.
John Dibling
7
@ 7vies: 1. parce que ce n'est tout simplement pas nécessaire - vous pouvez faire de même avec des classes définies en externe, ce qui réduit la portée de toute variable donnée, ce qui est une bonne chose. 2. parce que vous pouvez faire tout ce que les classes imbriquées peuvent faire avec typedef. 3. parce qu'ils ajoutent un niveau supplémentaire d'indentation dans un environnement où éviter les longues lignes est déjà difficile 4. parce que vous déclarez deux objets conceptuellement distincts dans une seule classdéclaration, etc.
Billy ONeal

Réponses:

229

Les classes imbriquées sont cool pour cacher les détails de l'implémentation.

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 pourraient 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, je cache non seulement l'implémentation, je dis aussi que c'est la mienne et je peux la changer à tout moment pour que vous ne puissiez pas l'utiliser.

Regardez std::listou std::mapils contiennent tous des classes cachées (ou le font-ils?). 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 pu mettre à jour le code sans affecter la façon dont vous avez utilisé le code, ou laisser beaucoup de vieux bagages traînant autour de la STL parce qu'ils ont besoin pour maintenir la compatibilité ascendante avec un imbécile qui a décidé d'utiliser la classe Node qui était cachée à l'intérieur list.

Martin York
la source
9
Si vous faites cela, cela Nodene devrait pas du tout être exposé dans le fichier d'en-tête.
Billy ONeal
6
@Billy ONeal: Et si je fais une implémentation de fichier d'en-tête comme la STL ou boost.
Martin York
6
@Billy ONeal: Non. C'est une question de bonne conception, pas d'opinion. Le placer dans un espace de noms ne le protège pas de son utilisation. Il fait désormais partie de l'API publique qui doit être maintenue à perpétuité.
Martin York
21
@Billy ONeal: Il le protège d'une utilisation accidentelle. Il documente également le fait qu'il est privé et ne doit pas être utilisé (ne peut être utilisé que si vous faites quelque chose de stupide). Ainsi, vous n'avez pas besoin de le soutenir. Le placer dans un espace de noms en fait une partie de l'API publique (ce qui vous manque toujours dans cette conversation. L'API publique signifie que vous devez la prendre en charge).
Martin York
10
@Billy ONeal: La classe imbriquée présente certains avantages par rapport à l'espace de noms imbriqué: vous ne pouvez pas créer d'instances d'un espace de noms, mais vous pouvez créer des instances d'une classe. En ce qui concerne la detailconvention: au lieu de cela, en fonction de ces conventions, il faut se souvenir, il vaut mieux dépendre du compilateur qui en garde la trace pour vous.
SasQ
142

Les classes imbriquées sont comme les classes normales, mais:

  • ils ont une restriction d'accès supplémentaire (comme toutes les définitions à l'intérieur d'une définition de classe),
  • ils ne polluent pas l'espace de noms donné , par exemple l'espace de noms global. Si vous pensez que la classe B est si profondément connectée à la classe A, mais que les objets de A et B ne sont pas nécessairement liés, alors vous voudrez peut-être que la classe B ne soit accessible que via l'étendue de la classe A (elle serait appelée A ::Classe).

Quelques exemples:

Classe d'imbrication publique pour la placer dans une portée de classe pertinente


Supposons que vous souhaitiez avoir une classe SomeSpecificCollectionqui regrouperait des objets de classe Element. Vous pouvez alors soit:

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

  2. introduire un espace de noms someSpecificCollectionet déclarer les classes someSpecificCollection::Collectionet someSpecificCollection::Element. Pas de risque de clash de nom, mais peut-il devenir plus bavard?

  3. déclarer deux classes globales SomeSpecificCollectionet SomeSpecificCollectionElement- ce qui a des inconvénients mineurs, mais c'est probablement OK.

  4. déclarer la classe globale SomeSpecificCollectionet la classe Elementcomme sa classe imbriquée. Ensuite:

    • vous ne risquez aucun conflit de nom car Element n'est pas dans l'espace de noms global,
    • dans la mise en œuvre de SomeSpecificCollectionvous faites référence à juste Element, et partout ailleurs comme SomeSpecificCollection::Element- ce 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", pas "un élément spécifique d'une collection"
    • il est visible que SomeSpecificCollectionc'est aussi une classe.

À mon avis, la dernière variante est certainement la conception la plus intuitive et donc la meilleure.

Permettez-moi de souligner - Ce n'est pas une grande différence de créer deux classes globales avec des noms plus verbeux. C'est juste un tout petit détail, mais à mon avis, cela rend le code plus clair.

Présentation d'une autre portée dans 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 appellera alors:

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

Mais quand on regarde les propositions de complétion de code pour Product::, on obtiendra souvent toutes les valeurs d'énumération possibles (BOX, FANCY, CRATE) et il est facile de faire une erreur ici (les énumérations fortement typées de C ++ 0x résolvent en quelque sorte cela, mais tant pis ).

Mais si vous introduisez une portée supplémentaire pour ces énumérations 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
};

Ensuite, l'appel ressemble à:

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

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

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

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

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 de l'en-tête. Cela réduit le besoin 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.

Il est généralement implémenté à l'aide d'une classe imbriquée:

Xh:

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 classe complète a besoin de la définition de types à partir d'une bibliothèque externe qui a un fichier d'en-tête lourd ou juste moche (prenez WinAPI). Si vous utilisez PIMPL, vous pouvez inclure une fonctionnalité spécifique à WinAPI uniquement dans .cppet ne jamais l'inclure dans .h.

Kos
la source
3
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 la génération de code erroné.
Gene Bushuyev
2
@Billy ONeal: Autant que je sache, vous pouvez déclarer un auto_ptrtype incomplet dans la plupart des implémentations, mais techniquement, c'est UB contrairement à certains des modèles de C ++ 0x (par exemple unique_ptr) où il a été précisé que le paramètre de modèle peut être un type incomplet et où exactement le type doit être complet. (par exemple, utilisation de ~unique_ptr)
CB Bailey
2
@Billy ONeal: En C ++ 03 17.4.6.3 [lib.res.on.functions] dit "En particulier, les effets ne sont pas définis dans les cas suivants: [...] si un type incomplet est utilisé comme argument de modèle lors de l'instanciation d'un composant de modèle. " alors qu'en C ++ 0x, il est dit "si un type incomplet est utilisé comme argument de modèle lors de l'instanciation d'un composant de modèle, sauf autorisation spécifique pour ce composant." et plus tard (par exemple): "Le paramètre Tde modèle de unique_ptrpeut être un type incomplet."
CB Bailey
1
@MilesRout C'est beaucoup trop général. Dépend du fait que le code client est autorisé à hériter. Règle: Si vous êtes certain que vous ne supprimerez pas via un pointeur de classe de base, alors le dtor virtuel est complètement redondant.
Kos
2
@IsaacPascual aww, je devrais mettre à jour cela maintenant que nous l'avons enum class.
Kos
21

Je n'utilise pas beaucoup les classes imbriquées, mais je les utilise de temps en temps. Surtout quand je définis une sorte de 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 une Fieldclasse générique qui a un numéro d'identification, un code de type et un nom de champ. Si je veux rechercher un vectorde ces Fields par numéro d'identification ou par 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 a besoin de rechercher ces Fields peut utiliser la matchportée dans la Fieldclasse elle-même:

vector<Field>::const_iterator it = find_if(fields.begin(), fields.end(), Field::match("FieldName"));
John Dibling
la source
Merci pour le bon exemple et les commentaires, même si je ne suis pas tout à fait au courant des fonctions STL. Je remarque que les constructeurs de match () sont publics. Je suppose que les constructeurs n'ont pas toujours besoin d'être publics, auquel cas ils ne peuvent pas être instanciés en dehors de la classe.
lunettes
1
@user: Dans le cas d'un foncteur STL, le constructeur doit être public.
John Dibling
1
@Billy: Je encore ai encore vu aucun raisonnement concret pourquoi les classes imbriquées sont mauvaises.
John Dibling
@John: Toutes les règles de style de codage se résument à une question d'opinion. J'ai énuméré plusieurs raisons dans plusieurs commentaires ici, qui (à mon avis) sont raisonnables. Il n'y a pas d'argument "factuel" qui peut être fait tant que le code est valide et n'invoque aucun comportement indéfini. Cependant, je pense que l'exemple de code que vous avez mis ici indique une grande raison pour laquelle j'évite les classes imbriquées - à savoir les conflits de nom.
Billy ONeal
1
Bien sûr, il y a des raisons techniques de préférer les inlines aux macros !!
Miles Rout
14

On peut implémenter un modèle Builder avec une classe imbriquée . Surtout en C ++, personnellement je le trouve sémantiquement plus propre. Par exemple:

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

Plutôt que:

class Product {}
class ProductBuilder {}
Yeo
la source
Bien sûr, cela fonctionnera s'il n'y a qu'une seule construction, mais deviendra désagréable s'il est nécessaire d'avoir plusieurs constructeurs en béton. Il faut prendre soigneusement les décisions de conception :)
irsis