operator overloading
ユーザー定義型のオペランドに対するC++演算子をカスタマイズします。
目次 |
構文
Operator functions are functions with special function names:
operator
op
|
(1) | ||||||||
operator
new
operator
new []
|
(2) | ||||||||
operator
delete
operator
delete []
|
(3) | ||||||||
operator
co_await
|
(4) | (C++20以降) | |||||||
| op | - | 以下のいずれかの演算子: + - * / % ^ & | ~ ! = < > + = - = * = / = % = ^ = & = | = << >> >>= <<= == ! = <= >= <=> (C++20以降) && || ++ -- , - > * - > ( ) [ ] |
句読点以外の演算子の動作はそれぞれの専用ページで説明されています。特に明記されていない限り、このページの残りの説明はこれらの関数には適用されません。
説明
演算子が 式 内に現れ、そのオペランドの少なくとも1つが クラス型 または 列挙型 を持つ場合、 オーバーロード解決 が使用され、以下のシグネチャに一致するすべての関数の中から呼び出すユーザー定義関数が決定されます:
| 式 | メンバ関数として | 非メンバ関数として | 例 |
|---|---|---|---|
| @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"); // str += "world"; と同じ operator<<(operator<<(std::cout, str), '\n'); // std::cout << str << '\n'; と同じ // (C++17以降) シーケンス処理を除く
静的オーバーロード演算子メンバ関数として定義されたオーバーロード演算子は static として宣言できます。ただし、これは 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 = には特別な特性があります:詳細は copy assignment および move assignment を参照してください。
正規のコピー代入演算子は、 自己代入に対して安全であること が求められ、左辺値を参照で返す必要があります:
// コピー代入 T& operator=(const T& other) { // 自己代入の防止 if (this == &other) return *this; // *thisがヒープ割り当てバッファmArrayなどの再利用可能なリソースを管理していると仮定 if (size != other.size) // *this内のリソースは再利用できない { temp = new int[other.size]; // リソースを割り当て、例外が発生した場合は何もしない delete[] mArray; // *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 のようなメンバーを持たない場合)では、一般的で便利な省略記法が存在します:コピー・アンド・スワップ代入演算子です。これはパラメータを値で受け取り(したがって引数の値カテゴリに応じてコピー代入とムーブ代入の両方として機能)、パラメータとスワップし、デストラクタに後処理を任せます。
このフォームは自動的に strong exception guarantee を提供しますが、リソースの再利用を禁止します。
ストリーム抽出と挿入
operator>>
および
operator<<
のオーバーロードで、左辺引数として
std::
istream
&
または
std::
ostream
&
を取るものは、挿入演算子および抽出演算子として知られています。これらはユーザー定義型を右辺引数として取るため(
a @ b
の
b
)、非メンバ関数として実装されなければなりません。
std::ostream& operator<<(std::ostream& os, const T& obj) { // オブジェクトをストリームに書き込む return os; } std::istream& operator>>(std::istream& is, T& obj) { // ストリームからオブジェクトを読み込む if (/* T の構築に失敗した場合 */) is.setstate(std::ios::failbit); return is; }
これらの演算子は、時として friend関数 として実装されます。
関数呼び出し演算子
ユーザー定義クラスが関数呼び出し演算子 operator ( ) をオーバーロードすると、それは FunctionObject 型となります。
この型のオブジェクトは関数呼び出し式で使用できます:
// この型のオブジェクトは、1変数の線形関数 a * x + b を表します。 struct Linear { double a, b; double operator()(double x) const { return a * x + b; } }; int main() { Linear f{2, 1}; // 関数 2x + 1 を表します。 Linear g{-1, 0}; // 関数 -x を表します。 // f と g は関数のように使用できるオブジェクトです。 double f_0 = f(0); double f_1 = f(1); double g_0 = g(0); }
多くの標準ライブラリ アルゴリズム は動作をカスタマイズするために FunctionObject s を受け入れます。 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 { // 前置インクリメント X& operator++() { // 実際のインクリメントはここで行われる return *this; // 新しい値を参照で返す } // 後置インクリメント X operator++(int) { X old = *this; // 古い値をコピー operator++(); // 前置インクリメント return old; // 古い値を返す } // 前置デクリメント X& operator--() { // 実際のデクリメントはここで行われる return *this; // 新しい値を参照で返す } // 後置デクリメント X operator--(int) { X old = *this; // 古い値をコピー operator--(); // 前置デクリメント return old; // 古い値を返す } };
前置インクリメント演算子と前置デクリメント演算子の標準的な実装は参照を返しますが、あらゆる演算子オーバーロードと同様に、戻り値の型はユーザー定義です。例えば、 std::atomic に対するこれらの演算子のオーバーロードは値を返します。
二項算術演算子
二項演算子は通常、対称性を維持するために非メンバとして実装されます(例えば、複素数と整数を加算する場合、 operator + が複素数型のメンバ関数である場合、 complex + integer のみがコンパイルされ、 integer + complex はコンパイルされません)。すべての二項算術演算子に対して対応する複合代入演算子が存在するため、二項演算子の標準的な形式はそれらの複合代入演算子を用いて実装されます:
class X { public: X& operator+=(const X& rhs) // 複合代入(メンバーである必要はないが、 { // プライベートメンバーを変更するためによく使用される) /* rhsを*thisに加算する処理がここで行われる */ return *this; // 参照で結果を返す } // クラス内で定義されたフレンド関数はインラインであり、非ADLルックアップから隠蔽される friend X operator+(X lhs, // lhsを値渡しすることでa+b+cのような連鎖演算を最適化 const X& rhs) // それ以外の場合、両パラメータはconst参照となる { lhs += rhs; // 複合代入を再利用 return lhs; // 値で結果を返す(ムーブコンストラクタを使用) } };
比較演算子
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); // 同じ順序を維持 } };
一般的に、 operator < が提供されると、他の関係演算子は operator < を用いて実装されます。
inline bool operator< (const X& lhs, const X& rhs) { /* 実際の比較を実行 */ } 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) { /* 実際の比較を実行 */ } 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 [ ] に対してconstと非constの2つのオーバーロードを定義します:
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 [ ] は単一の添字しか受け取れません。多次元配列アクセスのセマンティクスを提供するためには、例えば3次元配列アクセス a [ i ] [ j ] [ k ] = x ; を実装する場合、 operator [ ] は2次元平面への参照を返し、その平面が独自の operator [ ] を持って1次元行への参照を返し、さらにその行が operator [ ] を持って要素への参照を返す必要があります。この複雑さを回避するため、一部のライブラリでは代わりに operator ( ) をオーバーロードし、3次元アクセス式をFortran風の構文 a ( i, j, k ) = x ; で表現することを選択しています。 |
(C++23まで) |
|
operator [ ] は任意の数の添字を受け取ることができます。例えば、3次元配列クラスの 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以降) |
ビット演算演算子
BitmaskType の要件を実装するユーザー定義クラスおよび列挙型は、ビット単位演算子 operator & 、 operator | 、 operator ^ 、 operator~ 、 operator & = 、 operator | = 、および operator ^ = をオーバーロードする必要があり、シフト演算子 operator << 、 operator >> 、 operator >>= 、および operator <<= をオプションでオーバーロードすることができます。標準的な実装は通常、前述の二項算術演算子のパターンに従います。
論理否定演算子
|
operator operator ! は、ブーリアンコンテキストで使用されることを意図したユーザー定義クラスによって一般的にオーバーロードされます。このようなクラスはまた、ブーリアン型へのユーザー定義変換関数を提供し(標準ライブラリの例については std::basic_ios を参照)、 operator ! の期待される動作は operator bool の反対の値を返すことです。 |
(C++11以前) |
|
組み込みのoperator ! は 文脈に応じた bool への変換 を実行するため、ブーリアンコンテキストで使用されることを意図したユーザー定義クラスは operator bool のみを提供すればよく、 operator ! をオーバーロードする必要はありません。 |
(C++11以降) |
稀にオーバーロードされる演算子
以下の演算子は稀にしかオーバーロードされません:
-
アドレス取得演算子、
operator
&
。単項&が不完全型の左辺値に適用され、完全型がオーバーロードされた
operator
&
を宣言している場合、演算子が組み込みの意味を持つか演算子関数が呼び出されるかは未規定です。この演算子はオーバーロード可能なため、ジェネリックライブラリではユーザー定義型のオブジェクトのアドレスを取得するために
std::addressof
を使用します。標準的なオーバーロード例として最もよく知られているのはMicrosoftのクラス
CComPtrBaseです。この演算子のEDSLでの使用例は boost.spirit で見つけることができます。 - 論理演算子、 operator && および operator || 。組み込みバージョンとは異なり、オーバーロード版では短絡評価を実装できません。 また組み込みバージョンとは異なり、左側のオペランドを右側のオペランドより前に順序付けしません。 (C++17まで) 標準ライブラリでは、これらの演算子は std::valarray に対してのみオーバーロードされています。
- コンマ演算子、 operator, 。 組み込みバージョンとは異なり、オーバーロード版では左側のオペランドを右側のオペランドより前に順序付けしません。 (C++17まで) この演算子はオーバーロード可能なため、ジェネリックライブラリではユーザー定義型の式の実行順序を制御するために a, void ( ) , b のような式を a, b の代わりに使用します。Boostライブラリでは operator, が boost.assign 、 boost.spirit およびその他のライブラリで使用されています。データベースアクセスライブラリの 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 | Applied to | Behavior as published | Correct behavior |
|---|---|---|---|
| CWG 1481 | C++98 | 非メンバー前置インクリメント演算子はクラス型、列挙型、またはそれらの型への参照型のパラメータのみを持つことができた | 型の要件なし |
| CWG 2931 | C++23 | 明示的オブジェクトメンバー演算子関数はクラス型、列挙型、またはそれらの型への参照型のパラメータのみを持つことができた | 禁止 |
関連項目
| 一般的な演算子 | ||||||
|---|---|---|---|---|---|---|
| 代入 |
インクリメント
デクリメント |
算術 | 論理 | 比較 |
メンバー
アクセス |
その他 |
|
a
=
b
|
++
a
|
+
a
|
!
a
|
a
==
b
|
a
[
...
]
|
関数呼び出し
a ( ... ) |
|
カンマ
a, b |
||||||
|
条件演算子
a ? b : c |
||||||
| 特殊演算子 | ||||||
|
static_cast
関連する型間で変換を行う
|
||||||
外部リンク
|