Vai al contenuto

Overloading degli Operatori

Impara come sovraccaricare correttamente gli operatori — membro vs non membro, forme canoniche, operatori di stream, l'operatore spaceship del C++20 e le insidie più comuni.

La Filosofia dell'Overloading degli Operatori

Il C++ ti permette di definire come funzionano gli operatori (+, ==, <<, [], ecc.) per i tuoi tipi. La regola d'oro: fai quello che fanno gli int. Gli operatori sovraccaricati dovrebbero comportarsi come gli utenti si aspettano dai tipi built-in. Se a + b modifica a, hai tradito le aspettative di chi usa il tuo codice.

L'overloading degli operatori è ciò che rende naturali tipi come std::string, std::vector e std::complex. Ma usato male, produce codice confuso e impossibile da mantenere. Non sovraccaricare mai gli operatori per fare qualcosa di sorprendente.

Operatori Membro vs Non Membro

Alcuni operatori devono essere membri (=, [], ->, ()). Altri stanno meglio come non membri (soprattutto gli operatori binari come + e ==) per permettere conversioni simmetriche. Il pattern canonico: implementa += come membro, poi + come non membro usando +=.

operators.cpp
#include <iostream>

class Vec2 {
    double x_, y_;

public:
    Vec2(double x = 0, double y = 0) : x_{x}, y_{y} {}

    // += is a member: modifies *this
    Vec2& operator+=(const Vec2& rhs) {
        x_ += rhs.x_;
        y_ += rhs.y_;
        return *this;
    }

    // Unary minus (member)
    Vec2 operator-() const {
        return Vec2{-x_, -y_};
    }

    // Accessors for non-member operators
    double x() const { return x_; }
    double y() const { return y_; }

    // [] operator (must be member)
    double& operator[](int i) {
        return i == 0 ? x_ : y_;
    }
    const double& operator[](int i) const {
        return i == 0 ? x_ : y_;
    }
};

// + is a non-member using +=  (canonical form)
Vec2 operator+(Vec2 lhs, const Vec2& rhs) {  // lhs by value!
    lhs += rhs;
    return lhs;
}

// == as non-member (symmetric: allows conversions on both sides)
bool operator==(const Vec2& a, const Vec2& b) {
    return a.x() == b.x() && a.y() == b.y();
}

// Stream output operator (must be non-member)
std::ostream& operator<<(std::ostream& os, const Vec2& v) {
    return os << '(' << v.x() << ", " << v.y() << ')';
}

int main() {
    Vec2 a{1.0, 2.0}, b{3.0, 4.0};
    Vec2 c = a + b;           // uses non-member operator+
    std::cout << c << '\n';   // (4, 6)
    std::cout << -c << '\n';  // (-4, -6)
    std::cout << c[0] << '\n'; // 4
    std::cout << std::boolalpha << (a == b) << '\n'; // false
    return 0;
}

L'Operatore Spaceship del C++20 (<=>)

Il C++20 introduce l'operatore di confronto a tre vie <=> (l'"operatore spaceship"). Una singola definizione di operator<=> genera automaticamente tutti e sei gli operatori di confronto (<, >, <=, >=, ==, !=). Usa = default quando il confronto membro per membro è ciò che desideri.

spaceship.cpp
#include <iostream>
#include <compare>
#include <string>

class Version {
    int major_;
    int minor_;
    int patch_;

public:
    Version(int major, int minor, int patch)
        : major_{major}, minor_{minor}, patch_{patch} {}

    // Defaulted <=>: compares members in declaration order
    auto operator<=>(const Version&) const = default;

    friend std::ostream& operator<<(std::ostream& os, const Version& v) {
        return os << v.major_ << '.' << v.minor_ << '.' << v.patch_;
    }
};

class Student {
    std::string name_;
    double gpa_;

public:
    Student(const std::string& name, double gpa)
        : name_{name}, gpa_{gpa} {}

    // Custom <=>: compare only by GPA
    auto operator<=>(const Student& other) const {
        return gpa_ <=> other.gpa_;
    }

    // Need separate == if <=> is not defaulted
    bool operator==(const Student& other) const {
        return gpa_ == other.gpa_;
    }

    friend std::ostream& operator<<(std::ostream& os, const Student& s) {
        return os << s.name_ << " (GPA: " << s.gpa_ << ')';
    }
};

int main() {
    Version v1{2, 0, 0}, v2{1, 9, 5};
    std::cout << v1 << " > " << v2 << "? "
              << std::boolalpha << (v1 > v2) << '\n';  // true
    std::cout << (v1 == v2) << '\n';  // false

    Student alice{"Alice", 3.9};
    Student bob{"Bob", 3.7};
    std::cout << (alice > bob) << '\n';  // true (higher GPA)
    return 0;
}

operator() — Oggetti Funzione

Sovraccaricare operator() crea un funtore (oggetto funzione). I funtori possono contenere stato, il che li rende più flessibili dei semplici puntatori a funzione. Sono usati moltissimo con gli algoritmi della STL.

functor.cpp
#include <iostream>
#include <vector>
#include <algorithm>

// A functor that counts how many times it's called
class Counter {
    int count_ = 0;
    int threshold_;

public:
    explicit Counter(int threshold) : threshold_{threshold} {}

    bool operator()(int value) {
        ++count_;
        return value > threshold_;
    }

    int calls() const { return count_; }
};

int main() {
    std::vector<int> nums{1, 5, 3, 8, 2, 9, 4, 7, 6};

    Counter above_five{5};
    auto count = std::count_if(nums.begin(), nums.end(), above_five);
    std::cout << count << " elements above 5\n";  // 4

    // Functors can also be used directly
    Counter is_positive{0};
    std::cout << std::boolalpha;
    std::cout << is_positive(42) << '\n';   // true
    std::cout << is_positive(-1) << '\n';   // false
    std::cout << "Calls: " << is_positive.calls() << '\n';  // 2

    return 0;
}
Insidia

Non sovraccaricare gli operatori per fargli fare cose sorprendenti. operator+ deve sommare, non sottrarre. operator* su una Matrix dovrebbe eseguire la moltiplicazione tra matrici, non quella elemento per elemento (a meno che non sia ciò che il tuo dominio si aspetta). Alcuni operatori non possono essere sovraccaricati: ::, ., .*, ?: e sizeof. Inoltre, sovraccaricare && e || fa perdere la valutazione a corto circuito — non è quasi mai una buona idea.

Buona pratica

Segui le forme canoniche: implementa gli assegnamenti composti (+=, -=) come membri, poi definisci gli operatori binari (+, -) come non membri usando la versione composta. Implementa sempre operator== se implementi operator< (o meglio, usa <=> del C++20 per ottenerli tutti e sei). Implementa gli operatori di stream (<<, >>) come friend non membri. Usa explicit sugli operatori di conversione per impedire conversioni implicite accidentali.

Concetti chiave
  • Segui il principio "fai quello che fanno gli int" — gli operatori devono comportarsi come gli utenti si aspettano
  • Implementa l'assegnamento composto (+=) come membro, poi + come non membro usando +=
  • L'operator<=> del C++20 genera tutti e sei gli operatori di confronto da una sola definizione
  • Sovraccaricare operator() crea i funtori — oggetti che si possono chiamare come funzioni
  • && e || perdono la valutazione a corto circuito se sovraccaricati — evita di sovraccaricarli
  • Alcuni operatori non possono essere sovraccaricati: ::, ., .*, ?:, sizeof

Quiz — Metti alla prova le tue conoscenze

(15 XP)

1. Perché `operator+` viene tipicamente implementato come funzione non membro?

2. Cosa fa `auto operator<=>(const T&) const = default;` in C++20?

3. Perché dovresti evitare di sovraccaricare `operator&&` e `operator||`?