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 +=.
#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.
#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.
#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;
}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.
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.
- 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||`?