operator overloading
自定义用户定义类型操作数的 C++ 运算符。
目录 |
语法
运算符函数 是具有特殊函数名称的 函数 :
operator
op
|
(1) | ||||||||
operator
new
operator
new []
|
(2) | ||||||||
operator
delete
operator
delete []
|
(3) | ||||||||
operator
co_await
|
(4) | (自 C++20 起) | |||||||
| op | - | 可为以下任意运算符: + - * / % ^ & | ~ ! = < > + = - = * = / = % = ^ = & = | = << >> >>= <<= == ! = <= >= <=> (C++20 起) && || ++ -- , - > * - > ( ) [ ] |
非标点运算符的行为在各自单独的页面中描述。除非另有说明,本页面其余描述不适用于这些函数。
说明
当运算符出现在 表达式 中,且其至少一个操作数具有 类类型 或 枚举类型 时,将通过 重载决议 从所有函数签名匹配下列条件的函数中确定需要调用的用户定义函数:
| 表达式 | 作为成员函数 | 作为非成员函数 | 示例 |
|---|---|---|---|
| @a | (a).operator@ ( ) | operator@ (a) | ! std:: cin 调用 std:: cin . operator ! ( ) |
| a@b | (a).operator@ (b) | operator@ (a, b) | std:: cout << 42 调用 std:: cout . operator << ( 42 ) |
| a=b | (a).operator= (b) | 不能作为非成员函数 | 给定 std:: string s ; , s = "abc" ; 调用 s. operator = ( "abc" ) |
| a(b...) | (a).operator()(b...) | 不能作为非成员函数 | 给定 std:: random_device r ; , auto n = r ( ) ; 调用 r. operator ( ) ( ) |
| a[b...] | (a).operator[](b...) | 不能作为非成员函数 | 给定 std:: map < int , int > m ; , m [ 1 ] = 2 ; 调用 m. operator [ ] ( 1 ) |
| a-> | (a).operator->( ) | 不能作为非成员函数 | 给定 std:: unique_ptr < S > p ; , p - > bar ( ) 调用 p. operator - > ( ) |
| a@ | (a).operator@ (0) | operator@ (a, 0) | 给定 std:: vector < int > :: iterator i ; , i ++ 调用 i. operator ++ ( 0 ) |
|
在此表中,
|
|||
|
此外,对于比较运算符 == 、 ! = 、 < 、 > 、 <= 、 >= 、 <=> ,重载决议还会考虑 重写候选 operator == 或 operator <=> 。 |
(C++20 起) |
重载运算符(但不包括内置运算符)可以通过函数表示法调用:
std::string str = "Hello, "; str.operator+=("world"); // 等同于 str += "world"; operator<<(operator<<(std::cout, str), '\n'); // 等同于 std::cout << str << '\n'; // (C++17起) 除序列点外完全一致
静态重载运算符作为成员函数的重载运算符可以声明为 静态 。但这仅适用于 operator ( ) 和 operator [ ] 。 这类运算符可以通过函数表示法调用。然而当这些运算符出现在表达式中时,仍然需要类类型的对象。 struct SwapThem { template<typename T> static void operator()(T& lhs, T& rhs) { std::ranges::swap(lhs, rhs); } template<typename T> static void operator[](T& lhs, T& rhs) { std::ranges::swap(lhs, rhs); } }; inline constexpr SwapThem swap_them{}; void foo() { int a = 1, b = 2; swap_them(a, b); // OK swap_them[a, b]; // OK SwapThem{}(a, b); // OK SwapThem{}[a, b]; // OK SwapThem::operator()(a, b); // OK SwapThem::operator[](a, b); // OK SwapThem(a, b); // error, invalid construction SwapThem[a, b]; // error } |
(C++23 起) |
限制
- 运算符函数必须至少拥有一个函数参数或隐式对象参数,其类型为类、类的引用、枚举或枚举的引用。
-
运算符
::(作用域解析)、.(成员访问)、.*(通过成员指针访问成员)和?:(三元条件)不能被重载。 -
不能创建新的运算符,例如
**、<>或&|。 - 无法更改运算符的优先级、分组或操作数数量。
-
运算符
->的重载必须返回原始指针,或者返回一个对象(通过引用或值),该对象的运算符->本身也被重载。 -
运算符
&&和||的重载会失去短路求值特性。
|
(C++17 前) |
规范实现
除了上述限制外,语言对重载运算符的具体操作或返回类型(不参与重载决议)没有其他约束,但通常期望重载运算符的行为尽可能与内置运算符保持一致: operator + 应当执行加法而非乘法运算, operator = 应当执行赋值操作等。相关运算符应具有相似行为( operator + 与 operator + = 执行相同的类加法运算)。返回类型受运算符预期使用场景的限制:例如,赋值运算符通过引用返回来实现 a = b = c = d 的链式写法,因为内置运算符支持这种操作。
通常被重载的运算符具有以下典型规范形式: [1]
赋值运算符
赋值运算符 operator = 具有特殊属性:详见 复制赋值 与 移动赋值 说明。
规范的拷贝赋值运算符应当 对自赋值操作保持安全 ,并返回左侧运算对象的引用:
// 拷贝赋值 T& operator=(const T& other) { // 防止自赋值 if (this == &other) return *this; // 假设 *this 管理可重用资源,例如堆分配的缓冲区 mArray if (size != other.size) // *this 中的资源无法重用 { temp = new int[other.size]; // 分配资源,若抛出异常则不执行任何操作 delete[] mArray; // 释放 *this 中的资源 mArray = temp; size = other.size; } std::copy(other.mArray, other.mArray + other.size, mArray); return *this; }
|
规范的移动赋值操作应当: 使被移动对象处于有效状态 (即保持类不变量的状态),并且在自赋值时 不执行操作 或至少使对象保持有效状态,同时返回非常量左值的引用,且标记为 noexcept: // move assignment T& operator=(T&& other) noexcept { // Guard self assignment if (this == &other) return *this; // delete[]/size=0 would also be ok delete[] mArray; // release resource in *this mArray = std::exchange(other.mArray, nullptr); // leave other in valid state size = std::exchange(other.size, 0); return *this; } |
(since C++11) |
在那些拷贝赋值无法受益于资源复用的情况下(即不管理堆分配数组且没有(可能传递性的)成员管理此类资源,例如成员 std::vector 或 std::string ),存在一种流行且便捷的简写方式:拷贝交换赋值运算符。该运算符通过值接收参数(从而根据实参的值类别同时实现拷贝赋值和移动赋值),与参数进行交换,并让析构函数完成清理工作。
此表单自动提供 强异常保证 ,但禁止资源重用。
流提取与插入
以
std::
istream
&
或
std::
ostream
&
作为左操作数的
operator>>
和
operator<<
重载被称为插入与提取运算符。由于它们将用户定义类型作为右参数(即
a @ b
中的
b
),因此必须作为非成员函数实现。
std::ostream& operator<<(std::ostream& os, const T& obj) { // 将对象写入流 return os; } std::istream& operator>>(std::istream& is, T& obj) { // 从流中读取对象 if (/* 无法构造 T */) is.setstate(std::ios::failbit); return is; }
这些运算符有时被实现为 friend functions 。
函数调用运算符
当用户自定义类重载了函数调用运算符 operator ( ) 时,它便成为 FunctionObject 类型。
此类对象可用于函数调用表达式:
// 此类型对象表示单变量线性函数 a * x + b struct Linear { double a, b; double operator()(double x) const { return a * x + b; } }; int main() { Linear f{2, 1}; // 表示函数 2x + 1 Linear g{-1, 0}; // 表示函数 -x // f 和 g 是可像函数一样使用的对象 double f_0 = f(0); double f_1 = f(1); double g_0 = g(0); }
许多标准库 算法 接受 函数对象 来自定义行为。 operator ( ) 并没有特别显著的规范形式,但为说明其用法:
#include <algorithm> #include <iostream> #include <vector> struct Sum { int sum = 0; void operator()(int n) { sum += n; } }; int main() { std::vector<int> v = {1, 2, 3, 4, 5}; Sum s = std::for_each(v.begin(), v.end(), Sum()); std::cout << "The sum is " << s.sum << '\n'; }
输出:
The sum is 15
自增与自减
当后缀递增或递减运算符出现在表达式中时,会调用对应的用户定义函数( operator ++ 或 operator -- )并传入整型参数 0 。通常其声明形式为 T operator ++ ( int ) 或 T operator -- ( int ) ,该参数实际会被忽略。后缀递增和递减运算符通常基于其前缀版本实现:
struct X { // 前缀递增 X& operator++() { // 实际递增操作在此执行 return *this; // 通过引用返回新值 } // 后缀递增 X operator++(int) { X old = *this; // 复制旧值 operator++(); // 调用前缀递增 return old; // 返回旧值 } // 前缀递减 X& operator--() { // 实际递减操作在此执行 return *this; // 通过引用返回新值 } // 后缀递减 X operator--(int) { X old = *this; // 复制旧值 operator--(); // 调用前缀递减 return old; // 返回旧值 } };
虽然前缀递增和递减运算符的标准实现通过引用返回,但如同所有运算符重载一样,返回类型是由用户定义的;例如这些运算符针对 std::atomic 的重载版本会通过值返回。
二元算术运算符
二元运算符通常被实现为非成员函数以保持对称性(例如,当对复数与整数进行加法运算时,若 operator + 是复数类型的成员函数,则仅 complex + integer 能通过编译,而 integer + complex 则不能)。由于每个二元算术运算符都存在对应的复合赋值运算符,二元运算符的规范实现通常基于其复合赋值运算来实现:
class X { public: X& operator+=(const X& rhs) // 复合赋值(不必是成员函数, { // 但通常作为成员函数以修改私有成员) /* 此处实现 rhs 与 *this 的相加操作 */ return *this; // 通过引用返回结果 } // 在类体内定义的友元函数是内联的,且对非ADL查找隐藏 friend X operator+(X lhs, // 按值传递 lhs 有助于优化链式运算 a+b+c const X& rhs) // 否则两个参数都应为常引用 { lhs += rhs; // 复用复合赋值运算符 return lhs; // 按值返回结果(使用移动构造函数) } };
比较运算符
标准库算法如 std::sort 和容器如 std::set 默认要求用户自定义类型定义 operator < ,并要求其实现严格弱序(从而满足 Compare 要求)。为结构体实现严格弱序的惯用方法是使用 std::tie 提供的字典序比较:
struct Record { std::string name; unsigned int floor; double weight; friend bool operator<(const Record& l, const Record& r) { return std::tie(l.name, l.floor, l.weight) < std::tie(r.name, r.floor, r.weight); // 保持相同顺序 } };
通常,一旦提供了 operator < ,其他关系运算符就会基于 operator < 来实现。
inline bool operator< (const X& lhs, const X& rhs) { /* 执行实际比较操作 */ } inline bool operator> (const X& lhs, const X& rhs) { return rhs < lhs; } inline bool operator<=(const X& lhs, const X& rhs) { return !(lhs > rhs); } inline bool operator>=(const X& lhs, const X& rhs) { return !(lhs < rhs); }
同样地,不等运算符通常基于 operator == 实现:
inline bool operator==(const X& lhs, const X& rhs) { /* 执行实际比较操作 */ } inline bool operator!=(const X& lhs, const X& rhs) { return !(lhs == rhs); }
当提供三路比较(例如 std::memcmp 或 std::string::compare )时,所有六个双向比较运算符都可以通过它来表达:
inline bool operator==(const X& lhs, const X& rhs) { return cmp(lhs,rhs) == 0; } inline bool operator!=(const X& lhs, const X& rhs) { return cmp(lhs,rhs) != 0; } inline bool operator< (const X& lhs, const X& rhs) { return cmp(lhs,rhs) < 0; } inline bool operator> (const X& lhs, const X& rhs) { return cmp(lhs,rhs) > 0; } inline bool operator<=(const X& lhs, const X& rhs) { return cmp(lhs,rhs) <= 0; } inline bool operator>=(const X& lhs, const X& rhs) { return cmp(lhs,rhs) >= 0; }
`标签内)已按规则保留原文未翻译 - 保留了所有HTML标签和属性 - 遵循了不翻译专业术语的要求 - 仅添加了中文说明文本
数组下标运算符
提供类似数组读写访问的用户自定义类通常需要为 operator [ ] 定义两个重载版本:常量版本与非常量版本:
struct T { value_t& operator[](std::size_t idx) { return mVector[idx]; } const value_t& operator[](std::size_t idx) const { return mVector[idx]; } };
|
或者,它们也可以通过使用 显式对象形参 的单个成员函数模板来表示: struct T { decltype(auto) operator[](this auto& self, std::size_t idx) { return self.mVector[idx]; } }; |
(C++23 起) |
如果已知值类型为标量类型,const 变体应当按值返回。
当不希望或无法直接访问容器元素,或者需要区分左值 c [ i ] = v ; 与右值 v = c [ i ] ; 用法时, operator [ ] 可以返回一个代理对象。具体示例可参阅 std::bitset::operator[] 。
|
operator [ ] 只能接受一个下标。为实现多维数组访问语义,例如实现三维数组访问 a [ i ] [ j ] [ k ] = x ; , operator [ ] 必须返回对二维平面的引用,该平面又需要有自己的 operator [ ] 返回对一维行的引用,而该行又需要具有 operator [ ] 返回对元素的引用。为避免这种复杂性,某些库选择重载 operator ( ) ,使得三维访问表达式具有类似 Fortran 的语法 a ( i, j, k ) = x ; 。 |
(C++23 前) |
|
operator [ ] 可以接受任意数量的下标。例如,三维数组类的 operator [ ] 声明为 T & operator [ ] ( std:: size_t x, std:: size_t y, std:: size_t z ) ; 时可以直接访问元素。
运行此代码
#include <array> #include <cassert> #include <iostream> template<typename T, std::size_t Z, std::size_t Y, std::size_t X> struct Array3d { std::array<T, X * Y * Z> m{}; constexpr T& operator[](std::size_t z, std::size_t y, std::size_t x) // C++23 { assert(x < X and y < Y and z < Z); return m[z * Y * X + y * X + x]; } }; int main() { Array3d<int, 4, 3, 2> v; v[3, 2, 1] = 42; std::cout << "v[3, 2, 1] = " << v[3, 2, 1] << '\n'; } 输出: v[3, 2, 1] = 42 |
(C++23 起) |
位运算算术运算符
实现 BitmaskType 要求的用户自定义类与枚举类型,必须重载位运算运算符 operator & 、 operator | 、 operator ^ 、 operator~ 、 operator & = 、 operator | = 与 operator ^ = ,并可选择性地重载移位运算符 operator << 、 operator >> 、 operator >>= 和 operator <<= 。其规范实现通常遵循前述二元算术运算符的模式。
布尔取反运算符
|
运算符 operator ! 通常由旨在布尔上下文中使用的用户定义类重载。此类类还提供向布尔类型的用户定义转换函数(标准库示例参见 std::basic_ios ),而 operator ! 的预期行为是返回与 operator bool 相反的值。 |
(C++11 前) |
|
由于内置运算符 ! 会执行 向 bool 的上下文转换operator bool 而无需重载 operator ! 。 |
(C++11 起) |
罕见重载运算符
以下运算符很少被重载:
-
取址运算符
operator
&
。若将一元 & 应用于不完整类型的左值,且其完整类型声明了重载的
operator
&
,则未指定该运算符是具有内置含义还是调用运算符函数。由于此运算符可能被重载,泛型库使用
std::addressof
来获取用户定义类型对象的地址。最著名的规范重载
operator
&
实例是微软的
CComPtrBase类。此运算符在 EDSL 中的使用示例可见于 boost.spirit 。 - 布尔逻辑运算符 operator && 和 operator || 。与内置版本不同,重载版本无法实现短路求值。 同时与内置版本不同的是,它们不会在右操作数之前对左操作数进行定序。 (C++17 前) 在标准库中,这些运算符仅针对 std::valarray 进行了重载。
- 逗号运算符 operator, 。 与内置版本不同,重载版本不会在右操作数之前对左操作数进行定序。 (C++17 前) 由于此运算符可能被重载,泛型库使用如 a, void ( ) , b 而非 a, b 的表达式来对用户定义类型的表达式执行序列化。boost 库在 boost.assign 、 boost.spirit 及其他库中使用了 operator, 。数据库访问库 SOCI 也重载了 operator, 。
- 通过成员指针访问成员的运算符 operator - > * 。重载此运算符没有特定缺点,但在实践中很少使用。有建议认为它可以作为 智能指针接口 的组成部分,实际上 boost.phoenix 中的执行器就以此方式使用它。在如 cpp.react 等 EDSL 中更为常见。
注释
| 功能测试 宏 | 值 | 标准 | 功能 |
|---|---|---|---|
__cpp_static_call_operator
|
202207L
|
(C++23) | static operator ( ) |
__cpp_multidimensional_subscript
|
202211L
|
(C++23) | static operator [ ] |
关键词
示例
#include <iostream> class Fraction { // 或 C++17 的 std::gcd constexpr int gcd(int a, int b) { return b == 0 ? a : gcd(b, a % b); } int n, d; public: constexpr Fraction(int n, int d = 1) : n(n / gcd(n, d)), d(d / gcd(n, d)) {} constexpr int num() const { return n; } constexpr int den() const { return d; } constexpr Fraction& operator*=(const Fraction& rhs) { int new_n = n * rhs.n / gcd(n * rhs.n, d * rhs.d); d = d * rhs.d / gcd(n * rhs.n, d * rhs.d); n = new_n; return *this; } }; std::ostream& operator<<(std::ostream& out, const Fraction& f) { return out << f.num() << '/' << f.den(); } constexpr bool operator==(const Fraction& lhs, const Fraction& rhs) { return lhs.num() == rhs.num() && lhs.den() == rhs.den(); } constexpr bool operator!=(const Fraction& lhs, const Fraction& rhs) { return !(lhs == rhs); } constexpr Fraction operator*(Fraction lhs, const Fraction& rhs) { return lhs *= rhs; } int main() { constexpr Fraction f1{3, 8}, f2{1, 2}, f3{10, 2}; std::cout << f1 << " * " << f2 << " = " << f1 * f2 << '\n' << f2 << " * " << f3 << " = " << f2 * f3 << '\n' << 2 << " * " << f1 << " = " << 2 * f1 << '\n'; static_assert(f3 == f2 * 10); }
输出:
3/8 * 1/2 = 3/16 1/2 * 5/1 = 5/2 2 * 3/8 = 3/4
缺陷报告
下列行为变更缺陷报告被追溯应用于先前发布的C++标准。
| 缺陷报告 | 适用标准 | 发布行为 | 正确行为 |
|---|---|---|---|
| CWG 1481 | C++98 | 非成员前缀递增运算符的参数只能是类类型、枚举类型或这些类型的引用类型 | 无类型限制 |
| CWG 2931 | C++23 | 显式对象成员运算符函数只能具有无类类型、枚举类型或这些类型的引用类型的参数 | 禁止 |
参见
| 常用运算符 | ||||||
|---|---|---|---|---|---|---|
| 赋值 |
自增
自减 |
算术 | 逻辑 | 比较 |
成员
访问 |
其他 |
|
a
=
b
|
++
a
|
+
a
|
!
a
|
a
==
b
|
a
[
...
]
|
函数调用
a ( ... ) |
|
逗号
a, b |
||||||
|
条件
a ? b : c |
||||||
| 特殊运算符 | ||||||
|
static_cast
将一种类型转换为另一种相关类型
|
||||||