Aller au contenu

Surcharge d'opérateurs

Apprenez à surcharger correctement les opérateurs — membre vs non-membre, formes canoniques, opérateurs de flux, l'opérateur spaceship C++20 et les pièges courants.

Philosophie de la surcharge d'opérateurs

Le C++ vous permet de définir comment les opérateurs (+, ==, <<, [], etc.) fonctionnent pour vos propres types. La règle d'or : faites comme les ints. Les opérateurs surchargés devraient se comporter comme les utilisateurs l'attendent des types intégrés. Si a + b modifie a, vous avez violé les attentes des utilisateurs.

La surcharge d'opérateurs est ce qui rend les types comme std::string, std::vector et std::complex naturels à utiliser. Mais mal utilisée, elle crée du code confus et inmaintenable. Ne surchargez jamais les opérateurs pour faire quelque chose de surprenant.

Opérateurs membres vs non-membres

Certains opérateurs doivent être membres (=, [], ->, ()). D'autres sont mieux en tant que non-membres (en particulier les opérateurs binaires comme + et ==) pour permettre des conversions symétriques. Le patron canonique : implémentez += comme membre, puis + comme non-membre utilisant +=.

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'opérateur spaceship C++20 (<=>)

C++20 introduit l'opérateur de comparaison trilatérale <=> (l'« opérateur spaceship »). Une seule définition de operator<=> génère automatiquement les six opérateurs de comparaison (<, >, <=, >=, ==, !=). Utilisez = default quand la comparaison membre par membre est ce que vous voulez.

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() — Objets fonctions

Surcharger operator() crée un foncteur (objet fonction). Les foncteurs peuvent contenir un état, les rendant plus flexibles que les pointeurs de fonctions simples. Ils sont utilisés intensivement avec les algorithmes 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;
}
Piège

Ne surchargez pas les opérateurs pour faire des choses surprenantes. operator+ devrait additionner, pas soustraire. operator* sur une Matrix devrait faire la multiplication matricielle, pas la multiplication élément par élément (sauf si c'est ce que votre domaine attend). Certains opérateurs ne peuvent pas être surchargés : ::, ., .*, ?: et sizeof. De plus, surcharger && et || perd l'évaluation en court-circuit — presque jamais une bonne idée.

Bonne pratique

Suivez les formes canoniques : implémentez l'assignation composée (+=, -=) comme membres, puis définissez les opérateurs binaires (+, -) comme non-membres utilisant la version composée. Implémentez toujours operator== si vous implémentez operator< (ou mieux, utilisez <=> de C++20 pour obtenir les six). Implémentez les opérateurs de flux (<<, >>) comme des amies non-membres. Utilisez explicit sur les opérateurs de conversion pour empêcher les conversions implicites accidentelles.

Points clés
  • Suivez le principe « faites comme les ints » — les opérateurs devraient se comporter comme les utilisateurs l'attendent
  • Implémentez l'assignation composée (+=) comme membre, puis + comme non-membre utilisant +=
  • L'operator<=> de C++20 génère les six opérateurs de comparaison à partir d'une seule définition
  • Surcharger operator() crée des foncteurs — des objets qui peuvent être appelés comme des fonctions
  • &&, || perdent l'évaluation en court-circuit quand ils sont surchargés — évitez de les surcharger
  • Certains opérateurs ne peuvent pas être surchargés : ::, ., .*, ?:, sizeof

Quiz — Testez vos connaissances

(15 XP)

1. Pourquoi `operator+` est-il typiquement implémenté comme une fonction non-membre ?

2. Que fait `auto operator<=>(const T&) const = default;` en C++20 ?

3. Pourquoi devriez-vous éviter de surcharger `operator&&` et `operator||` ?