运算符重载
为用户定义类型的操作数定制 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"); // same as str += "world"; operator<<(operator<<(std::cout, str), '\n'); // same as std::cout << str << '\n'; // (since C++17) except for sequencing
静态重载运算符作为成员函数的重载运算符可以声明为静态。然而,这只允许用于 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= 具有特殊属性:详细信息请参见复制赋值和移动赋值。
规范的复制赋值运算符预期对自赋值是安全的,并通过引用返回左值
// copy assignment T& operator=(const T& other) { // Guard self assignment if (this == &other) return *this; // assume *this manages a reusable resource, such as a heap-allocated buffer mArray if (size != other.size) // resource in *this cannot be reused { temp = new int[other.size]; // allocate resource, if throws, do nothing delete[] mArray; // release resource in *this mArray = temp; size = other.size; } std::copy(other.mArray, other.mArray + other.size, mArray); return *this; }
规范的移动赋值预期将移动源对象置于有效状态(即,类不变量保持不变的状态),并且在自赋值时不做任何事情或至少将对象置于有效状态,并通过对非 const 的引用返回左值,并且为 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; } |
(C++11 起) |
在复制赋值不能从资源重用中受益(它不管理堆分配的数组,并且没有(可能是传递的)成员这样做,例如 std::vector 或 std::string 成员)的情况下,有一种流行的便捷简写:复制并交换赋值运算符,它通过值接收其参数(因此根据参数的值类别,既可作为复制赋值又可作为移动赋值),与参数交换,然后让析构函数清理它。
// copy assignment (copy-and-swap idiom) T& T::operator=(T other) noexcept // call copy or move constructor to construct other { std::swap(size, other.size); // exchange resources between *this and other std::swap(mArray, other.mArray); return *this; } // destructor of other is called to release the resources formerly managed by *this
此形式自动提供强异常保证,但禁止资源重用。
[编辑] 流提取和插入
接受 std::istream& 或 std::ostream& 作为左操作数的 operator>>
和 operator<<
的重载称为插入和提取运算符。由于它们将用户定义类型作为右操作数(a @ b
中的 b
),因此它们必须作为非成员函数实现。
std::ostream& operator<<(std::ostream& os, const T& obj) { // write obj to stream return os; } std::istream& operator>>(std::istream& is, T& obj) { // read obj from stream if (/* T could not be constructed */) is.setstate(std::ios::failbit); return is; }
这些运算符有时作为友元函数实现。
[编辑] 函数调用运算符
当用户定义的类重载函数调用运算符 operator() 时,它成为一个函数对象类型。
这种类型的对象可以在函数调用表达式中使用
// An object of this type represents a linear function of one variable a * x + b. struct Linear { double a, b; double operator()(double x) const { return a * x + b; } }; int main() { Linear f{2, 1}; // Represents function 2x + 1. Linear g{-1, 0}; // Represents function -x. // f and g are objects that can be used like a function. 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 { // prefix increment X& operator++() { // actual increment takes place here return *this; // return new value by reference } // postfix increment X operator++(int) { X old = *this; // copy old value operator++(); // prefix increment return old; // return old value } // prefix decrement X& operator--() { // actual decrement takes place here return *this; // return new value by reference } // postfix decrement X operator--(int) { X old = *this; // copy old value operator--(); // prefix decrement return old; // return old value } };
尽管前缀自增和自减运算符的规范实现通过引用返回,但与任何运算符重载一样,返回类型是用户定义的;例如,std::atomic 的这些运算符的重载通过值返回。
[编辑] 二元算术运算符
二元运算符通常实现为非成员函数以保持对称性(例如,当添加一个复数和一个整数时,如果 operator+ 是复数类型的成员函数,那么只有 complex + integer 可以编译,而 integer + complex 则不能)。因为每个二元算术运算符都存在一个相应的复合赋值运算符,二元运算符的规范形式是通过其复合赋值来实现的
class X { public: X& operator+=(const X& rhs) // compound assignment (does not need to be a member, { // but often is, to modify the private members) /* addition of rhs to *this takes place here */ return *this; // return the result by reference } // friends defined inside class body are inline and are hidden from non-ADL lookup friend X operator+(X lhs, // passing lhs by value helps optimize chained a+b+c const X& rhs) // otherwise, both parameters may be const references { lhs += rhs; // reuse compound assignment return lhs; // return the result by value (uses move constructor) } };
[编辑] 比较运算符
标准库算法,例如 std::sort,以及容器,例如 std::set,默认要求为用户提供的类型定义 operator<,并要求它实现严格弱序(从而满足比较要求)。为结构实现严格弱序的惯用方法是使用 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); // keep the same order } };
通常,一旦提供了 operator<,其他关系运算符都会通过 operator< 来实现。
inline bool operator< (const X& lhs, const X& rhs) { /* do actual comparison */ } 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) { /* do actual comparison */ } 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; }
[编辑] 数组下标运算符
提供数组式访问并允许读写的用户定义类通常为 operator[] 定义两个重载:const 和非 const 变体
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[] 只能带一个下标。为了提供多维数组访问语义,例如实现 3D 数组访问 a[i][j][k] = x;,operator[] 必须返回一个 2D 平面的引用,该平面必须有自己的 operator[],它返回一个 1D 行的引用,该行必须有 operator[],它返回元素的引用。为了避免这种复杂性,一些库选择重载 operator(),以便 3D 访问表达式具有类似 Fortran 的语法 a(i, j, k) = x;。 |
(直至 C++23) |
operator[] 可以接受任意数量的下标。例如,一个 3D 数组类的 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 起) |
[编辑] 位算术运算符
实现位掩码类型要求的用户定义类和枚举需要重载位算术运算符 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& 最著名的例子是 Microsoft 类
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 中的 actor 就以这种方式使用它。在 EDSLs(例如 cpp.react)中更常见。
[编辑] 注意
特性测试宏 | 值 | 标准 | 特性 |
---|---|---|---|
__cpp_static_call_operator |
202207L |
(C++23) | static operator() |
__cpp_multidimensional_subscript |
202211L |
(C++23) | static operator[] |
[编辑] 关键词
[编辑] 示例
#include <iostream> class Fraction { // or C++17's 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 将一种类型转换为另一种相关类型 |