Vai al contenuto

Operatori, Espressioni e Conversioni

Padroneggia la precedenza degli operatori, le conversioni aritmetiche, i pericoli del mescolare signed/unsigned e i quattro cast con nome del C++.

Le Espressioni: I Mattoni del Calcolo

Un'espressione in C++ è una sequenza di operatori e operandi che calcola un valore. Anche un semplice statement come x = a + b; contiene più sotto-espressioni: a, b, a + b e x = a + b (l'assegnamento stesso è un'espressione che restituisce il valore assegnato).

Ogni espressione ha due proprietà: un tipo e una categoria di valore (lvalue, rvalue, ecc.). Comprendere queste proprietà è essenziale perché il C++ applica conversioni implicite tra tipi, e le regole per queste conversioni possono produrre risultati sorprendenti se non si fa attenzione.

La precedenza degli operatori determina come gli operatori vengono raggruppati (quale lega più strettamente), mentre l'associatività determina la direzione del raggruppamento per operatori allo stesso livello di precedenza. Sbagliare porta a bug sottili che compilano senza warning.

Sorprese sulla Precedenza degli Operatori

La precedenza degli operatori è una delle fonti più comuni di bug. In caso di dubbio, usa le parentesi per rendere esplicito il tuo intento:

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
}
Output

Premi Esegui per compilare ed eseguire questo esempio.

Conversioni Aritmetiche e Promozione Intera

Quando mescoli tipi in un'espressione, il C++ applica conversioni implicite seguendo un insieme di regole chiamate conversioni aritmetiche usuali:

1. Promozione intera: i tipi più piccoli di int (char, short, bool) vengono prima promossi a int (o unsigned int se il valore non può stare in un int). Questo significa che char + char produce un int, non un char.

2. Bilanciamento: se gli operandi differiscono ancora di tipo dopo la promozione, il tipo "più piccolo" viene convertito in quello "più grande" seguendo una gerarchia: int → unsigned int → long → unsigned long → long long → unsigned long long → float → double → long double.

3. Interazione signed/unsigned: quando un tipo con segno incontra un tipo senza segno dello stesso rango, il valore con segno viene convertito in unsigned. Qui si verificano le conversioni implicite più pericolose.

Insidia

Mescolare interi con segno e senza segno è una delle insidie più comuni e pericolose del C++. Quando un valore negativo con segno viene implicitamente convertito in unsigned, va a capo a un numero positivo molto grande (poiché i tipi unsigned usano l'aritmetica modulare).

Questo accade frequentemente quando si confrontano valori int con size_t (che è unsigned) — per esempio, iterando su un std::vector con un contatore del ciclo int e confrontandolo con vector.size().

Compila con -Wsign-compare (incluso in -Wall) per catturare questi problemi.

  • Un int negativo convertito in unsigned diventa un numero molto grande (es. -1 diventa 4294967295 per 32 bit)
  • Confrontare valori signed e unsigned converte prima il valore signed in unsigned
  • vector.size() restituisce size_t (unsigned) — confrontare con un int negativo dà risultati errati
  • Usa -Wsign-compare per catturare i problemi di confronto signed/unsigned a tempo di compilazione

Cast con Nome del C++ vs Cast in Stile C

Il C++ fornisce quattro operatori di cast con nome che sostituiscono il pericoloso cast in stile C (Type)expr. Ciascuno ha uno scopo specifico e diversi livelli di sicurezza:

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;
}
Output

Premi Esegui per compilare ed eseguire questo esempio.

Buona pratica

Usa i cast con nome per comunicare l'intento e ottenere i controlli appropriati del compilatore:

- static_cast — Per conversioni ben definite: conversioni numeriche di tipo, upcast nelle gerarchie di classi e da void* a puntatore tipizzato. Questo è il cast che userai più spesso.
- dynamic_cast — Per il downcast sicuro nelle gerarchie polimorfiche. Richiede almeno una funzione virtuale nella classe base. Restituisce nullptr (per i puntatori) o lancia std::bad_cast (per i riferimenti) in caso di fallimento. Ha un costo a runtime.
- const_cast — Solo per interfacciarsi con API legacy che non sono const-corrette. Non usarlo mai per modificare un oggetto veramente const.
- reinterpret_cast — Per la reinterpretazione dei bit a basso livello. Quasi sempre indica codice specifico per piattaforma. Raramente necessario nel codice applicativo.

Non usare mai i cast in stile C (Type)expr in C++ — nascondono quale conversione viene eseguita e aggirano i controlli di sicurezza.

  • Usa static_cast per le conversioni numeriche e gli upcast sicuri — viene controllato a tempo di compilazione
  • Usa dynamic_cast per i downcast sicuri nelle gerarchie polimorfiche — viene controllato a runtime
  • Usa const_cast solo per interfacciarti con API legacy non const-corrette
  • Non usare mai i cast in stile C in C++ — nascondono quale conversione viene applicata
Concetti chiave
  • La precedenza degli operatori determina il raggruppamento — in caso di dubbio, usa le parentesi per rendere esplicito l'intento
  • La valutazione in cortocircuito in && e || garantisce l'ordine da sinistra a destra e salta la valutazione non necessaria
  • La promozione intera converte i tipi piccoli (char, short, bool) in int prima dell'aritmetica
  • Mescolare tipi signed e unsigned converte il valore signed in unsigned — un int negativo diventa un numero positivo enorme
  • Usa i quattro cast con nome (static_cast, dynamic_cast, const_cast, reinterpret_cast) invece dei cast in stile C
  • I cast in stile C sono pericolosi perché provano silenziosamente più tipi di cast senza dirti quale è stato applicato

Quiz — Metti alla prova le tue conoscenze

(15 XP)

1. Come viene effettivamente valutata l'espressione `x & 1 == 0`?

2. Cosa succede quando confronti `-1 < 0u` (dove `0u` è unsigned)?

3. Quale cast C++ dovresti usare per un downcast sicuro in una gerarchia di classi polimorfica?