跳至内容

运算符重载

学习如何正确重载运算符 ── 成员与非成员、规范形式、流运算符、C++20 太空船运算符以及常见陷阱。

运算符重载的理念

C++ 允许你定义运算符(+、==、<<、[] 等)对你自己的类型如何工作。黄金法则:像 int 一样做。重载的运算符应该按照用户对内置类型的期望来行为。如果 a + b 修改了 a,你就违反了用户的期望。

运算符重载使得 std::string、std::vector 和 std::complex 等类型使用起来感觉自然。但如果使用不当,它会创建令人困惑、难以维护的代码。永远不要重载运算符做出令人惊讶的事情。

成员与非成员运算符

某些运算符必须是成员(=、[]、->、())。其他运算符作为非成员更好(特别是二元运算符如 + 和 ==),以允许对称转换。规范模式:将 += 实现为成员,然后将 + 实现为使用 += 的非成员。

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

C++20 太空船运算符(<=>)

C++20 引入了三路比较运算符 <=> (「太空船运算符」)。单个 operator<=> 定义自动生成所有六个比较运算符(<、>、<=、>=、==、!=)。当逐成员比较正是你想要的时,使用 = default。

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() ── 函数对象

重载 operator() 会创建一个仿函数(函数对象)。仿函数可以持有状态,使其比普通函数指针更灵活。它们在 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;
}
常见陷阱

不要重载运算符做令人惊讶的事情。operator+ 应该做加法,而不是减法。Matrix 上的 operator* 应该做矩阵乘法,而不是逐元素乘法(除非那是你的领域所期望的)。某些运算符不能被重载:::、.、.*、?: 和 sizeof。此外,重载 && 和 || 会失去短路求值 ── 几乎从来都不是好主意。

最佳实践

遵循规范形式:将复合赋值运算符(+=、-=)实现为成员,然后使用复合版本将二元运算符(+、-)定义为非成员。如果你实现了 operator==,也要实现 operator<(或者更好的做法是使用 C++20 的 <=> 来获得全部六个)。将流运算符(<<、>>)实现为非成员友元。在转换运算符上使用 explicit 以防止意外的隐式转换。

关键要点
  • 遵循「像 int 一样做」原则 ── 运算符应按用户期望的方式行为
  • 将复合赋值(+=)实现为成员,然后将 + 实现为使用 += 的非成员
  • C++20 的 operator<=> 从单个定义生成所有六个比较运算符
  • 重载 operator() 创建仿函数 ── 可以像函数一样调用的对象
  • &&、|| 重载后会失去短路求值 ── 避免重载它们
  • 某些运算符不能被重载:::、.、.*、?:、sizeof

测验 — 检验你的知识

(15 经验值)

1. 为什么 `operator+` 通常实现为非成员函数?

2. C++20 中 `auto operator<=>(const T&) const = default;` 做了什么?

3. 为什么应该避免重载 `operator&&` 和 `operator||`?