Оператор перегрузки
Узнайте, как правильно перегружать операторов — участник против нечлена, канонические формы, операторы потоков, оператор космического корабля C++20 и распространенные подводные камни.
Оператор перегрузил философию
C++ позволяет определить, как операторы (+, ==, <<, [] и т.д.) работают для ваших собственных типов. Золотое правило: делай то, что делают инты. Перегруженные операторы должны вести себя так, как ожидают пользователи от встроенных типов. Если a + b модифицирует a, вы нарушили ожидания пользователей.
Перегрузка оператора делает такие типы, как std::string, std::vector и std::complex естественными. Но используется плохо, это создает запутанный, неподъемный код. Никогда не перегружайте операторов, чтобы сделать что-то удивительное.
Участник vs. Операторы, не являющиеся членами
Некоторые операторы должны быть членами (=, [], ->, ()). Другие лучше, чем non-members (особенно бинарные операторы, такие как + и ==), чтобы обеспечить симметричные преобразования. Каноническая схема: реализовать += в качестве члена, затем + в качестве нечлена, используя +=.
#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;
}Оператор космического корабля C++20 (<=>)
C++20 представляет собой трехсторонний оператор сравнения ** <=> («оператор космического корабля»). Одно определение operator<=> автоматически генерирует все шесть операторов сравнения (<, >, <=, >=, ==, !=). Используйте = default, когда сравнение по члену - это то, что вам нужно.
#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() создает «функциональный объект». Функторы могут удерживать состояние, делая их более гибкими, чем простые указатели функций. Они широко используются с алгоритмами 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;
}Не перегружайте операторов делать удивительные вещи. operator+ следует добавлять, а не вычитать. operator* на Matrix должен выполнять умножение матрицы, а не умножение по элементам (если это не то, что ожидает ваш домен). Некоторые операторы не могут быть перегружены: ::, ., .*, ?: и sizeof. Кроме того, перегрузка && и || теряет оценку короткого замыкания.
Следуйте каноническим формам: реализуйте назначение соединения (+=, -=) в качестве членов, затем определите двоичных операторов (+, -) в качестве нечленов, используя составную версию. Всегда внедряйте operator==, если вы реализуете operator< (или лучше, используйте C++20 <=>, чтобы получить все шесть). Внедрение потоковых операторов (<<, >>) в качестве друзей-нечленов. Используйте explicit для предотвращения случайных неявных конверсий.
- Следуйте принципу «делай то, что делают инты» — операторы должны вести себя так, как ожидают пользователи.
- Внедрить назначение соединения (
+=) в качестве члена, затем+в качестве нечлена, используя+= - C++20
operator<=>генерирует все шесть операторов сравнения из одного определения. - Перегрузка
operator()создает функторы — объекты, которые можно назвать похожими на функции &&,||при перегрузке теряет оценку короткого замыкания — избегайте их перегрузки- Некоторые операторы не могут быть перегружены:
::,.,.*,?:,sizeof
Тест — проверьте свои знания
(15 XP)1. Почему `operator+` обычно реализуется как функция, не являющаяся членом?
2. Что делает `auto operator<=>(const T&) const = default;` в C++20?
3. Почему стоит избегать перегрузки `operator&&` и `operator||`?