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 :
-
déclarer deux classes : SomeSpecificCollection
y Element
- mauvais, car le nom "Element" est suffisamment général pour provoquer un éventuel conflit de noms
-
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 ?
-
déclarer deux classes globales SomeSpecificCollection
y SomeSpecificCollectionElement
- qui présente des inconvénients mineurs, mais qui est probablement acceptable.
-
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
.
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.
14 votes
@Billy : Pourquoi ? Ça me semble trop large.
0 votes
La composition de classes peut peut-être être une alternative aux classes imbriquées
1 votes
@Akhil : Peut-être que la composition de classes comme alternative aux classes imbriquées est une façon de mettre une cheville carrée dans un trou rond.
1 votes
La seule fois où j'ai eu besoin de classes, c'était lorsque j'utilisais l'horrible API d'un fournisseur qui nécessitait diverses manigances d'exécution pour utiliser leur constructeur. C'était un désordre horrible et c'est notre base de code la plus laide. Morale de l'histoire : @Billy a raison ; n'utilisez pas de classes imbriquées si vous n'en avez pas besoin.
1 votes
@John : Parce qu'il n'y a jamais de raison de déclarer une classe imbriquée. Ce n'est pas comme, disons, Java, où le fait d'être une classe imbriquée confère à cette classe un avantage sur son parent.
35 votes
Je n'ai toujours pas vu d'argument expliquant pourquoi les classes imbriquées sont mauvaises par nature.
5 votes
@Billy : Qu'en est-il du name scoping, comme dans mon exemple ci-dessous ? L'expression n'est-elle pas
Field::match(...)
assez naturel et auto-documenté ? Bien sûr, ce n'est pas nécessaire on pourrait déclarer une classe adaptée à l'espace de nommage comme suitmatch_field
tout aussi facilement, mais cela ne rend pas la classe imbriquée mauvais c'est une autre façon de dépecer le chat.3 votes
@John : En fait, je pense que votre exemple illustre parfaitement pourquoi il faut éviter les classes imbriquées. Vous avez un
id
à la fois dans la classe externe et dans la classe imbriquée, et il est difficile de savoir lequel des deux fait référence à quoi. En ce qui concerne lesField::match
Quel est le problème avec une classe non imbriquée ?FieldMatch
? Si vous voulezField::match
pour travailler, il suffit de fairetypedef FieldMatch match
à l'intérieur deField
et cela fonctionnera toujours, et vous n'aurez pas le problème des conflits de portée entre la classe principale et la classe imbriquée.0 votes
@Billy, si
id
n'est pas un membre statique, pourquoi cela porterait-il à confusion ? Pour moi, la question n'est pas de savoir comment obtenir un membre statique.Field::match
avec une classe définie de manière externe, mais quelle est la raison de ne pas faire il est imbriqué. À cause des conflits de portée que vous avez mentionnés ? Qu'est-ce qu'ils ont de si mauvais ?8 votes
@7vies : 1. parce que ce n'est tout simplement pas nécessaire -- vous pouvez faire la même chose 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 d'indentation supplémentaire dans un environnement où il est déjà difficile d'éviter les longues lignes 4. parce que vous déclarez deux objets conceptuellement distincts dans un seulclass
déclaration, etc.4 votes
@Billy : 1,2. Vous ne pouvez pas obtenir exactement les mêmes effets qu'une classe privée imbriquée avec une classe séparée plus
typedef
. 3. Pas d'indentation supplémentaire si vous définissez la classe imbriquée à l'extérieur. `struct Outer { struct Inner ; } ; struct Outer::Inner {} ; 4. habituellement une classe imbriquée et sa classe externe sont si étroitement liées qu'il est logique de laisser leurs déclarations s'entrecroiser.1 votes
@aschepler : RE : 1,2 : En quoi cela diffère-t-il exactement alors ?
0 votes
@Billy : Même si vous avez une classe définie en externe, elles auront toutes les deux un
id
dans le cas de mon exemple. Regardez à nouveau.1 votes
@John : Je le sais - ce que je veux dire, c'est que c'est plus déroutant d'avoir deux noms identiques de cette façon - ce n'est pas faux ou ambigu, mais ce n'est certainement pas évident à l'examen superficiel qui
id
que l'auteur de la classe recherchait. (En particulier parce que dans votre exemple, nous avons vu que la classe externeid
avant qu'elle ne soit utilisée, mais nous n'avons pas vu la classe interneid
quand il est utilisé, en supposant que nous lisions de haut en bas)4 votes
Il semble utile de mentionner qu'une classe interne est implicitement une amie du parent ?
0 votes
Un inconvénient des classes imbriquées que je n'ai pas encore vu mentionné est qu'elles ne peuvent pas être déclarées en avant mais nécessitent la définition de la classe parente, ce qui nécessite des dépendances d'en-tête que vous pourriez autrement éviter.
0 votes
Java utilise beaucoup de classes imbriquées/internes (j'en ai utilisé quelques-unes), peut-être pouvons-nous obtenir quelques résultats à partir de là. Personnellement, je n'en ai jamais utilisé en C++.