演算子オーバーロード
ユーザー定義型のオペランドに対して C++ 演算子をカスタマイズします。
目次 |
[編集] 構文
演算子関数は、特殊な関数名を持つ関数です。
operator op |
(1) | ||||||||
operator newoperator new [] |
(2) | ||||||||
operator deleteoperator delete [] |
(3) | ||||||||
operator co_await |
(4) | (C++20以降) | |||||||
| op | - | 以下のいずれかの演算子: + - * / % ^ & | ~ ! = < > += -= *= /= %= ^= &= |= << >> >>= <<= == != <= >= <=>(C++20 以降) && || ++ -- , ->* -> () [] |
句読点以外の演算子の動作は、それぞれのページで説明されています。特に指定がない限り、このページの残りの説明はこれらの関数には適用されません。
[編集] 説明
演算子が式に現れ、そのオペランドの少なくとも1つがクラス型または列挙型を持つ場合、オーバーロード解決を使用して、以下のシグネチャに一致するすべての関数の中から呼び出されるユーザー定義関数が決定されます。
| Expression | メンバ関数として | 非メンバ関数として | 例 |
|---|---|---|---|
| @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から) |
[編集] 制限
- 演算子関数は、少なくとも1つの関数パラメータ、または型がクラス、クラスへの参照、列挙型、あるいは列挙型への参照である暗黙のオブジェクトパラメータを持つ必要があります。
- 演算子
::(スコープ解決),.(メンバアクセス),.*(メンバへのポインタによるメンバアクセス),?:(三項条件) はオーバーロードできません。 **,<>,&|などの新しい演算子は作成できません。- 演算子の優先順位、グループ化、またはオペランドの数を変更することはできません。
- 演算子
->のオーバーロードは、生ポインタを返すか、または演算子->がオーバーロードされているオブジェクト(参照または値渡し)を返す必要があります。 - 演算子
&&と||のオーバーロードは短絡評価を失います。
|
(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; }
|
標準的なムーブ代入は、ムーブ元のオブジェクトを有効な状態(つまり、クラスの不変条件が損なわれていない状態)に保ち、自己代入時には何もせず、または少なくともオブジェクトを有効な状態に保ち、非 const 参照で lhs を返し、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); }
多くの標準ライブラリのアルゴリズムは、動作をカスタマイズするためにFunctionObjectを受け入れます。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< が定義され、それが厳密な弱順序(したがって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 など)が提供される場合、6つの二方比較演算子すべてをそれを通じて表現できます。
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[] の2つのオーバーロード、すなわち 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 バリアントは値を返す必要があります。
コンテナの要素への直接アクセスが望ましくない、または不可能である場合、あるいはlvalue c[i] = v; とrvalue v = c[i]; の使用法を区別する場合、operator[] はプロキシを返すことがあります。例として std::bitset::operator[] を参照してください。
|
operator[] は1つの添字しか取ることができません。多次元配列アクセスセマンティクス、例えば 3D 配列アクセス a[i][j][k] = x; を実装するには、operator[] は 2D 平面への参照を返す必要があり、その 2D 平面は独自の operator[] を持ち、それが 1D 行への参照を返し、その 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&。不完全型のlvalueに単項 & が適用され、完全型がオーバーロードされた operator& を宣言している場合、演算子が組み込みの意味を持つか、演算子関数が呼び出されるかは未規定です。この演算子はオーバーロードできるため、汎用ライブラリはユーザー定義型のオブジェクトのアドレスを取得するために std::addressof を使用します。標準的にオーバーロードされた operator& の最もよく知られた例は、Microsoft のクラス
CComPtrBaseです。この演算子が EDSL で使用されている例は boost.spirit にあります。 - ブール論理演算子 operator&& と operator||。組み込みバージョンとは異なり、オーバーロードは短絡評価を実装できません。組み込みバージョンとは異なり、左オペランドを右オペランドの前にシーケンスしません。(C++17 まで) 標準ライブラリでは、これらの演算子は std::valarray に対してのみオーバーロードされています。
- カンマ演算子 operator,。組み込みバージョンとは異なり、オーバーロードは左オペランドを右オペランドの前にシーケンスしません。(C++17 まで) この演算子はオーバーロードできるため、汎用ライブラリは、ユーザー定義型の式の実行をシーケンスするために、a, b の代わりに a, void(), 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 { // 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 | ||||||
| conditional a ? b : c | ||||||
| 特殊な演算子 | ||||||
|
static_castは、ある型を関連する別の型に変換する | ||||||
[編集] 外部リンク
|