Skip to content

13 Overloading C++ Operators

运算符重载概述

C++ 允许我们重载运算符,比如 + < * <<,常见的运算符,-> * 解引用的运算符,[] () 这样数组索引、函数调用运算符,还有转化运算符、分配回收内存的运算符等等。

重载运算符的好处:1)使我们的自定义类型更像是内置类型,也更容易理解;2)更好的控制程序,比如重载内存相关的运算符。重载运算符不是为了类的开发者,而是为了使用者!

重载运算符有一些限制:不能新增运算符;. .* 对象成员访问、:: 范围限定、?: 条件运算符不能重载;除了 () 函数调用、new delete、C++23 引入的 [] 之外,不能修改运算符的参数个数;不能修改运算符的优先级和结合性;不能用于内置类型,至少有一个参数是用户自定义类型。另外,有些运算符有不同的含义,比如 operator- 是负号还是减号、* 是乘号也可以解引用指针,<< 流插入运算符也是左移运算符,那么可以重载所有的含义。

当决定重载一个运算符,有许多点需要决策。

第一点是成员函数还是全局函数。有些算子必须是成员函数,有些算子必须是全局函数,有些算子两者皆可。如果两者都行,推荐是成员函数,因为可以标记为 virtual,可以利用继承体系的优势,还可以直接访问私有变量而无需引入 friend 函数。如果实现为全局函数,那么应该与自定义类在一个命名空间。

第二个点是参数类型。首先参数个数有很大的限制。虽然类型可以选择的余地很大,但是合理的选择并不多,比如对 T 类型实现 +,明显两个 std::string 不是合理的选择。

第三点是返回类型。返回类型并不影响重载决议。可以写任意类型,但是这不意味着应该做,有意义才是第一位要考虑的。

最后一点是行为,一定要有意义,语义直观。

有几个运算符可以被重载,但是不应该重载。首先是 & 取地址,会导致语义不明确;其次是 && ||,因为运算前两个操作数都必须先求值,丢失了短路效果;最后是 , 序列操作符,从左往右求值,很少有合理的原因需要重载这个。

下表总结了所有可以被重载的运算符,并且给出了相关的建议。T 是当前实现运算符重载的类型,E 是其他类型。这里没有给出全部可能的组合。

OPERATOR NAME OR CATEGORY MEMBER FUNCTION OR GLOBAL FUNCTION WHEN TO OVERLOAD SAMPLE PROTOTYPES
operator+
operator-
operator*
operator/
operator%
Binary arithmetic Global function recommended Whenever you want to provide these operations for your class T operator+(const T&, const T&);
T operator+(const T&,const E&);
operator-
operator+
operator~
Unary arithmetic and bitwise operators Member function recommended Whenever you want to provide these operations for your class T operator-() const;
operator++
operator--
Pre-increment and pre-decrement Member function recommended Whenever you overload += and -= taking an arithmetic argument (int, long, ...) T& operator++();
operator++
operator--
Post-increment and post-decrement Member function recommended Whenever you overload += and -= taking an arithmetic argument (int, long, ...) T operator++(int);
operator= Assignment operator Member function required Whenever your class has dynamically allocated resources, or members that are references T& operator=(const T&);
operator+=
operator-=
operator*=
operator/= operator%=
Shorthand / compound arithmetic assignment operator Member function recommended Whenever you overload the binary arithmetic operators and your class is not designed to be immutable T& operator+=(const T&);
T& operator+=(const E&);
operator<<
operator>>
operator&
operator\|
operator^
Binary bitwise operators Global function recommended Whenever you want to provide these operations T operator<<(const T&, const T&);
T operator<<(const T&, const E&);
operator<<=
operator>>=
operator&=
operator\|=
operator^=
Shorthand / compound bitwise assignment operator Member function recommended Whenever you overload the binary bitwise operators and your class is not designed to be immutable T& operator<<=(const T&);
T& operator<<=(const E&);
operator<=> Three-way comparison operator Member function recommended Whenever you want to provide comparison support for your class; if possible, this should be defaulted using =default auto operator<=>(const T&) const = default;
partial_ordering operator<=>(const E&) const;
operator== Binary equality operator Post-C++20: member function recommended
Pre-C++20: global function recommended
Whenever you want to provide comparison support for your class, and you cannot default the three-way comparison operator bool operator==(const T&) const;
bool operator==(const E&) const;
bool operator==(const T&, const T&);
bool operator==(const T&, const E&);
operator!= Binary inequality operator Post-C++20: member function recommended
Pre-C++20: global function recommended
Post-C++20: not needed as the compiler automatically provides != when == is supported
Pre-C++20: Whenever you want to provide comparison support for your class
bool operator!=(const T&) const;
bool operator!=(const E&) const;
bool operator!=(const T&, const T&);
bool operator!=(const T&, const E&);
operator<
operator>
operator<=
operator>=
Binary comparison operators Global function recommended Whenever you want to provide these operations; not needed when <=> is provided bool operator<(const T&, const T&);
bool operator<(const T&, const E&);
operator<<
operator>>
I/O stream operators (insertion and extraction) Global function required Whenever you want to provide these operations ostream& operator<<(ostream&, const T&);
istream& operator>>(istream&, T&);
operator! Boolean negation operator Member function recommended Rarely; use bool or void* conversion instead bool operator!() const;
operator&&
operator||
Binary Boolean operators Global function recommended Rarely, if ever, because you lose short-circuiting; it’s better to overload & and | instead, as these never short-circuit bool operator&&(const T&, const T&);
operator[] Subscripting (array index) operator Member function required When you want to support subscripting E& operator[] (size_t);
const E& operator[] (size_t) const;
operator() Function call operator Member function required When you want objects to behave like function pointers Return type and parameters can vary; see later examples in this chapter
operator type() Conversion, or cast, operators (separate operator for each type) Member function required When you want to provide conversions from your class to other types operator double() const;
operator ""_x User-defined literal operator Global function required When you want to support user defined literals T operator""_i(long double d);
operator new
operator new[]
Memory allocation routines Member function recommended When you want to control memory allocation for your classes (rarely) void* operator new(size_t size);
void* operator new[](size_t size);
operator delete
operator delete[]
Memory deallocation routines Member function recommended Whenever you overload the memory allocation routines (rarely) void operator delete(void* ptr) noexcept;
void operator delete[](void* ptr) noexcept;
operator*
operator->
Dereferencing operators Member function recommended for operator*
Member function required for operator->
Useful for smart pointers E& operator*() const;
E* operator->() const;
operator& Address-of operator N/A Never N/A
operator->* Dereference pointer-to-member N/A Never N/A
operator, Comma operator N/A Never N/A

上面的表中的例子并没有右值引用的例子。对于大部分运算符,普通的左值引用和右值引用都是有意义的,当然实际要依赖于类本身。比如 std::stringoperator+ 有四种重载,对于右值的重载,由于传入对象不再使用了,有可能可以使用对应的内存而提升性能。

std::string operator+(const std::string &lhs, const std::string &rhs); // No memory reuse.
std::string operator+(std::string &&lhs, const std::string &rhs);      // Can reuse memory of lhs.
std::string operator+(const std::string &lhs, std::string &&rhs);      // Can reuse memory of rhs.
std::string operator+(std::string &&lhs, std::string &&rhs); // Can reuse memory of lhs or rhs.

运算符有优先级,个人经验是记住乘除大于加减,逻辑运算、比较运算比算数运算低,其他时候如果不确定,加括号。结合性指的是先处理哪一个操作数,大部分是从左往右的。只有类似 += ++x *x &x (T) 这些是从右往左。

重载算数运算符