運算子過載
為使用者定義型別的運算元定製 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 將一種型別轉換為另一種相關型別 |