C++ : le mot clé explicit
Rédigé par Nicolas K 2 commentairesDepuis C++11, tout constructeur défini sans le mot clé explicit
est un constructeur de conversion. En d’autres termes, le ou les types d’arguments d’un tel constructeur peuvent être converti implicitement par le compilateur en la classe contenant ce constructeur.
De la même façon, il est possible de définir des opérateurs de conversion pour convertir notre classe en un type donné. Ces conversions, également implicites par défaut, sont parfois très pratiques.
Cependant, dans la majorité des cas, et comme tout ce qui est implicite, cela peut conduire à des erreurs. Nous allons voir par l’exemple comment le mot clé explicit
peut nous permettre d’éviter certaines erreurs.
Constructeur de conversion
Comme dit plus haut, depuis C++11 :
explicit
est un constructeur de conversion.Cela signifie que les types de paramètres du constructeur peuvent être convertis implicitement en la classe contenant ce constructeur. Un exemple pour être plus clair : imaginons qu’on ait une classe Color
qui représente une couleur, et une classe Circle
qui représente un cercle coloré :
struct Color {
int r, g, b;
};
class Circle {
public:
Circle(Color const& color) : color(color) {}
float getRayon() const {
return rayon;
}
private :
float rayon{5};
Color color{255,255,255};
};
Jusque là, rien de bien compliqué. On souhaite maintenant avoir une fonction getSurface
qui retourne la surface d’un cercle :
// déclaration de pi
template<typename T>
T const pi = static_cast<T>(3.141592653589793238462643383279502884);
// fonction qui retourne la surface d’un cercle
float getSurface(Circle const& circle) {
return circle.getRayon()*2.f*pi<float>;
}
Toujours rien de bien compliqué. Le but est de pouvoir utiliser notre fonction comme ceci :
int main()
{
// on déclare une couleur
auto red = Color{255,0,0};
// on déclare un cercle
auto circle = Circle{red};
// on affiche la surface du cercle
std::cout << getSurface(circle) << std::endl;
}
Très bien. Mais puisque le constructeur de Circle
qui prend en paramètre un objet de type Color
est implicite, il s’agit d’un constructeur de conversion, et ceci fonctionne également :
std::cout << getSurface(red) << std::endl;
Plutôt surprenant n’est-ce pas ? Ici, le compilateur a créé un objet temporaire Circle
à partir de la couleur red
et a appliqué la fonction getSurface
sur cet objet temporaire… Ça peut parfois être déroutant, et même dans des cas plus complexes générer des erreurs surprenantes.
Pour éviter que ce code fonctionne, il faut donc interdire cette conversion implicite, et pour cela, il suffit de déclarer notre constructeur explicit
:
explicit Circle(Color const& color) : color(color) {}
De cette façon, l’appel getSurface(red)
ne compile plus.
Avec la pratique, on se rend compte que dans la majorité des cas, les conversions implicites ne sont pas souhaitées. Ainsi, un bon réflexe à avoir :
explicit
.Opérateurs de conversion
Après les constructeurs de conversion, les opérateurs de conversion. Déjà, contrairement aux constructeurs, ils ont normalement été écrits dans le but de convertir un type en un autre. Mais comme les constructeurs, la conversion qu’ils permettent est également implicite par défaut. Et encore une fois, ce n’est pas toujours souhaité.
Imaginons qu’on ait une classe qui gère une ressource par exemple et pour laquelle on souhaite pouvoir tester si elle est bien disponible simplement avec un if :
int main() {
auto resource = Resource{};
if(resource) {
std::cout << "ok" << std::endl;
} else {
std::cout << "ressource indisponible" << std::endl;
}
}
On déclare dans notre classe Resource
l’opérateur de conversion bool
pour permettre ce if :
class Resource { public : operator bool() const { return true; } };
Super. Ça fonctionne. Maintenant, un peu plus loin dans le code, on crée une seconde ressource, et on souhaite tester son égalité avec la première :auto resource2 = Resource{}; if(resource == resource2) { std::cout << "les ressources sont identiques !" << std::endl; }
On s’attend à ce qu’il compare les deux ressources n’est-ce pas ? Eh bien pas du tout. L’opérateur ==
n’étant pas déclaré, resource
et resource2
sont d’abord converties en booléen et ce sont les booléens obtenus qui sont comparés entre eux. La condition est donc vérifiée.
J’ai simplifié volontairement ici les exemples, mais on peut vite tomber sur des exemples beaucoup plus pernicieux. La solution à cela : à nouveau le mot clé explicit
:
explicit operator bool() const {
std::cerr << "plop";
return true;
}
Et maintenant, le code précédent ne compile plus, et on remarque tout de suite que l’opérateur ==
n’est pas définit.
Et parce qu’on n’aime pas ce qui est implicite, un autre conseil pour éviter les erreurs de ce type :
explicit
!Conclusion
Que ce soit par les constructeurs de conversion ou par les opérateurs de conversions, les conversions sont par défaut implicites et cela peut conduire à des erreurs ou tout au moins des comportements inattendus. Utiliser le mot clé explicit
à chaque fois permet de détecter ce genre de problèmes dès la compilation, histoire de ne pas s’arracher les cheveux devant une erreur inexpliquée. Et lorsque la conversion implicite est souhaitée, il suffit de retirer le mot clé explicit
.
Je trouve d’ailleurs que c’est dommage que le comportement ne soit pas inversé : conversion explicite par défaut, et un mot clé implicit pour déclarer la volonté de pouvoir convertir implicitement. Il y a sans doute des raisons qui ont poussé les concepteurs du C++ de faire ce choix, mais j’avoue qu’à l’instant je ne l’explique pas. Auriez-vous une idée de ce qui a conduit à ce choix ?