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?
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
.
Les classes imbriquées ressemblent aux classes ordinaires, mais:
Quelques exemples:
Supposons que vous souhaitiez avoir une classe SomeSpecificCollection
qui agrégerait des objets de classe Element
. Vous pouvez alors soit:
déclarer deux classes: SomeSpecificCollection
et Element
- mauvais, car le nom "Element" est suffisamment général pour provoquer un conflit de noms possible
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?
déclarer deux classes globales SomeSpecificCollection
et SomeSpecificCollectionElement
- ce qui présente des inconvénients mineurs, mais est probablement OK.
déclarer la classe globale SomeSpecificCollection
et la classe Element
en tant que classe imbriquée. Ensuite:
SomeSpecificCollection
vous vous référez à juste Element
, et partout ailleurs comme SomeSpecificCollection::Element
- qui a l'air + - identique à 3., mais plus clairSomeSpecificCollection
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.
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.
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
.
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 Field
s 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 Field
s 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"));
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 {}