Étonnamment, l'initialisation est un sujet complexe en C++. Ce qui suit couvre quelques aspects de cette question, mais vous voudrez sans doute en savoir aussi sur :
Depuis C++ 98 (au moins!), il est possible de qualifier un constructeur du mot clé explicit, pour forcer le code client à affirmer son intention et éviter certains accidents. Par exemple :
Sans constructeur qualifié explicit | Avec constructeur qualifié explicit |
---|---|
|
|
Les constructeurs explicit sont surtout utiles pour les objets qui peuvent être instanciés à l'aide d'un seul paramètre. Pour quelques exemples :
#include <string>
struct X {
X(int); // envisagez explicit ici
};
struct Y {
Y(int, std::string = ""); // envisagez explicit ici
};
struct Z {
Z(int = 3); // envisagez explicit ici
};
Avec C++ 20, il est possible d'apposer explicit de manière conditionnelle.
Empruntant un exemple à Simon Brand, supposons une classe générique wrapper<T> contenant un T et encadrant son utilisation, et supposons que nous souhaitions un constructeur paramétrique capable de construire un T à partir d'un U. Dans l'extrait de code ci-dessous, le constructeur de wrapper<T> acceptant un const U& sera qualifié explicit seulement s'il n'y a pas de conversion implicite d'un T à un U (donc, si la conversion d'un U à un T est implicite, alors la conversion d'un U à un wrapper<T> le sera aussi) :
#include <type_traits>
template <class T>
struct wrapper {
template <class U>
explicit(!std::is_convertible_v<U, T>)
wrapper(U const & u) : val(u) {
}
T val;
// ...
};
Ceci donne un contrôle plus fin sur ce subtil mais utile mécanisme.
Il est possible depuis C++ 11 de qualifier les opérateurs de conversion du mot-clé explicit, comme on pouvait déjà le faire pour les constructeurs paramétriques. Ceci donne ici encore un contrôle plus fin sur ce mécanisme.
Par exemple, avant C++ 11, le code suivant aurait mené à une ambiguïté :
struct Trois {
operator int() const {
return 3;
}
operator const char*() const {
return "trois";
}
};
void f(const char*) {
}
void f(int) {
}
int main() {
f(Trois{}); // ambigu
}
La raison de cette ambiguïté est qu'il n'existe pas de fonction f(Trois), mais qu'il existe deux fonctions f(int) et f(const char*), or la conversion d'un Trois à un int ou à un const char* est implicite, ce qui empêche le compilateur de faire un choix.
Depuis C++ 11, il est possible de qualifier explicit l'un, l'autre ou tous les opérateurs de conversion, ce qui impose au code client d'expliciter son intention. Nous en arrivons donc à :
struct Trois {
explicit operator int() const { // <-- ICI
return 3;
}
operator const char*() const {
return "trois";
}
};
void f(const char*) {
}
void f(int) {
}
int main() {
Trois tr;
// Ok, f(int), conversion explicite
f(static_cast<int>(tr));
// Ok, f(int), conversion explicite
f(int{ tr });
f(tr); // Ok, f(const char*)
}
... qui compile sans ambiguïté.
Quelques liens pour enrichir le propos.