🔝 Retour au Sommaire
Les sections précédentes ont montré comment surcharger les opérateurs arithmétiques et d'affectation pour manipuler des objets avec une syntaxe naturelle. Mais une autre dimension de l'intégration d'un type dans l'écosystème C++ est la capacité à se convertir vers d'autres types — et inversement.
En C++, deux mécanismes permettent les conversions entre types utilisateur et types existants :
- Les constructeurs de conversion : un constructeur qui accepte un seul argument d'un autre type. C'est le mécanisme vu avec
Fraction(int)en section 8.1 — l'entier est converti enFraction. - Les opérateurs de conversion (conversion operators) : des fonctions membres spéciales qui permettent à un objet d'être converti vers un autre type. C'est ce que cette section couvre.
Ces deux mécanismes sont complémentaires mais asymétriques. Le constructeur de conversion est défini dans le type cible (celui qu'on construit). L'opérateur de conversion est défini dans le type source (celui qu'on convertit). Quand vous contrôlez les deux types, vous avez le choix. Quand le type cible est un type que vous ne pouvez pas modifier (int, bool, double, std::string), l'opérateur de conversion est votre seule option.
La question centrale de cette section est celle du contrôle : les conversions implicites sont pratiques mais dangereuses, et le mot-clé explicit est l'outil qui permet de trouver le bon équilibre.
Un opérateur de conversion se déclare comme une fonction membre sans type de retour (le type de retour est implicitement le type cible) et sans paramètre :
class Pourcentage {
double valeur_; // stocké comme 0.0 à 100.0
public:
explicit Pourcentage(double val) : valeur_{val} {}
// Opérateur de conversion vers double
operator double() const {
return valeur_ / 100.0; // retourne la fraction (0.0 à 1.0)
}
};L'utilisation :
Pourcentage tva{20.0};
double facteur = tva; // conversion implicite → 0.2
double prix = 100.0 * tva; // 100.0 * 0.2 = 20.0 La syntaxe est inhabituelle : pas de type de retour devant le nom de la fonction. Le type cible (double) fait partie intégrante du nom de l'opérateur. La méthode est const car la conversion ne modifie pas l'objet source.
L'opérateur de conversion tel que défini ci-dessus est implicite. Cela signifie que le compilateur l'invoquera automatiquement chaque fois qu'il a besoin d'un double et qu'il dispose d'un Pourcentage. C'est pratique dans les cas prévus, mais cela ouvre la porte à des conversions non intentionnelles et silencieuses :
Pourcentage tva{20.0};
// Conversions attendues :
double d = tva; // ✅ Clair — on veut un double
if (tva > 0.5) { ... } // ✅ Comparaison avec un double
// Conversions surprenantes :
int i = tva; // ⚠️ Compile — double → int (troncature silencieuse)
tva + tva; // ⚠️ Compile — les deux sont convertis en double
// le résultat est un double, pas un PourcentageLe dernier cas est particulièrement insidieux : tva + tva ne retourne pas un Pourcentage mais un double, parce que le compilateur convertit les deux opérandes en double et applique l'addition native. Si l'utilisateur s'attend à obtenir un Pourcentage, il obtient un résultat de type différent sans aucun avertissement.
Les conversions implicites se composent. Si Pourcentage a un operator double() implicite et qu'il existe ailleurs une fonction qui accepte un int, le compilateur peut enchaîner Pourcentage → double → int sans que le développeur en soit conscient. Plus un type a de conversions implicites, plus les résolutions de surcharge deviennent imprévisibles.
Le mot-clé explicit (appliqué aux opérateurs de conversion depuis C++11) interdit la conversion implicite. Le compilateur ne l'invoquera que dans un contexte de cast explicite ou dans certains contextes booléens spéciaux :
class Pourcentage {
double valeur_;
public:
explicit Pourcentage(double val) : valeur_{val} {}
// Conversion EXPLICITE vers double
explicit operator double() const {
return valeur_ / 100.0;
}
};Maintenant :
Pourcentage tva{20.0};
double d1 = tva; // ❌ Erreur : conversion implicite interdite
double d2 = static_cast<double>(tva); // ✅ Cast explicite — autorisé
double d3 = double(tva); // ✅ Cast fonctionnel — autorisé
auto d4 = (double)tva; // ✅ C-style cast — autorisé (mais déconseillé)
if (tva > 0.5) { } // ❌ Erreur : pas de conversion implicite
if (static_cast<double>(tva) > 0.5) { } // ✅ Explicite Le code est plus verbeux dans les cas légitimes, mais il est sûr : aucune conversion ne se produit sans que le développeur l'ait explicitement demandée.
La conversion vers bool est le cas d'usage le plus fréquent des opérateurs de conversion. De nombreux types de la bibliothèque standard la définissent : std::optional, std::unique_ptr, std::shared_ptr, std::ifstream, std::expected (C++23), etc.
Avant C++11, la bibliothèque standard utilisait des contournements tortueux (le "safe bool idiom" via conversion vers pointeur de membre) pour éviter les effets de bord d'un operator bool() implicite. Les problèmes étaient réels :
// Supposons un operator bool() IMPLICITE :
class Fichier {
public:
operator bool() const { return est_ouvert_; }
// ...
};
Fichier f1, f2;
int x = f1; // ⚠️ Compile : Fichier → bool → int
f1 + f2; // ⚠️ Compile : bool + bool = int
f1 < 42; // ⚠️ Compile : bool < int
f1 == "hello"; // ⚠️ Pourrait compiler selon le contexte Toutes ces expressions sont absurdes, mais elles compilent grâce à la conversion implicite Fichier → bool, suivie des promotions et conversions standard de bool.
class Fichier {
bool est_ouvert_;
public:
explicit operator bool() const {
return est_ouvert_;
}
};Avec explicit, les conversions absurdes sont bloquées :
Fichier f1, f2;
int x = f1; // ❌ Erreur
f1 + f2; // ❌ Erreur
f1 < 42; // ❌ Erreur Mais — et c'est la subtilité clé — les contextes booléens restent fonctionnels grâce à une exception spéciale du langage appelée contextual conversion to bool :
Fichier f;
if (f) { } // ✅ Contexte booléen — conversion autorisée
while (f) { } // ✅ Contexte booléen
bool ok = f ? true : false; // ✅ Contexte booléen (opérateur ternaire)
bool b = f && true; // ✅ Contexte booléen (opérateurs logiques)
if (!f) { } // ✅ Négation logique — contexte booléenLe standard définit précisément les contextes où la conversion explicit operator bool() est autorisée implicitement : conditions de if, while, for, do-while, opérandes de !, &&, ||, et l'opérateur ternaire ?:. En dehors de ces contextes, un cast explicite est requis.
Quasiment tous les types "nullable" ou "testables" de la bibliothèque standard utilisent explicit operator bool() :
std::optional<int> opt = trouver_valeur();
if (opt) { // explicit operator bool
std::println("Trouvé : {}", *opt);
}
std::unique_ptr<Widget> ptr = creer_widget();
if (ptr) { // explicit operator bool
ptr->afficher();
}
std::ifstream fichier{"data.txt"};
if (fichier) { // explicit operator bool
// lecture...
}💡 Règle simple : si vous implémentez un
operator bool(), il doit toujours êtreexplicit. Les cas où unoperator bool()implicite est justifié sont si rares qu'il est plus sûr de considérer cette règle comme absolue.
Les deux mécanismes permettent de convertir entre types, mais ils sont définis dans des classes différentes et ont des comportements distincts :
class Celsius {
double temp_;
public:
// Constructeur de conversion depuis Fahrenheit
Celsius(Fahrenheit const& f)
: temp_{(f.valeur() - 32.0) * 5.0 / 9.0} {}
};Le type Celsius sait comment se construire à partir d'un Fahrenheit. C'est le type cible qui contrôle la conversion.
class Fahrenheit {
double temp_;
public:
// Opérateur de conversion vers Celsius
operator Celsius() const {
return Celsius{(temp_ - 32.0) * 5.0 / 9.0};
}
};Le type Fahrenheit sait comment se convertir en Celsius. C'est le type source qui contrôle la conversion.
| Situation | Mécanisme recommandé |
|---|---|
| Vous contrôlez le type cible | Constructeur de conversion |
| Vous contrôlez le type source mais pas le type cible | Opérateur de conversion |
| Vous contrôlez les deux | Constructeur de conversion (préféré) |
Le type cible est un type primitif (int, double, bool) |
Opérateur de conversion (seule option) |
En général, le constructeur de conversion est préféré car il centralise la logique dans le type qui est construit, ce qui est plus naturel. L'opérateur de conversion est réservé aux cas où le type cible ne peut pas être modifié.
⚠️ Ne définissez jamais les deux pour la même paire de types. SiFahrenheita unoperator Celsius()et queCelsiusa un constructeurCelsius(Fahrenheit), le compilateur ne saura pas lequel choisir et signalera une ambiguïté.
Quand un type possède plusieurs opérateurs de conversion, le compilateur peut se retrouver face à des ambiguïtés. La règle du C++ est que la résolution de surcharge choisit la conversion la moins coûteuse, mais quand plusieurs conversions sont de même rang, c'est une erreur :
class Valeur {
double d_;
public:
explicit Valeur(double d) : d_{d} {}
operator double() const { return d_; }
operator int() const { return static_cast<int>(d_); }
};
void traiter(long x) { std::println("long: {}", x); }
Valeur v{3.14};
// traiter(v); // ❌ Ambiguïté : Valeur → double → long ou Valeur → int → long ?
traiter(static_cast<int>(v)); // ✅ Désambiguïsation explicite
traiter(static_cast<double>(v)); // ✅ Désambiguïsation explicite Rendre les conversions explicit élimine ce problème car aucune conversion implicite n'est tentée :
class Valeur {
double d_;
public:
explicit operator double() const { return d_; }
explicit operator int() const { return static_cast<int>(d_); }
};C'est un argument supplémentaire en faveur de l'utilisation systématique de explicit.
Les opérateurs de conversion ne sont pas limités aux types primitifs. On peut convertir vers n'importe quel type, y compris des types de la bibliothèque standard :
#include <string>
#include <string_view>
class Identifiant {
std::string valeur_;
public:
explicit Identifiant(std::string val) : valeur_{std::move(val)} {}
// Conversion vers std::string_view (légère, pas de copie)
operator std::string_view() const noexcept {
return valeur_;
}
// Conversion explicite vers std::string (copie)
explicit operator std::string() const {
return valeur_;
}
};L'asymétrie est intentionnelle ici : la conversion vers std::string_view est implicite car elle est légère (pas de copie, juste une vue) et souvent utilisée dans des contextes où une string_view est attendue. La conversion vers std::string est explicit car elle implique une copie — le développeur doit en être conscient.
Identifiant id{"ABC-123"};
std::string_view sv = id; // ✅ Implicite — léger
void afficher(std::string_view s);
afficher(id); // ✅ Implicite
std::string s1 = id; // ❌ Erreur — explicit
std::string s2 = static_cast<std::string>(id); // ✅ Cast explicite
⚠️ Attention avec la conversion implicite versstd::string_view: si l'objetIdentifiantest détruit ou déplacé, lastring_viewdevient un dangling reference. Ce risque est inhérent à toute vue non-owning et doit être documenté.
Certains types se comportent comme des wrappers autour d'un pointeur ou d'une ressource. Plutôt qu'un opérateur de conversion vers un pointeur brut (dangereux), on surcharge les opérateurs de déréférencement :
template <typename T>
class OptionalRef {
T* ptr_;
public:
OptionalRef() : ptr_{nullptr} {}
explicit OptionalRef(T& ref) : ptr_{&ref} {}
explicit operator bool() const noexcept {
return ptr_ != nullptr;
}
T& operator*() const {
return *ptr_;
}
T* operator->() const {
return ptr_;
}
};Ce pattern est celui utilisé par std::optional, std::unique_ptr et std::shared_ptr. Il combine explicit operator bool() pour le test de validité avec operator*/operator-> pour l'accès — sans jamais exposer de conversion implicite vers un pointeur brut.
std::string texte = "Hello";
OptionalRef<std::string> ref{texte};
if (ref) { // explicit operator bool
std::println("{}", ref->size()); // operator->
std::println("{}", *ref); // operator*
}class Temperature {
public:
operator double() const { return celsius_; } // ❌ Implicite — dangereux
};Cela permet Temperature t; int x = t;, t + 42, t < "hello" (si une conversion chaînée existe). Utilisez explicit ou fournissez un accesseur nommé (double en_celsius() const).
class Flexible {
public:
operator int() const;
operator double() const;
operator std::string() const; // ❌ Trop de conversions implicites
};Chaque conversion implicite multiplie les risques d'ambiguïté et de surprises. Rendez-les toutes explicit ou remplacez-les par des méthodes nommées (to_int(), to_double(), to_string()).
class A {
public:
A(B const&); // B → A implicite
};
class B {
public:
operator A() const; // B → A implicite (doublon !)
};Deux chemins de conversion pour la même paire de types. Le compilateur ne saura pas lequel choisir — ambiguïté garantie. Choisissez un seul mécanisme et marquez l'autre explicit ou supprimez-le.
Marquez vos opérateurs de conversion explicit par défaut. La conversion implicite est l'exception, pas la règle. Commencez toujours par explicit et ne retirez le mot-clé que si vous avez une justification solide et que les conséquences ont été analysées.
operator bool() est toujours explicit. C'est la convention universelle depuis C++11, suivie par toute la bibliothèque standard. Ne dérogez jamais à cette règle.
Préférez les méthodes nommées aux opérateurs de conversion. to_string(), to_double(), as_span() sont plus lisibles, n'interfèrent pas avec la résolution de surcharge, et expriment clairement l'intention. Réservez les opérateurs de conversion aux cas où l'intégration syntaxique est essentielle (wrapping de pointeurs, types "testables").
Ne définissez pas de conversion implicite vers un pointeur brut. C'est une source de bugs mémoire. Utilisez operator*/operator-> pour les types qui encapsulent des pointeurs, et explicit operator bool() pour le test de nullité.
Limitez le nombre de conversions par type. Un type avec une seule conversion bien choisie est prévisible. Un type avec trois conversions implicites est un piège pour la résolution de surcharge.
Documentez les conversions non-évidentes. Si votre type a un operator std::string_view() implicite, documentez le risque de dangling reference. Si votre explicit operator double() retourne une valeur normalisée plutôt que la valeur brute interne, documentez cette sémantique.
| Aspect | Conversion implicite | Conversion explicit |
|---|---|---|
| Déclenchement | Automatique par le compilateur | Uniquement sur cast explicite (+ contextes booléens pour bool) |
| Risques | Ambiguïtés, conversions chaînées non intentionnelles, surprises | Aucun — le développeur contrôle chaque conversion |
| Cas d'usage | Très rares (vues légères vers string_view par ex.) |
Règle par défaut pour tout opérateur de conversion |
operator bool() |
❌ Jamais | ✅ Toujours |
| Mécanisme | Défini dans | Syntaxe | Contrôle |
|---|---|---|---|
| Constructeur de conversion | Type cible | Cible(Source const&) |
Le type construit contrôle |
| Opérateur de conversion | Type source | operator Cible() const |
Le type converti contrôle |