web-dev-qa-db-fra.com

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

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

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

173
zengal

Les classes imbriquées sont cool pour cacher les détails d'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, ce qui m'empêcherait de mettre à jour ma classe car tout élément 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 je peux la modifier à tout moment pour que vous ne puissiez pas l’utiliser.

Regardez std::list ou std::map ils 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 du STL ont été en mesure de mettre à jour le code sans affecter la façon dont vous l'avez utilisé, ou de laisser beaucoup d'anciens bagages se déposer autour du STL car ils ont besoin pour maintenir une compatibilité ascendante avec un imbécile qui a décidé d'utiliser la classe Node qui était cachée à l'intérieur de list.

210
Martin York

Les classes imbriquées ressemblent aux classes ordinaires, 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 nom donné, par exemple. espace de noms global. Si vous estimez 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, vous voudrez peut-être que la classe B ne soit accessible que via la portée de la classe A (on l'appellerait A). ::Classe).

Quelques exemples:

Publier des classes imbriquées pour les placer dans une portée de classe pertinente


Supposons que vous souhaitiez avoir une classe SomeSpecificCollection qui agrégerait des objets de classe Element. Vous pouvez alors soit:

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

  2. introduisez un espace de noms someSpecificCollection et déclarez les classes someSpecificCollection::Collection et someSpecificCollection::Element. Aucun risque de conflit de noms, mais peut-il en dire davantage?

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

  4. déclarer la classe globale SomeSpecificCollection et la classe Element en tant que classe imbriquée. Ensuite:

    • vous ne risquez aucun conflit de noms, car Element n'est pas dans l'espace de noms global,
    • dans la mise en œuvre de SomeSpecificCollection vous vous référez à juste Element, et partout ailleurs comme SomeSpecificCollection::Element - qui a l'air + - identique à 3., mais plus clair
    • il devient simplement simple que c'est "un élément d'une collection spécifique", pas "un élément spécifique d'une collection"
    • il est visible que SomeSpecificCollection est aussi une classe.

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

Laissez-moi souligner - il n’est pas très différent 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.

Introduire une autre portée dans une portée de classe


Ceci est particulièrement utile pour l'introduction de typedefs ou d'énums. 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 va souvent lister toutes les valeurs enum possibles (BOX, FANCY, CRATE) et il est facile de se tromper ici (sorte d'énumères fortement typés de C++ 0x ça, mais peu importe).

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

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

Ensuite, en tapant Product::ProductType:: dans un IDE, seuls les enums de l’étendue souhaitée seront suggérés. Cela réduit également le risque de se tromper.

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

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

Le langage PIMPL


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é en utilisant 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 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 moche (par exemple, WinAPI). Si vous utilisez PIMPL, vous pouvez inclure toute fonctionnalité spécifique à WinAPI uniquement dans .cpp et ne jamais l'inclure dans .h.

138
Kos

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

Par exemple, considérons une classe Field générique ayant un numéro d'identification, un code de type et un nom de champ. Si je veux rechercher une vector de ces Fields par un numéro ou un 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 Fields peut utiliser le match défini dans la classe Field elle-même:

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

On peut implémenter un motif 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 {}
13
Yeo