Aller au contenu

Opérateurs, expressions et conversions

Maîtrisez la priorité des opérateurs, les conversions arithmétiques, les dangers du mélange signé/non signé, et les quatre casts nommés du C++.

Les expressions : les briques de base du calcul

Une expression en C++ est une séquence d'opérateurs et d'opérandes qui calcule une valeur. Même une instruction simple comme x = a + b; contient plusieurs sous-expressions : a, b, a + b, et x = a + b (l'assignation elle-même est une expression qui retourne la valeur assignée).

Chaque expression possède deux propriétés : un type et une catégorie de valeur (lvalue, rvalue, etc.). Comprendre ces propriétés est essentiel car le C++ applique des conversions implicites entre les types, et les règles de ces conversions peuvent produire des résultats surprenants si vous n'êtes pas vigilant.

La priorité des opérateurs détermine comment les opérateurs sont regroupés (lequel lie plus fort), tandis que l'associativité détermine la direction du regroupement pour les opérateurs au même niveau de priorité. Se tromper dans ces règles mène à des bogues subtils qui compilent sans avertissement.

Surprises de priorité des opérateurs

La priorité des opérateurs est l'une des sources les plus courantes de bogues. En cas de doute, utilisez des parenthèses pour rendre votre intention explicite :

precedence.cppc++23
#include <iostream>

int main() {
    // Precedence trap #1: bitwise vs comparison
    int x = 5;
    // This does NOT check if bit 1 is set:
    if (x & 1 == 0) {              // Parsed as: x & (1 == 0) → x & 0 → 0
        std::cout << "Never reached!\n";
    }
    // Correct:
    if ((x & 1) == 0) {            // Parentheses fix the grouping
        std::cout << "Even\n";
    }

    // Precedence trap #2: ternary vs assignment
    int a = 1, b = 2;
    int result = a > b ? a : b = 10;  // Parsed as: (a > b ? a : b) = 10 — ERROR!
    // Correct:
    int correct = (a > b) ? a : b;    // Parenthesize for clarity

    // Precedence trap #3: pointer dereference vs member access
    // (*ptr).member  vs  ptr->member  — both are equivalent
    // *ptr.member    is parsed as *(ptr.member) — usually a compile error

    // Short-circuit evaluation
    int* ptr = nullptr;
    // Safe: the second operand is NOT evaluated if the first is false
    if (ptr != nullptr && *ptr > 0) {
        std::cout << "Value: " << *ptr << '\n';
    }
    // The && and || operators guarantee left-to-right evaluation and short-circuit
    // Overloaded && and || do NOT short-circuit — avoid overloading them
}
Sortie

Cliquez sur Exécuter pour compiler et lancer cet exemple.

Conversions arithmétiques et promotion d'entiers

Lorsque vous mélangez des types dans une expression, le C++ applique des conversions implicites suivant un ensemble de règles appelé les conversions arithmétiques habituelles :

1. Promotion d'entiers : Les types plus petits que int (char, short, bool) sont d'abord promus en int (ou unsigned int si la valeur ne tient pas dans un int). Cela signifie que char + char produit un int, pas un char.

2. Équilibrage : Si les opérandes diffèrent encore en type après la promotion, le type « plus petit » est converti vers le « plus grand » selon la hiérarchie : int → unsigned int → long → unsigned long → long long → unsigned long long → float → double → long double.

3. Interaction signé/non signé : Quand un type signé rencontre un type non signé de même rang, la valeur signée est convertie en non signé. C'est là que se produisent les conversions implicites les plus dangereuses.

Piège

Mélanger des entiers signés et non signés est l'un des pièges les plus courants et dangereux en C++. Lorsqu'une valeur signée négative est implicitement convertie en non signé, elle s'enroule vers un très grand nombre positif (puisque les types non signés utilisent l'arithmétique modulaire).

Cela se produit fréquemment lors de la comparaison de valeurs int avec size_t (qui est unsigned) — par exemple, en itérant sur un std::vector avec un compteur de boucle int et en le comparant avec vector.size().

Compilez avec -Wsign-compare (inclus dans -Wall) pour détecter ces problèmes.

  • Un int négatif converti en non signé devient un très grand nombre (par ex., -1 devient 4294967295 pour du 32 bits)
  • Comparer des valeurs signées et non signées convertit d'abord la valeur signée en non signé
  • vector.size() retourne size_t (non signé) — la comparaison avec un int négatif donne des résultats erronés
  • Utilisez -Wsign-compare pour détecter les problèmes de comparaison signé/non signé à la compilation

Casts nommés C++ vs casts de style C

Le C++ fournit quatre opérateurs de cast nommés qui remplacent le dangereux cast de style C (Type)expr. Chacun a un objectif spécifique et différents niveaux de sécurité :

casts.cppc++23
#include <iostream>
#include <cstdint>

class Base {
public:
    virtual ~Base() = default;
    virtual void speak() { std::cout << "Base\n"; }
};

class Derived : public Base {
public:
    void speak() override { std::cout << "Derived\n"; }
    void only_in_derived() { std::cout << "Special!\n"; }
};

int main() {
    // static_cast — compile-time checked, for "natural" conversions
    double pi = 3.14159;
    int truncated = static_cast<int>(pi);    // Explicit: you acknowledge the truncation
    std::cout << truncated << '\n';          // 3

    // dynamic_cast — runtime-checked, for safe downcasting in class hierarchies
    Base* base_ptr = new Derived();
    Derived* derived_ptr = dynamic_cast<Derived*>(base_ptr);
    if (derived_ptr) {                       // Check for success!
        derived_ptr->only_in_derived();       // Safe
    }

    // const_cast — adds or removes const (use sparingly!)
    const int ci = 42;
    // int& mutable_ref = const_cast<int&>(ci);  // Legal but modifying ci is UB!
    // Only valid use: calling a non-const API with a const object you know won't be modified

    // reinterpret_cast — reinterprets the bit pattern (dangerous!)
    std::int64_t address = reinterpret_cast<std::int64_t>(base_ptr);
    std::cout << "Address: " << address << '\n';

    // C-style cast: (int)pi — NEVER use this in C++
    // It tries static_cast, then const_cast, then reinterpret_cast — 
    // you don't know which one was applied!

    delete base_ptr;
}
Sortie

Cliquez sur Exécuter pour compiler et lancer cet exemple.

Bonne pratique

Utilisez les casts nommés pour communiquer votre intention et obtenir une vérification appropriée du compilateur :

- static_cast — Pour les conversions bien définies : conversions de types numériques, upcasts dans les hiérarchies de classes, et void* vers un pointeur typé. C'est le cast que vous utiliserez le plus souvent.
- dynamic_cast — Pour un downcast sûr dans les hiérarchies polymorphiques. Nécessite au moins une fonction virtuelle dans la classe de base. Retourne nullptr (pour les pointeurs) ou lance std::bad_cast (pour les références) en cas d'échec. A un coût à l'exécution.
- const_cast — Uniquement pour s'interfacer avec des API anciennes qui ne sont pas const-correctes. Ne l'utilisez jamais pour modifier un objet véritablement const.
- reinterpret_cast — Pour la réinterprétation bas niveau des bits. Indique presque toujours du code spécifique à la plateforme. Rarement nécessaire dans le code applicatif.

N'utilisez jamais les casts de style C (Type)expr en C++ — ils masquent quelle conversion est effectuée et contournent les vérifications de sécurité.

  • Utilisez static_cast pour les conversions numériques et les upcasts sûrs — il est vérifié à la compilation
  • Utilisez dynamic_cast pour les downcasts sûrs dans les hiérarchies polymorphiques — il est vérifié à l'exécution
  • Utilisez const_cast uniquement pour s'interfacer avec des API anciennes non const-correctes
  • N'utilisez jamais les casts de style C en C++ — ils masquent quelle conversion est appliquée
Points clés
  • La priorité des opérateurs détermine le regroupement — en cas de doute, utilisez des parenthèses pour rendre votre intention explicite
  • L'évaluation en court-circuit dans && et || garantit l'ordre gauche-droite et saute l'évaluation inutile
  • La promotion d'entiers convertit les petits types (char, short, bool) en int avant l'arithmétique
  • Mélanger des types signés et non signés convertit la valeur signée en non signé — un int négatif devient un énorme nombre positif
  • Utilisez les quatre casts nommés (static_cast, dynamic_cast, const_cast, reinterpret_cast) au lieu des casts de style C
  • Les casts de style C sont dangereux car ils essaient silencieusement plusieurs types de cast sans vous indiquer lequel a été appliqué

Quiz — Testez vos connaissances

(15 XP)

1. Comment l'expression `x & 1 == 0` est-elle réellement évaluée ?

2. Que se passe-t-il quand vous comparez `-1 < 0u` (où `0u` est non signé) ?

3. Quel cast C++ devriez-vous utiliser pour un downcast sûr dans une hiérarchie de classes polymorphique ?