演算子オーバーロード
ユーザ定義型の被演算子に対して C++ の演算子をカスタマイズします。
目次 |
[編集] 構文
オーバーロードされた演算子は特別な名前を持つ関数です。
operator op
|
(1) | ||||||||
operator type
|
(2) | ||||||||
operator new operator new []
|
(3) | ||||||||
operator delete operator delete []
|
(4) | ||||||||
operator "" suffix-identifier
|
(5) | (C++11以上) | |||||||
op | - | 38 (C++20未満)40 (C++20以上) 個の演算子 + - * / % ^ & | ~ ! = < > += -= *= /= %= ^= &= |= << >> >>= <<= == != <= >= <=> (C++20以上) && || ++ -- , ->* -> ( ) [ ] co_await (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-> ( ) | 非メンバにできません | auto p = std::make_unique<S>(); p->bar() は p.operator->() を呼びます。 |
a@ | (a).operator@ (0) | operator@ (a, 0) | std::vector<int>::iterator i = v.begin(); i++ は i.operator++(0) を呼びます。 |
この表の |
ノート: ユーザ定義変換関数、ユーザ定義リテラル、確保関数および解放関数のオーバーロードについては、それぞれの項目を参照してください。
オーバーロードされた演算子は関数表記を用いて呼び出すことができます (が、組み込みの演算子はできません)。
std::string str = "Hello, "; str.operator+=("world"); // str += "world"; と同じ。 operator<<(operator<<(std::cout, str) , '\n'); // std::cout << str << '\n'; と同じ。 // (C++17 から) ただし評価順序を除きます。
[編集] 制限
- 演算子
::
(スコープ解決)、.
(メンバアクセス)、.*
(メンバポインタを通したメンバアクセス)、?:
(三項条件) はオーバーロードできません。 -
**
とか<>
とか&|
などの新しい演算子を作成することはできません。 - 演算子
&&
および||
のオーバーロードは短絡評価を失わせます。 - 演算子
->
のオーバーロードは、生のポインタを返すか、同様に演算子->
がオーバーロードされているオブジェクトを (値または参照で) 返すかの、いずれかでなければなりません。 - 演算子の優先順位、グループ化、被演算子の数を変更することはできません。
|
(C++17未満) |
[編集] 標準的な実装
言語は上記の制限以外にオーバーロードされた演算子が行う処理の内容や戻り値の型に何の制約も設けていませんが、一般的には、オーバーロードされた演算子は組み込みの演算子と可能な限り似たように振る舞うことが期待されます。 operator+ は引数を乗算するのではなく加算することが期待される、 operator= は代入を行うことが期待される、などです。 関連する演算子は同様に振る舞う (operator+ と operator+= は同じ演算を行う) ことが期待されます。 戻り値の型はその演算子を使用する式によって制限されます。 例えば a = b = c = d と書けるようにする (組み込みの演算子ではできるので) ためには、代入演算子は参照を返すようにします。
よくオーバーロードされる演算子は、以下の典型的な、標準的な形式を持ちます。[1]
[編集] 代入演算子
代入演算子 (operator=) には特別な性質があります。 詳細はコピー代入およびムーブ代入を参照してください。
標準的なコピー代入演算子は自己代入時に何も行わないこと、および lhs を参照として返すことが期待されます。
// オブジェクトは再利用可能な記憶域 (例えばヒープ確保されたバッファ mArray) を保持していると仮定します。 T& operator=(const T& other) // コピー代入 { if (this != &other) { // 期待されている自己代入チェック。 if (other.size != size) { // 記憶域は再利用できない。 delete[] mArray; // this の記憶域を破棄します。 size = 0; mArray = nullptr; // 次の行が例外を投げた場合に不変条件を維持します。 mArray = new int[other.size]; // this の記憶域を作成します。 size = other.size; } std::copy(other.mArray, other.mArray + other.size, mArray); } return *this; }
標準的なムーブ代入演算子はムーブ元オブジェクトを有効な状態 (つまりクラスの不変条件を維持している状態) とすること、および、自己代入時に何もしないかオブジェクトが少なくとも有効な状態とすること、 lhs を非 const 参照として返すこと、および noexcept であることが期待されます。
T& operator=(T&& other) noexcept // ムーブ代入 { if(this != &other) { // 自己代入時に何もしません。 delete[] mArray; // this の記憶域を削除します。 mArray = std::exchange(other.mArray, nullptr); // ムーブ元オブジェクトを有効な状態とします。 size = std::exchange(other.size, 0); } return *this; }
コピー代入がリソース再利用の利点を活かせない (ヒープ確保された配列を管理しておらず、そういったことを行う std::vector や std::string などのメンバを (推移的にも) 保持していない) 状況では、人気のある便利な略記法があります。 引数を値で取り (そのため引数の値カテゴリに依存してコピーおよびムーブの両方として動作します)、引数をスワップし、デストラクタにクリーンアップを任せる、コピー&スワップ代入演算子です。
この形式は強い例外保証を自動的に提供しますが、リソースの再利用はできなくなります。
[編集] ストリーム抽出/挿入演算子
左側の引数として std::istream& または std::ostream& を取る operator>>
および operator<<
のオーバーロードは挿入演算子および抽出演算子と呼ばれます。 これらはユーザ定義型を右側の引数 (a@b の b
) として取るため、非メンバとして実装しなければなりません。
std::ostream& operator<<(std::ostream& os, const T& obj) { // ここでストリームに obj を書き込みます。 return os; } std::istream& operator>>(std::istream& is, T& obj) { // ここでストリームから obj を読み込みます。 if( /* T が構築できなかった */ ) is.setstate(std::ios::failbit); return is; }
これらの演算子は時々フレンド関数として実装されます。
[編集] 関数呼び出し演算子
ユーザ定義クラスが関数呼び出し演算子 operator() をオーバーロードすると、それは FunctionObject 型になります。 std::sort から std::accumulate まで、多くの標準アルゴリズムが動作をカスタマイズするためにそういった型のオブジェクトを受け取ります。 operator() に特筆すべき標準的な形式はありませんが、使い方の一例を挙げます。
struct Sum { int sum; Sum() : sum(0) { } void operator()(int n) { sum += n; } }; Sum s = std::for_each(v.begin(), v.end(), Sum());
[編集] インクリメントおよびデクリメント
式に後置インクリメントおよびデクリメントが現れると、対応するユーザ定義関数 (operator++ または operator--) が呼ばれ、整数引数 0
が渡されます。 これは一般的には T operator++(int) として実装され、引数は無視されます。 後置インクリメントおよびデクリメント演算子は、通常、前置バージョンを用いて実装されます。
struct X { X& operator++() { // ここで実際のインクリメント行います。 return *this; } X operator++(int) { X tmp(*this); // コピー。 operator++(); // 前置インクリメント。 return tmp; // 古い値を返します。 } };
標準的な形式の前置インクリメントおよびデクリメントは参照を返しますが、あらゆる演算子オーバーロード同様、戻り値の型はユーザ定義です。 例えば std::atomic に対するこれらの演算子のオーバーロードは値で返します。
[編集] 二項算術演算子
二項演算子は一般的には対称性を維持するため非メンバとして実装されます (例えば、複素数と整数を加算する場合、 operator+
が複素数型のメンバ関数であれば、複素数+整数のみがコンパイルでき、整数+複素数はコンパイルできないでしょう)。 すべての二項算術演算子について、対応する複合代入演算子が存在するため、標準的な形式の二項演算子はその対応する複合代入を用いて実装されます。
class X { public: X& operator+=(const X& rhs) // 複合代入 (メンバである必要はないが、 { // プライベートメンバを変更するためにメンバであることが多い)。 /* ここで *this への rhs の加算を行います。 */ 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<=> が定義されている場合は、6つの関係演算子すべてが、また、デフォルト化されていれば三方比較演算子自体も、コンパイラによって自動的に生成されます。 struct Record { std::string name; unsigned int floor; double weight; auto operator<=>(const Record&) = default; }; // この時点で Record は ==, !=, <, <=, >, >= を用いて比較できます。 詳細についてはデフォルト比較を参照してください。 |
(C++20以上) |
[編集] 配列添字演算子
読み込みと書き込みの両方を可能とする配列ライクなアクセスを提供するユーザ定義クラスは、一般的に 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]; } };
値の型が組み込み型であると判っている場合は、 const 版は値で返すべきです。
コンテナの要素への直接アクセスが望ましくないか、可能でないか、左辺値 c[i] = v; と右辺値 v = c[i]; の用途を区別している場合、 operator[] はプロキシを返しても構いません。 例えば std::bitset::operator[] を参照してください。
多次元配列アクセスの意味論を提供するため、例えば 3D 配列アクセス a[i][j][k] = x; を実装するためには、 operator[] は 2D 平面への参照を返す必要があり、それは 1D 行への参照を返す独自の operator[] を持つ必要があり、それは要素への参照を返す operator[] を持つ必要があります。 この複雑さを避けるため、代わりに 3D アクセスの式が Fortran ライクな構文 a(i, j, k) = x; を持つように、 operator() のオーバーロードを選ぶライブラリもあります。
[編集] ビット単位の算術演算子
BitmaskType の要件を実装するユーザ定義クラスおよび列挙は、ビット単位の算術演算子 operator&、 operator|、 operator^、 operator~、 operator&=、 operator|=、 operator^= をオーバーロードすることが要求され、オプションでシフト演算子 operator<<、 operator>>、 operator>>=、 operator<<= をオーバーロードすることもあります。 標準的な実装は、通常、上で説明した二項算術演算子のパターンに従います。
[編集] ブーリアン否定演算子
演算子 operator! はブーリアンの文脈で使用することを意図しているユーザ定義クラスによってよくオーバーロードされます。 そのようなクラスはユーザ定義変換関数 explicit operator bool() も提供しており (標準ライブラリの例は std::basic_ios を参照してください)、 operator! の期待される動作は operator bool と逆の値を返すことです。
[編集] 稀にしかオーバーロードされない演算子
以下の演算子は稀にしかオーバーロードされません。
- アドレス取得演算子 operator&。 単項 & が不完全型の左辺値に適用され、その完全型がオーバーロードされた operator& を宣言している場合、動作は未定義です (C++11未満) その演算子が組み込みの意味を持つか演算子関数が呼ばれるかは未規定です (C++11以上)。 この演算子がオーバーロードされる可能性があるため、汎用的なライブラリはユーザ定義型のオブジェクトのアドレスを取得するために std::addressof を使用します。 標準的なオーバーロードされた operator& の最も良く知られている例は Microsoft の CComPtr クラスです。 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 で使用される方が一般的です。
[編集] 例
#include <iostream> class Fraction { int gcd(int a, int b) { return b == 0 ? a : gcd(b, a % b); } int n, d; public: Fraction(int n, int d = 1) : n(n/gcd(n, d)), d(d/gcd(n, d)) { } int num() const { return n; } int den() const { return d; } 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() ; } bool operator==(const Fraction& lhs, const Fraction& rhs) { return lhs.num() == rhs.num() && lhs.den() == rhs.den(); } bool operator!=(const Fraction& lhs, const Fraction& rhs) { return !(lhs == rhs); } Fraction operator*(Fraction lhs, const Fraction& rhs) { return lhs *= rhs; } int main() { 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'; }
出力:
3/8 * 1/2 = 3/16 1/2 * 5/1 = 5/2 2 * 3/8 = 3/4
[編集] 欠陥報告
以下の動作変更欠陥報告は以前に発行された C++ 標準に遡って適用されました。
DR | 適用先 | 発行時の動作 | 正しい動作 |
---|---|---|---|
CWG 1458 | C++11 | taking address of incomplete type that overloads address-of was undefined behavior | the behavior is only unspecified |
[編集] 関連項目
一般的な演算子 | ||||||
---|---|---|---|---|---|---|
代入 | インクリメント デクリメント |
算術 | 論理 | 比較 | メンバアクセス | その他 |
a = b |
++a |
+a |
!a |
a == b |
a[b] |
a(...) |
特殊な演算子 | ||||||
static_cast は型を別の関連する型に変換します。 |
[編集] 参考文献
- ↑ StackOverflow C++ FAQ の Operator Overloading