🔝 Retour au Sommaire
Chapitre 3 — Types, Variables et Opérateurs · Section 3 sur 5
Prérequis : 3.2 — Types primitifs, tailles et représentation mémoire
Dans un langage à typage statique fort, les opérations entre types différents ne vont pas de soi. Que se passe-t-il quand on additionne un int et un double ? Quand on affecte un double à un int ? Quand on passe un pointeur vers une classe dérivée là où un pointeur vers la classe de base est attendu ?
Dans chacun de ces cas, une conversion de type intervient — le passage d'une valeur d'un type vers un autre. En C++, ces conversions prennent deux formes radicalement différentes par leur niveau de contrôle et de sécurité : les conversions implicites, décidées par le compilateur sans intervention du programmeur, et les conversions explicites (casts), demandées volontairement par le programmeur.
Comprendre la distinction entre ces deux mécanismes est essentiel pour écrire du code correct, sûr et maintenable. Cette section présente les principes généraux des conversions en C++, explique pourquoi les casts C-style sont dangereux, et introduit les quatre opérateurs de cast du C++ moderne qui les remplacent.
Le compilateur C++ effectue automatiquement certaines conversions lorsque les types ne correspondent pas exactement. Ces conversions sont dites implicites parce qu'elles ne nécessitent aucune syntaxe particulière de la part du programmeur — elles se produisent « en coulisses ».
Les promotions sont des conversions implicites qui élargissent un type vers un type plus grand sans perte d'information :
short s = 10;
int i = s; // Promotion : short → int (aucune perte)
int x = 42;
double d = x; // Promotion : int → double (aucune perte)
float f = 1.5f;
double d2 = f; // Promotion : float → double (aucune perte) Ces conversions sont toujours sûres : le type cible est capable de représenter toutes les valeurs du type source. Le compilateur les effectue sans émettre le moindre avertissement.
Lorsque deux opérandes de types différents apparaissent dans une expression, le compilateur convertit l'opérande de type « inférieur » vers le type « supérieur » selon une hiérarchie définie (vue en section 3.2.1). C'est ce qui permet d'écrire des expressions mixtes :
int count = 7;
double average = count / 2.0; // count est converti en double avant la division
// Résultat : 3.5 (division flottante)Sans cette conversion implicite, il faudrait écrire static_cast<double>(count) / 2.0 — ce qui serait correct mais inutilement verbeux.
Certaines conversions implicites sont autorisées par le compilateur mais comportent un risque de perte d'information. Ce sont les conversions dites narrowing :
double pi = 3.14159;
int truncated = pi; // ⚠️ Perte de la partie fractionnaire → 3
int64_t big = 5'000'000'000;
int32_t small = big; // ⚠️ Troncation des bits de poids fort
int negative = -1;
unsigned int u = negative; // ⚠️ Réinterprétation du signe → 4'294'967'295 Le compilateur émet un avertissement pour ces conversions (avec -Wall -Wextra), mais il les accepte. C'est l'un des héritages du C que le C++ moderne cherche à corriger — l'initialisation par accolades interdit ces conversions :
int safe{3.14}; // ❌ Erreur de compilation : narrowing conversion
int unsafe = 3.14; // ⚠️ Compile avec un warning seulement Tout type arithmétique ou pointeur peut être implicitement converti en bool. La règle est simple : zéro (ou nullptr pour les pointeurs) donne false, toute autre valeur donne true :
int x = 42;
if (x) { /* s'exécute : x != 0 → true */ }
int* ptr = nullptr;
if (ptr) { /* ne s'exécute pas : nullptr → false */ } La conversion inverse — de bool vers un type arithmétique — est également implicite : true devient 1 et false devient 0.
Un pointeur (ou une référence) vers une classe dérivée peut être implicitement converti vers un pointeur (ou une référence) vers sa classe de base. C'est le mécanisme fondamental du polymorphisme :
class Animal { /* ... */ };
class Dog : public Animal { /* ... */ };
Dog rex;
Animal* animal_ptr = &rex; // Conversion implicite : Dog* → Animal*
Animal& animal_ref = rex; // Conversion implicite : Dog& → Animal& Cette conversion est sûre car un Dog est un Animal (relation « est-un »). La conversion inverse (de base vers dérivée) n'est pas implicite — elle nécessite un cast explicite, car elle peut échouer. Nous verrons cela avec dynamic_cast en section 3.3.4.
Le C dispose d'une syntaxe unique pour toutes les conversions explicites : le cast C-style, qui consiste à placer le type cible entre parenthèses devant l'expression :
double pi = 3.14159;
int n = (int)pi; // Cast C-style : supprime la partie fractionnaire
void* raw = malloc(100);
int* data = (int*)raw; // Cast C-style : convertit void* en int* Cette syntaxe est toujours valide en C++, mais elle est fortement déconseillée pour plusieurs raisons :
Elle est trop permissive. Un cast C-style essaie successivement const_cast, static_cast, static_cast suivi de const_cast, reinterpret_cast, et reinterpret_cast suivi de const_cast — dans cet ordre — et utilise le premier qui fonctionne. Cela signifie qu'un cast C-style peut effectuer une réinterprétation mémoire dangereuse là où le programmeur pensait faire une simple conversion arithmétique, sans aucun avertissement.
Elle est invisible dans le code. La syntaxe (int)x se fond dans le code et est difficile à repérer lors d'une revue ou d'une recherche textuelle. Si un bug est lié à une conversion, identifier tous les casts dans une base de code est une opération laborieuse.
Elle ne distingue pas les intentions. Convertir un double en int (opération arithmétique courante) et convertir un void* en int* (opération dangereuse sur la mémoire brute) utilisent exactement la même syntaxe. Le lecteur du code ne peut pas savoir, en lisant un cast C-style, quel degré de risque il porte.
Le C++ offre en remplacement un système de casts fonctionnels — quatre variantes nommées, chacune avec une syntaxe distincte, un domaine d'application précis et un niveau de risque clairement identifié.
Le C++ définit quatre opérateurs de cast explicite. Chacun porte un nom qui exprime son intention et ses limites :
C'est le cast le plus courant et le plus sûr. Il effectue des conversions vérifiables à la compilation : conversions numériques, conversions dans les hiérarchies de classes (vers le haut et vers le bas), conversions de void* vers un type pointeur, et conversions vers/depuis enum. Il refuse les conversions manifestement incohérentes.
double pi = 3.14159;
int n = static_cast<int>(pi); // Conversion numérique explicite C'est le cast le plus dangereux. Il réinterprète les bits d'une valeur comme un autre type, sans aucune vérification de cohérence. Il est utilisé pour les conversions de pointeurs entre types non liés, l'interaction avec des API C bas niveau, et certains cas de programmation système.
int x = 42;
char* bytes = reinterpret_cast<char*>(&x); // Accès aux octets bruts de x C'est le seul cast capable d'ajouter ou de retirer les qualificateurs const et volatile. Il est utilisé dans des situations très spécifiques, principalement pour interfacer du code C++ avec des API C qui ne respectent pas la const-correctness.
const char* msg = "hello";
char* mutable_msg = const_cast<char*>(msg); // Retire const
// ⚠️ Modifier *mutable_msg est un comportement indéfini si l'objet original est vraiment constC'est le seul cast qui effectue une vérification à l'exécution. Il est utilisé pour les conversions vers le bas (downcast) dans les hiérarchies polymorphiques (classes avec des fonctions virtuelles). Si la conversion est invalide, il retourne nullptr (pour les pointeurs) ou lève une exception std::bad_cast (pour les références).
Animal* animal = get_animal();
Dog* dog = dynamic_cast<Dog*>(animal); // nullptr si animal n'est pas un Dog La multiplication des casts en C++ n'est pas un caprice de conception — c'est une stratégie délibérée de séparation des responsabilités. Chaque cast exprime une intention différente et porte un niveau de risque différent :
| Cast | Vérifié à la compilation | Vérifié à l'exécution | Niveau de risque |
|---|---|---|---|
static_cast |
Oui | Non | Faible |
dynamic_cast |
Oui | Oui | Faible (avec coût runtime) |
const_cast |
Oui | Non | Moyen |
reinterpret_cast |
Minimal | Non | Élevé |
Cette hiérarchie guide le développeur vers le cast le moins dangereux possible :
- Essayez d'abord de vous en passer — les conversions implicites suffisent souvent.
- Si un cast est nécessaire, utilisez
static_cast— c'est le bon choix dans 90% des cas. - Si vous travaillez avec du polymorphisme et que vous avez besoin de vérifier le type à l'exécution, utilisez
dynamic_cast. - Si vous interfacez avec une API C qui ne respecte pas
const, utilisezconst_cast. - N'utilisez
reinterpret_castqu'en dernier recours, pour de la manipulation mémoire brute.
Si vous trouvez un reinterpret_cast dans du code, il devrait attirer votre attention immédiatement : c'est une opération dangereuse qui mérite un commentaire expliquant pourquoi elle est nécessaire.
Il existe une cinquième syntaxe de conversion, parfois appelée cast fonctionnel :
double pi = 3.14159;
int n = int(pi); // Syntaxe fonctionnelle : équivalent à (int)pi Pour les types primitifs, cette syntaxe est équivalente au cast C-style et en partage les défauts. Elle est toutefois considérée comme acceptable dans un cas précis : l'initialisation explicite d'un type primitif là où l'intention est clairement une conversion arithmétique :
auto ratio = double(numerator) / double(denominator);Pour les types définis par l'utilisateur, la syntaxe fonctionnelle appelle le constructeur — c'est alors une construction, pas un cast :
std::string s = std::string("hello"); // Appel de constructeur, pas un castEn général, préférez static_cast au cast fonctionnel pour les conversions entre types primitifs : l'intention est plus explicite et la syntaxe se distingue clairement de la construction d'objets.
L'un des avantages pratiques des casts nommés C++ est leur facilité de recherche. Si vous soupçonnez qu'un bug est lié à une conversion, une simple recherche textuelle suffit :
grep -rn "static_cast" src/
grep -rn "reinterpret_cast" src/
grep -rn "const_cast" src/
grep -rn "dynamic_cast" src/ La même recherche pour un cast C-style — (int), (double*), etc. — est pratiquement impossible sans un outil d'analyse syntaxique, car les parenthèses sont omniprésentes en C++. C'est un argument supplémentaire en faveur de l'abandon des casts C-style.
Les outils d'analyse statique comme clang-tidy (chapitre 32) détectent les casts C-style et proposent automatiquement leur remplacement par le cast nommé approprié (check google-readability-casting ou cppcoreguidelines-pro-type-cstyle-cast).
Les quatre sous-sections suivantes explorent chaque opérateur de cast en profondeur :
-
3.3.1 —
static_cast: Conversions sûres. Le cast par défaut du C++ moderne. Conversions numériques, conversions de pointeurs dans les hiérarchies de classes, conversions vers/depuisenum, et limites du mécanisme. -
3.3.2 —
reinterpret_cast: Réinterprétation mémoire. Quand et pourquoi réinterpréter les bits bruts d'une valeur. Conversions de pointeurs entre types non liés, interaction avec les API C, règles d'aliasing strictes, et dangers. -
3.3.3 —
const_cast: Manipulation deconst. Retirer ou ajouterconst: dans quels cas c'est légal, dans quels cas c'est un comportement indéfini, et comment l'éviter autant que possible. -
3.3.4 —
dynamic_cast: Cast polymorphique. Vérification de type à l'exécution, fonctionnement avec les vtables, coût en performance, et alternatives (static_castquand le type est garanti,std::variantpour le polymorphisme sans héritage).