运算符重载
为用户定义类型的操作数自定义 C++ 运算符。
内容 |
[编辑] 语法
重载运算符是具有特殊函数名的函数
operator op |
(1) | ||||||||
operator type |
(2) | ||||||||
operator new operator new [] |
(3) | ||||||||
operator delete operator delete [] |
(4) | ||||||||
operator "" suffix-identifier |
(5) | (自 C++11 起) | |||||||
operator co_await |
(6) | (自 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 起) |
注意:对于重载 co_await
、(自 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]
[edit] 赋值运算符
赋值运算符(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; }
规范的移动赋值运算符应 将移动后的对象留在有效状态(即,状态具有完整的类不变式),并且要么 不执行任何操作,或者至少在自赋值时将对象留在有效状态,并通过引用返回左值,并声明为 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
此形式自动提供 强异常保证,但禁止资源重用。
[edit] 流提取和插入
接受 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; }
这些运算符有时被实现为 友元函数。
[edit] 函数调用运算符
当用户定义的类重载函数调用运算符,operator() 时,它将成为 FunctionObject 类型。
这种类型的对象可以在函数调用表达式中使用。
// 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); }
从 std::sort 到 std::accumulate 的许多标准算法都接受 FunctionObjects 来定制行为。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
[edit] 递增和递减
当后缀递增或递减运算符出现在表达式中时,相应的用户定义函数(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 的重载按值返回。
[edit] 二元算术运算符
二元运算符通常被实现为非成员函数,以保持对称性(例如,在添加复数和整数时,如果 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) } };
[edit] 比较运算符
标准算法(如 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); // 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==,则编译器会自动生成不等运算符。同样,如果三方比较运算符 operator<=> 被定义,则编译器会自动生成四个关系运算符。 operator== 和 operator!= 反过来会由编译器生成,如果 operator<=> 被定义为默认。 struct Record { std::string name; unsigned int floor; double weight; auto operator<=>(const Record&) const = default; }; // records can now be compared with ==, !=, <, <=, >, and >= 有关详细信息,请参阅 默认比较。 |
(自 C++20 起) |
[edit] 数组下标运算符
提供数组式访问的用户定义类(允许读写)通常为 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[] 可以接受任意数量的下标。例如,一个声明为 T& operator[](std::size_t x, std::size_t y, std::size_t z); 的 3D 数组类的 operator[] 可以直接访问元素。 运行此代码 #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 起) |
[edit] 位运算符
实现 BitmaskType 要求的用户定义类和枚举需要重载位运算符 operator&、operator|、operator^、operator~、operator&=、operator|= 和 operator^=,并且可以选择重载移位运算符 operator<< operator>>、operator>>= 和 operator<<=。规范的实现通常遵循上面描述的二元算术运算符模式。
[edit] 布尔取反运算符
运算符 operator! 通常由旨在用于布尔上下文的用户定义类重载。此类类还提供了一个用户定义的转换为布尔类型的转换函数(参见 std::basic_ios 获取标准库示例),并且 operator! 的预期行为是返回与 operator bool 相反的值。 |
(直到 C++11) |
由于内置运算符 ! 执行 隐式转换为 |
(自 C++11 起) |
[edit] 很少重载的运算符
以下运算符很少重载
- 取地址运算符 operator&。如果对不完整类型的左值应用一元 &,并且完整类型声明了一个重载的 operator&,则该运算符是否具有内置含义或调用运算符函数是未指定的。因为此运算符可能被重载,所以通用库使用 std::addressof 来获取用户定义类型对象的地址。最著名的规范重载运算符& 示例是 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 中以该身份使用。在诸如 cpp.react 的 EDSL 中,它更为常见。
[edit] 备注
功能测试 宏 | 值 | Std | 功能 |
---|---|---|---|
__cpp_static_call_operator |
202207L | (C++23) | static operator() |
__cpp_multidimensional_subscript |
202211L | (C++23) | static operator[] |
[edit] 关键字
[edit] 示例
#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
[edit] 缺陷报告
以下更改行为的缺陷报告被追溯地应用于先前发布的 C++ 标准。
DR | 应用于 | 已发布的行为 | 正确的行为 |
---|---|---|---|
CWG 1481 | C++98 | 非成员前缀递增运算符可以 只有类或枚举类型的参数 |
没有类型要求 |
[edit] 参见
常用运算符 | ||||||
---|---|---|---|---|---|---|
赋值 | 递增 递减 |
算术 | 逻辑 | 比较 | 成员 访问 |
其他 |
a = b |
++a |
+a |
!a |
a == b |
a[...] |
函数调用 |
a(...) | ||||||
逗号 | ||||||
a, b | ||||||
条件 | ||||||
a ? b : c | ||||||
特殊运算符 | ||||||
static_cast 将一种类型转换为另一种相关类型 |