On peut déclarer une classe qui soit interne à une autre classe, comme on peut déclarer une classe qui soit interne à une fonction.
La syntaxe est telle que visible à droite. Une classe imbriquée peut être privée, protégée ou publique, comme n'importe quel autre membre. Une classe interne a directement accès à tous les membres de classe (les membres spécifiés static, qu'ils soient privés, protégés ou publics) de sa classe externe. On peut référer directement à une classe interne à l'aide d'une syntaxe rappelant celle des membres de classe. Par exemple, pour instancier x comme étant de la classe Interne elle-même interne à la classe Externe, on écrira Externe::Interne x;). En langage Java, une classe imbriquée peut être un membre d'instance ou un membre de classe. En C++, une classe imbriquée est nécessairement un membre de classe, qu'on la déclare static ou non. |
|
Cela peut sembler étrange de prime abord. Pourquoi utiliserait-on cette possibilité? En général, les classes imbriquées seront un choix de design lorsque :
En Java, les classes imbriquées sont assez fréquemment rencontrées, mais cela découle surtout de l'habitude qu'on a de faire beaucoup de classes dérivées à usage unique plutôt que de déclarer des instances particulières d'une classe donnée comme on le fait en C++.
Un exemple classique de classes imbriquées en Java est de définir le curseur associé à une zone de texte de manière interne à la classe représentant cette zone de texte.
La zone de texte serait une instance d'une classe dérivée de la classe standard représentant une zone de texte (la classe JTextField, disons) et on retrouverait entre autres dans cette classe une autre classe pour ses propres curseurs, qui seraient eux-mêmes des dérivés des classes de curseur standards.
Vous trouverez ci-dessous un exemple de classe imbriquée. Cet exemple vous propose une classe Resultats, qui garde en note des résultats scolaires (ceux d'un(e) étudiant(e) ou d'une classe entière, peu importe).
Pour faciliter la bonne gestion des notes sur une base individuelle, une classe Note qui est interne (et privée) à Resultats est déclarée. Cette classe assure la validité de chaque note prise sur une base individuelle. Chaque Note sait si elle représente une réussite ou un échec.
La classe Resultats permet d'ajouter des notes à celles déjà emmagasinées. Les instances de Note conservées par une instance de Resultats le sont dans un vecteur standard. Resultats offre des services de calcul de la moyenne, du nombre d'échecs et du nombre de réussites pour les résultats qu'elle représente.
//
// Resultats.h
//
#include <vector>
class Resultats
{
public:
class NoteInvalide { }; // pour traitement d'exceptions
private:
class Note // classe imbriquée
{
public:
using value_type = int;
private:
//
// membres de classe de Resultats::Note
// (p. ex. : Resultats::Note::MIN_VALEUR)
//
static const int
MIN_VALEUR, MAX_VALEUR, SEUIL_PASSAGE;
value_type valeur_;
public:
static bool valeur_valide(value_type val) noexcept
{ return MIN_VALEUR <= val && val <= MAX_VALEUR; }
value_type valeur() const noexcept
{ return valeur_; }
Note() noexcept
: valeur_{MIN_VALEUR}
{
}
Note(value_type val)
: valeur_{val}
{
if (!valeur_valide(Val))
throw NoteInvalide{};
}
//
// La sainte-trinité par défaut est Ok
// (construction par copie, destruction,
// affectation)
//
bool operator==(const Note &n) const noexcept
{ return valeur() == n.valeur(); }
bool operator!= const Note &n) const noexcept
{ return !(*this == n); }
bool operator<(const Note &n) const noexcept
{ return valeur() < n.valeur(); }
bool operator>(const Note &n) const noexcept
{ return n < *this; }
bool operator>=(const Note &n) const noexcept
{ return !(*this < n); }
bool operator<=(const Note &n) const noexcept
{ return !(n < *this); }
bool est_reussite() const noexcept
{ return valeur() >= SEUIL_PASSAGE; }
bool est_echec() const noexcept
{ return !est_reussite(); }
};
//
// Vecteur de Resultat::Note. Remarquez qu'on peut omettre
// Resultat:: ici, étant dans le contexte de Resultat
//
std::vector<Note> resultats_;
public:
using size_type = std::vector<Note>::size_type;
using value_type = Note::value_type;
class PasDeNotes { }; // pour traitement d'exceptions
//
// Pas de constructeurs particuliers; la construction
// par défaut et par copie générés par le compilateur
// sont corrects; nous avons un pur type valeur
//
void ajouter(const Note &n)
{ resultats_.push_back(n); }
value_type moyenne() const;
size_type nb_echecs() const noexcept;
size_type nb_reussites() const noexcept;
size_type nb_resultats() const noexcept
{ return resultats_.size(); }
};
//
// Resultats.cpp
//
#include "Resultats.h"
//
// Définition de méthodes de la classe externe,
// qui utilisent le type Resultat::Note
//
#include <algorithm>
#include <numeric>
using namespace std;
auto Resultats::moyenne() const -> value_type
{
if (!nb_resultats())
throw PasDeNotes{};
return accumulate(begin(resultats_), end(resultats_), value_type{})/ nb_resultats();
}
auto Resultats::nb_echecs() const -> size_type
{
return count_if(
begin(resultats_), end(resultats_), [](const Note &n) {
return n.est_echec();
});
}
auro Resultats::nb_reussites() const -> size_type
{
return nb_resultats() - nb_echecs();
}
//
// Initialisation des constantes de classe de la
// classe interne Resultats::Note
//
const int Resultats::Note::MIN_VALEUR = 0,
Resultats::Note::MAX_VALEUR = 100,
Resultats::Note::SEUIL_PASSAGE = 60;
//
// Principal.cpp
// Exemple d'utilisation de Resultats. La classe
// interne y est invisible
//
#include "Resultats.h"
#include <iostream>
using namespace std;
int main()
{
Resultats r;
for(Note::value_type n; cin >> n;)
r.ajouter(n);
cout << "Moyenne: " << r.moyenne() << ", "
<< "Nb. echecs: " << r.nb_echecs() << ", "
<< "Nb. succes: " << r.nb_reussites() << ", "
<< "Nb. resultats (total): " << r.nb_resultats()
<< endl;
}
Cette section propose une implémentation simpliste (et qui n'est pas de calibre industriel!) d'une « optimisation » maintenant interdite sur des chaînes de caractères en C++, soit Copy on Write, ou COW. Conséquemment, ne prenez ce qui suit que comme une illustration des possibilités des classes internes, pas comme un exemple à suivre.
Les classes internes servent souvent de classes de support pour permettre une implémentation plus efficace de certains concepts. L'exemple à cet effet que je me propose de vous esquisser ici en est un cas classique. Imaginons l'ébauche de programme visible à droite. Le programme semble banal et efficace : après tout, copier du texte d'un endroit à un autre est une tâche informatique routinière. Mais c'est sous-estimer un facteur important: nous ne connaissons pas ici la taille du texte retourné par lire_texte()... et nous ne voulons pas nécessairement la connaître, d'ailleurs! |
|
Si lire_texte() retourne une chaîne de quelques milliers de caractères à peine, l'action de copier ce texte de sa représentation dans lire_texte() à s1 dans main() est banale, et il en va de même pour la copie dans s2 de s1 à la ligne suivante.
Si par contre lire_texte() retourne une masse de plusieurs mégaoctets de caractères, ces deux opérations de copie seront à la fois longues et potentiellement coûteuses en espace (s2 et s1 ayant la même portée).
L'encapsulation et les classes internes viennent toutefois à notre secours – et ce, à notre insu et de manière transparente, comme il se doit quand on parle d'encapsulation.
En effet, on présume sans doute que std::string conserve les caractères qui y sont insérés dans un tableau, comme proposé dans l'ébauche (inexacte à plusieurs égards; il s'agit d'une illustration) à droite. Selon cette illustration (incorrecte; entre autres, en pratique, std::string n'est qu'une instanciation de std::basic_string<C> où C est le type char), chaque chaîne aurait sa propre copie du texte contenu. Copier s1 dans s2 signifierait alors (grossièrement) créer un tableau de la bonne taille dans s2 et y copier un à un les caractères contenus dans s1. |
|
Ce n'est toutefois pas la seule implantation possible du concept de chaîne de caractères, ni celle réellement utilisée d'ailleurs dans les implémentations efficaces. On peut faire beaucoup mieux.
Imaginons qu'une std::string soit plutôt comme une enveloppe, dans laquelle on retrouverait la représentation du texte qu'elle contient. La représentation en question est une abstraction entièrement locale et interne à la chaîne – c'est un cas clair de classe interne.
Copier s1 dans s2 pourrait alors être équivalent à faire de la représentation de s2 une copie de la représentation de s1, bien sûr, mais on n'y gagnerait pas en efficacité.
Par contre, il pourrait s'agir de faire pointer s2 sur la même représentation que celle sur laquelle pointe s1. Ainsi, plutôt que de copier un contenu (de taille arbitraire), on copie dans la mesure du possible un pointeur (de taille fixe et petite).
Pour que cela fonctionne bien, il faut :
Illustrons le concept avec une chaîne académique (une std::string simplifiée pour démontrer le concept présenté ici). Nous nommerons chaine notre chaîne simplifiée.
Nous n'implanterons pas toutes les opérations utiles qui soient possibles sur une instance de chaine. Vous êtes invité(e)s à en ajouter si le coeur vous en dit.
Programme de démonstrationNotre programme de démonstration sera celui visible à droite. Si notre implantation est correcte, il ne devrait y avoir qu'une seule représentation interne du texte "allo toi" jusqu'à la ligne où on trouve l'opération c3+='!'; qui modifie la représentation de c3, forçant c3 à se créer une version interne propre à lui de la représentation du texte avant de procéder à l'ajout du caractère '!'. L'affichage à la console, à la toute fin, devrait présenter "allo toi", "allo toi" et "allo toi!", dans l'ordre. |
|
Le fichier chaine.h contiendra la déclaration de la classe chaine, incluant celle de sa classe interne chaine::Rep.
#ifndef CHAINE_H
#define CHAINE_H
#include <iosfwd> // pour surcharger << sur les flux
#include <utility>
class chaine
{
public:
uaing size_type = int;
using value_type = char;
private:
// ----------- Début de la classe interne
class Rep
{
size_type lg_; // nombre de caractères représentés
// texte représenté, ASCIIZ, délimité à la fin par un 0
value_type *texte_;
int cptref_; // compteur de références interne
public:
void ajouter(value_type); // ajoute un caractère à la fin
size_type lg() const noexcept; // retourne la longueur du texte
const value_type* texte() const; // retourne le texte (constant)
value_type* texte(); // retourne le texte (modifiable)
// retire une référence à l'objet; le détruit au besoin
void release();
void add_ref(); // ajoute une référence à l'objet
bool solitaire() const; // vrai seulement si c'est la dernière référence sur la représentation
Rep(); // constructeur par défaut
Rep(const value_type*); // constructeur paramétrique
Rep(const Rep&); // constructeur de copie
~Rep() noexcept; // destructeur
};
// ----------- Fin de la classe interne
// ...
Examinons l'interface proposée pour la classe interne :
Chaque chaine aura comme attribut un pointeur de Rep nommé rep_. C'est avec cet attribut que nous implanterons la stratégie dans laquelle nous nous sommes engagés.
Le reste de la déclaration de chaine suit.
// ...
mutable Rep *rep_; // le pointeur sur la représentation interne
public:
void swap(chaine &s) noexcept
{
using std::swap;
swap(rep_, s.rep_);
}
// pour gérer les index invalides avec []
class HorsBornes {};
chaine() noexcept; // constructeur par défaut
chaine(const value_type*); // constructeur paramétrique
chaine(const chaine&) noexcept; // constructeur par copie
~chaine() noexcept; // destructeur
size_type longueur() const noexcept; // retourne la longueur de la chaine
operator const value_type*() const; // retourne le texte brut (constant)
// opérateurs ayant le comportement habituel; représentation
// non modifiée
bool operator==(const chaine&) const noexcept;
bool operator!=(const chaine&) const noexcept;
char operator[](size_type) const;
// la représentation peut être modifiée avec celui-ci, car on
// retourne une référence sur un caractère. Il faudra donc être
// plus prudent ici
char& operator[](size_type);
// opérateurs modifiant clairement la représentation interne
chaine& operator=(const chaine&);
chaine& operator+=(value_type);
// la fonction qui suit est déclarée amie pour éviter de
// rendre publique la représentation interne de la chaîne
// (nous y reviendrons sous peu)
friend std::ostream& operator<<(std::ostream&, const chaine&);
};
#endif
Nous y irons, pour cette section, d'explications par étapes. On présumera bien sûr que le fichier chaine.h aura été inclus au préalable.
La fonction globale surchargeant l'opérateur << sur un std::ostream (comme par exemple std::cout) est relativement simple.
Il est à noter que, pour éviter d'avoir à écrire un à un les caractères de la chaîne à afficher (utilisant l'opérateur [] de chaine), nous accédons directement ici à son attribut rep_. C'est ce qui explique l'emploi de la mention friend dans la classe chaine au sujet de cette fonction. |
|
Classe interne chaine::RepLa classe interne chaine::Rep sert pour représenter le texte contenu dans la chaîne et la longueur de ce texte, au sens du nombre de caractères qui y sont représentés. La méthode ajouter() de cette classe sert à insérer un caractère à la fin de la chaîne représentée. Ce qui suit est une implantation correcte, mais qui a un gros défaut : elle est très, très inefficace. Pour nos fins illustratives, elle suffira malgré tout. |
|
Quelques méthodes banales devront être rédigées :
L'implémentation de chacune de ces méthodes est banale. Notez toutefois que les deux versions de texte() doivent être manipulées avec beaucoup de prudence, se prêtant à des abus et à des manipulations impropres des états de la chaine. Le mot const aide un peu ici mais ne règle pas la plupart des problèmes de fond de cette interface (notez que les principaux problèmes de cette interface existeraient aussi, sous une forme connexe, en Java ou en C#). |
|
La représentation interne étant vouée à être partagée entre plusieurs instances de chaine, il nous faudra implanter un mécanisme pour y compter les références. Cette interface est discutable, mais s'inspire de pratiques existantes en industrie. |
|
Les constructeurs iront essentiellement comme suit. Notez que les compteurs de référence sont, comme il se doit, initialisés à 1, sachant que celui créant la représentation est alors nécessairement son unique client (pour le moment). Le destructeur suite une forme prévisible dans les circonstances. Vous remarquerez que plusieurs des opérations suggérées ici sont lentes en susceptibles d'être optimisées. Ne vous gênez pas pour le faire, mais testez votre code par la suite! |
|
Bien que notre classe chaine soit délestée de la tâche de gérer le détail de sa représentation interne, la responsabilité de gérer les accès à cette représentation, de même que celle d'offrir une interface permettant d'accéder de manière transparente à cette représentation, lui est dévolue.
Les constructeurs auront chacun une stratégie distincte :
Le destructeur se limitera à retirer une référence à la représentation interne (s'il y a effectivement une représentation interne à la chaîne), ce qui permettra à la représentation interne de s'autodétruire si elle reconnaît ne plus être utilisée. |
|
Pour implanter les accesseurs, nous profiterons ici de l'encapsulation stricte appliquée à la représentation interne de la chaîne :
|
|
Les opérateurs de comparaison == et != s'implantent de manière évidente si on utilise la méthode longueur() et l'opérateur [], présumant bien entendu que ces méthodes sont correctement implantées. Remarquez qu'à l'interne, toutefois, j'ai choisi d'éviter l'opérateur [] du fait que cette méthode valide l'indice reçu en paramètre, ce dont je n'ai pas besoin ici (cela ralentirait l'exécution du code de manière importante). |
|
L'opérateur [] doit être offert sous deux versions si nous voulons être conformes aux usages courants sur des chaînes de caractères :
Remarquez, dans une forme comme dans l'autre, que les indices hors bornes sont notés en levant une exception, ce qui est probablement la solution la plus élégante dans un tel cas.
auto chaine::operator[](size_type n) const -> value_type
{
if (!rep_ || n >= rep_->lg()) throw HorsBornes{};
return rep_->texte()[n];
}
auto chaine::operator[](size_type n) -> value_type&
{
if (!rep_ || n >= rep_->lg()) throw HorsBornes{};
if (!rep_->solitaire())
{
auto p = new Rep{*rep_};
rep_->release();
rep_ = p;
}
return rep_->texte()[n];
}
La forme non-const, bien qu'elle ne modifie pas en elle-même la représentation interne de la chaîne, permet une telle modification en retournant une référence à un des caractères. De ce fait, il est impératif que cette méthode s'assure que la représentation interne de la chaîne lui soit unique – que le nombre de références à la représentation soit 1.
Si d'autres utilisateurs existent de la même représentation, alors la chaîne courante doit s'en faire une copie au préalable et relâcher sa référence à la représentation partagée.
L'opérateur d'affectation doit faire en sorte que la représentation initiale de l'opérande de gauche soit relâchée (s'il y a lieu) et que l'opérande de gauche utilise par la suite la représentation de l'opérande de droite (s'il y a lieu). Nous réalisons tous cela en appliquant le très pertinent idiome d'affectation sécuritaire, mis de l'avant par Herb Sutter. Le lieu de la duplication est le constructeur de copie; le lieu d'échange des valeurs est swap(), et le lieu du nettoyage est le destructeur. |
|
L'opérateur d'ajout d'un caractère à la fin (opérateur +=) est clairement un opérateur voué à la modification de la représentation interne. Il est terriblement lent dans ce cas, du fait que la méthode ajouter() est elle-aussi horriblement lente. |
|