Plusieurs ne le savent pas, mais le langage C++ repose fortement sur le principe des noms. Il est par exemple possible d'utiliser un pointeur vers une classe ou vers un struct en introduisant son nom avec une déclaration a priori, et de manipuler ce pointeur en tant que pointeur dans le code. Tout ce qui ne dépend ni de la taille de l'objet pointé, ni de ses membres peut être fait sur la simple base de son nom, à travers une indirection.
class X; // déclaration a priori (Forward Declaration)
X generer(); // Ok; pas besoin de connaître le détail d'un X pour valider ceci
void utiliser(const X&); // Ok; pas besoin de connaître le détail d'un X pour valider ceci
X *creer(); // Ok techniquement, quoique retourner un pointeur brut d'une fonction soit une pratique discutable
int main() {
X *p = creer(); // Ok
utiliser(*p); // Ok
// utiliser(generer()); // Incorrect; susceptible de demander une copie intermédiaire
}
Un prototype de fonction est d'ailleurs une sorte de déclaration a priori, introduisant non seulement un nom mais aussi une signature cette fois. Le code ci-dessus compile sans problème; évidemment, si la définition des fonctions n'est pas trouvée dans l'un des modules objets lors de l'édition des liens, alors on aura une erreur à cette étape, mais la compilation aura tout de même réussi.
Lorsqu'une classe est dérivée d'une autre classe, par défaut, les noms publics et protégés du parent sont aussi exposés par l'enfant. Par exemple :
struct X {
int f() const {
return 3;
}
};
struct Y : X {
int g() const {
return 4;
}
};
int main() {
X x;
x.f(); // Ok
Y y;
y.f(); // Ok (visible à travers un Y car un Y est aussi un X)
y.g(); // Ok
}
Toutefois, comme mentionné plus haut, C++ procède par nom. Ainsi, si l'enfant utilise le même nom pour l'un de ses services que celui utilisé par l'un de ses parents, alors le nom du parent est caché par celui de l'enfant. Dans le cas des méthodes, ceci s'avère, même si les signatures sont différentes. Par exemple :
struct X {
int f() const {
return 3;
}
};
struct Y : X {
int g() const {
return 4;
}
int f(int n) const { // même nom que dans X, mais signatures différentes
return n + 2;
}
};
int main() {
X x;
x.f(); // Ok
Y y;
y.f(); // Incorrect; Y::f(int) const cache X::f() const
y.f(3); // Ok
y.g(); // Ok
}
Si le souhait de la programmeuse ou du programmeur est d'exposer X::f(), deux solutions sont possibles. La moins élégante (à mes yeux, du moins) est de forcer le code client à exprimer explicitement son intention d'avoir recours au f() de X à travers y, comme dans l'exemple suivant :
struct X {
int f() const {
return 3;
}
};
struct Y : X {
int g() const {
return 4;
}
int f(int n) const { // même nom que dans X, mais signatures différentes
return n + 2;
}
};
int main() {
X x;
x.f(); // Ok
Y y;
y.X::f(); // Ok, mais pas nécessairement élégant
y.f(3); // Ok
y.g(); // Ok
}
Une approche moins « manuelle » serait de faire en sorte que Y indique son souhait de rendre visibles le nom X::f à travers sa propre interface, malgré la présence de Y::f qui le masque :
struct X {
int f() const {
return 3;
}
};
struct Y : X {
int g() const {
return 4;
}
int f(int n) const { // même nom que dans X, mais signatures différentes
return n + 2;
}
using X::f; // voilà!
};
int main() {
X x;
x.f(); // Ok
Y y;
y.f(); // Ok; explicitement exposé par Y
y.f(3); // Ok
y.g(); // Ok
}
Nouveauté avec C++ 11 : un enfant peut exposer les constructeurs de son parent. Ceci permet de pallier une situation telle que la suivante :
class X {
int val = {};
public:
X() = default;
X(int val) : val{val} {
}
int valeur() const {
return val;
}
};
struct Y : X {
Y(int val) : X{ 2 * val } {
}
};
int main() {
X x0; // Ok, appelle X::X()
X x1{ 3 }; // Ok, appelle X::X(int)
// Y y0; // pas Ok; pas de constructeur par défaut dans Y
Y y1{ 3 }; // Ok, appelle Y::Y(int)
}
Ici, si l'intention de la programmeuse ou du programmeur était de spécialiser le comportement d'un Y paramétrique tout en conservant le comportement par défaut du X, alors traditionnellement, la seule option à sa disposition était d'implémenter explicitement un constructeur par défaut bidon pour l'enfant, constructeur qui ne faisait que déléguer à celui par défaut du parent :
class X {
int val = {};
public:
X()= default;
X(int val) : val{val} {
}
int valeur() const {
return val;
}
};
struct Y : X {
Y() = default; // appelle implicitement X::X()
Y(int val) : X{ 2 * val } {
}
};
int main() {
X x0; // Ok, appelle X::X()
X x1{ 3 }; // Ok, appelle X::X(int)
Y y0; // Ok; appelle Y::Y() qui appelle X::X()
Y y1{ 3 }; // Ok, appelle Y::Y(int)
}
Il est désormais possible pour l'enfant d'indiquer, par une instruction using, son souhait de laisser filtrer les constructeurs du parent. Notez que ceci se fait par nom, pas par signature; si certains constructeurs du parent doivent à votre avis demeurer cachés, alors il vous faudra encore faire un cas par cas :
class X {
int val = {};
public:
X() = default;
X(int val) : val{val} {
}
int valeur() const {
return val;
}
};
struct Y : X {
using X::X;
Y(int val) : X(2 * val) {
}
};
int main() {
X x0; // Ok, appelle X::X()
X x1{ 3 }; // Ok, appelle X::X(int)
Y y0; // Ok; appelle X::X()
Y y1{ 3 }; // Ok, appelle Y::Y(int)
}
Voilà!