运算符重载
为用户定义类型的操作数定制 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= 具有特殊属性:有关详细信息,请参见复制赋值和移动赋值。
规范复制赋值运算符应在自赋值时安全,并按引用返回 lhs
// 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; }
规范移动赋值应将已移动对象留在有效状态(即,类不变量保持不变的状态),并且在自赋值时不执行任何操作或至少将对象留在有效状态,并按引用返回 lhs(非 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() 时,它将成为 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); }
许多标准库算法接受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
[[编辑]] 递增与递减
当后缀递增或递减运算符出现在表达式中时,将使用整数参数 0 调用相应的用户定义函数(operator++ 或 operator--)。通常,它被声明为 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<,并期望它实现严格弱序(从而满足 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[] 定义两个重载: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 起) |
[[编辑]] 位运算算术运算符
实现 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& 最著名的例子是 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 中的 actors 中以这种能力使用。在 EDSL 中更常见,例如 cpp.react。
[[编辑]] 注解
特性测试 宏 | 值 | Std | 特性 |
---|---|---|---|
__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++ 标准。
DR | 应用于 | 已发布行为 | 正确行为 |
---|---|---|---|
CWG 1481 | C++98 | 非成员前缀递增运算符只能有一个参数 类类型、枚举类型或对此类类型的引用类型 |
无类型要求 |
CWG 2931 | C++23 | 显式对象成员运算符函数只能没有参数 类类型、枚举类型或对此类类型的引用类型 |
已禁止 |
[[编辑]] 参见
常用运算符 | ||||||
---|---|---|---|---|---|---|
赋值 | 递增 递减 |
算术 | 逻辑 | 比较 | 成员 访问 |
其他 |
a = b |
++a |
+a |
!a |
a == b |
a[...] |
函数调用 a(...) |
逗号 a, b | ||||||
条件 a ? b : c | ||||||
特殊运算符 | ||||||
static_cast 将一种类型转换为另一种相关类型 |