ホームに戻る
出典 :
演算子のオーバーロード 演算子のオーバーロード(C++) - 超初心者向けプログラミング入門 非クラス関数による演算子オーバーロード(C++) - 超初心者向けプログラミング入門 演算子オーバーロード | Programming Place Plus C++編【言語解説】 第19章
関連 :
関数オーバーロードとデフォルト引数 コピーコンストラクタ const修飾の使い方
目次 :

演算子オーバーロードとは

演算子(operator)の役割を設定すること。これにより、自作クラスに対する演算が可能となる。
また、既に定義されている演算子の役割を上書きすることも可能である。
class TestClass { private: int num; public: // コンストラクタ TestClass(int x = 0) { num = x; } // getter int get() { return num; } // setter void set(int x) { num = x; } }; int main() { TestClass tc1(10), tc2(20); // tc1 と tc2 の加算を行おうとしているが… // 未定義の動作のためエラーとなる // TestClass tc3 = tc1 + tc2; }
上記のコードでは、TestClass 同士の「加算」を行おうとしているが、クラス(および構造体)に対する + 演算は定義されていないためエラーとなる。
しかし、+ 演算子の挙動を定義(オーバーロード)することで、「加算」を行うことが可能となる。

復習 : 演算子とオペランド

画像
x + 10の演算(加算)においては、+ が演算の内容を決める「演算子」である。
x および 10 は演算の対象となる「オペランド」である。
なおオペランドのうち、演算子の左にあるものを「左オペランド( = 左辺項)」、右にあるものを「右オペランド( = 右辺項)」と呼ぶ。

演算子オーバーロードの前提

演算子の機能は関数(演算子関数)として実装する。即ち、演算子オーバーロードは関数オーバーロードの一部であると言える。
演算子関数には任意の処理を実装できるため、例えば - 演算子に「ファイルへの書き込みを行う」といった機能を持たせることも可能ではある。
しかし、演算子の一般的なイメージから外れる機能を実装することは、混乱の元となるため避けなければならない

二項演算子のオーバーロード

算術演算子

サンプルコード(未完成)
#include <iostream> class TestClass { private: int num; public: // コンストラクタ、getter、setter TestClass(int x = 0) { num = x; } int get() { return num; } void set(int x) { num = x; } // + 演算子をオーバーロード TestClass operator +(TestClass r) { TestClass tc; tc.num = this->num + r.num; //< this-> は省略可 return tc; } // - 演算子をオーバーロード TestClass operator -(TestClass r) { TestClass tc; tc.num = this->num - r.num; //< this-> は省略可 return tc; } }; int main() { TestClass tc1(10), tc2(20); TestClass tc3 = tc1 + tc2; TestClass tc4 = tc2 + tc1; std::cout << tc3.get() << std::endl; //< 30 std::cout << tc4.get() << std::endl; //< 10 }
上記コードでは TestClass において + 演算子および - 演算子のオーバーロードを行っている。
( operator 修飾により演算子関数であることを明示する必要がある。)
二項演算子の演算子関数における this は左オペランドを指し、右オペランドは引数として受け取る。
(tc1 + tc2では this は tc1 を表し、引数に tc2 が入る。)
これにより、TestClass 同士の + 演算、- 演算が可能となる。
なお、引数および戻り値を TestClass 以外の型とすることも可能である。

より実用的な実装

前節のコードでTestClass の加減算は実現できているが、引数は TestClass をそのまま受け取っているため、
クラスが大きくなればなるほど引数のコピーに伴うオーバーヘッドが大きくなる。
このため、以下のように改良を施す。
サンプルコード(完成・抜粋)
class TestClass { private: int num; public: (略) // + 演算子をオーバーロード // // 引数を const 修飾 // │ // │ 参照仮引数 // │ │ // │ │ 関数を const 修飾 // ↓ ↓ ↓ TestClass operator +(const TestClass& r) const { TestClass tc; tc.num = this->num + r.num; //< this-> は省略可 return tc; } (略) };
右オペランドを値ではなく参照で受け取ることで、コピーのコストがゼロとなる。
その際、受け取った右オペランドを書き換えることがないよう引数を const 修飾する。
また、関数を const 修飾する( const メンバ関数)ことで、this のメンバ変数を書き換えられなくする。
これにより、安全で低コストな実装となる。const修飾の使い方も併せて参照のこと。

注意が必要な点

演算子の左辺が this となる都合上、上記のような実装では左辺に他のデータ型を置くことができない。
TestClass tc1(10); // 整数との演算ができない(エラー) // TestClass tc2 = 1 + tc1;
解決法は後述。

代入演算子(コピー代入演算子)

代入演算子( = )をオーバーロードすることで、インスタンス同士のコピーを簡便に行うことができる。
#include <iostream> class TestClass { private: int num; public: // 代入演算子のオーバーロード TestClass& operator =(const TestClass& r) { num = r.num; return *this; } TestClass(int x = 0) { num = x; } int get() { return num; } void set(int x) { num = x; } }; int main() { TestClass tc1(10); TestClass tc2 = tc1; std::cout << tc2.get() << std::endl; //< 10 }
重要な点として以下が挙げられる。代入演算の性質上、左辺のインスタンスが書き換えられる。

コピーコンストラクタとの関係

用途が異なることから、代入演算子オーバーロードはコピーコンストラクタの代替とはならない。
このため、どちらが呼ばれてもよいよう代入演算子とコピーコンストラクタの両方を用意し、どちらにも同じ処理を記述する。
#include <iostream> class TestClass { private: int* pointer; public: TestClass(int* p = 0) { pointer = p; } // 代入演算子オーバーロード TestClass& operator =(const TestClass& r) { pointer = 0; return *this; } // コピーコンストラクタ TestClass(const TestClass &c) { pointer = 0; } int *getPointer() { return pointer; } void setPointer(int *p) { pointer = p; } }; int main() { int num = 10; TestClass tc1(&num); TestClass tc2(tc1); //< (1)初期化 TestClass tc3 = tc1; //< (2)初期化 TestClass tc4; tc4 = tc1; //< (3)代入 std::cout << tc1.getPointer() << std::endl; //< (numのアドレスを表示) std::cout << tc2.getPointer() << std::endl; //< 0 std::cout << tc3.getPointer() << std::endl; //< 0 std::cout << tc4.getPointer() << std::endl; //< 0 }
上記のコードにおいてはコピーコンストラクタと代入演算子の両方が呼ばれ、どちらも同じ処理を行っている。
((2) は = 演算子を用いているが、代入ではなく初期化である点に注意。コピーコンストラクタが呼ばれる。)

複合代入演算子

class TestClass { private: int num; public: // += 演算子オーバーロード (右辺 : TestClass) TestClass& operator +=(const TestClass& r) { num += r.num; return *this; } // += 演算子オーバーロード (右辺 : int) TestClass& operator +=(int r) { num += r; return *this; } };
基本的には代入演算子と同様。

比較演算子

#include <iostream> class TestClass { private: int num; public: TestClass(int n = 0) { num = n; } // == 演算子 bool operator ==(const TestClass& r) const { return num == r.num; } // != 演算子 bool operator !=(const TestClass& r) const { return !(*this == r); } // < 演算子 bool operator <(const TestClass& r) const { return num < r.num; } }; int main() { TestClass tc1(10); TestClass tc2(10); TestClass tc3(20); std::cout << ((tc1 == tc2) ? "true" : "false") << std::endl; //< true std::cout << ((tc1 != tc3) ? "true" : "false") << std::endl; //< true std::cout << ((tc1 < tc3) ? "true" : "false") << std::endl; //< true // std::cout << ((tc1 > tc3) ? "true" : "false") << std::endl; //< > が定義されていないためエラー }
比較結果は bool 型を返す。
!= は == の否定となるので、== を定義しておけばそれを利用できる。
なお、上記では < を定義しているが > は定義していない。

単項演算子のオーバーロード

単項 + / -

数値に対して用いた場合 + はそのまま、- は正負を反転させる、これらのオーバーロード例を示す。
二項演算子と異なり、引数を取らない点に注意。
class TestClass { private: int num; public: // 単項 + 演算子 TestClass operator +() const { return *this; } // 単項 - 演算子 TestClass operator -() const { TestClass tc; tc.num = -num; return tc; } };

インクリメント( ++ ) / デクリメント( -- )

オペランドの前に置く前置と、オペランドの後に置く後置とで、定義の仕方が異なる。
class TestClass { private: int num; public: // 前置 ++ TestClass operator ++() { ++num; return *this; } // 前置 -- TestClass operator --() { --num; return *this; } // 後置 ++ const TestClass operator ++(int) { TestClass tc = *this; ++(*this); return tc; } // 後置 -- const TestClass operator --(int) { TestClass tc = *this; --(*this); return tc; } };
引数に int と記述されたものが後置になると定められている。これは単なる目印に過ぎず、引数としては扱われない。
組み込み型に対して行った場合、前置は「増減を行った後に値を返す」、後置は「増減を行う前に値を返す」であるため、
それに倣った実装としている。

添字演算子

配列の要素へのアクセスに使用する [] が添字演算子である。クラス内に配列やコンテナを含む場合に、添字演算子オーバーロードが有用である。
#include <iostream> class TestClass { private: int *arr; public: TestClass(int size) { if (size < 0) { size = 0; } arr = new int[size]; } ~TestClass() { delete[] arr; } // 添字演算子オーバーロード (const) int const& operator [](int index) const { return arr[index]; } // 添字演算子オーバーロード (非const) int& operator [](int index) { return arr[index]; } }; void func(TestClass const &c) { // c は const のため書き換え不可 // c[0] = 10; // 値の取り出しは可能 // (const) for (int i = 0; i < 5; i++) { std::cout << c[i] << std::endl; } } int main() { TestClass tc(5); // tc に値をセット // (非const) for (int i = 0; i < 5; i++) { tc[i] = i + 1; } func(tc); }
添字演算子をオーバーロードする際は const 版と非 const 版の両方を用意しておくことで、インスタンスが const 修飾された場合でも使用できる。

クラス外での演算子オーバーロード

上述したように、クラス内で演算子オーバーロードを行った場合は二項演算の左辺にクラスインスタンス以外を取ることができないなどの問題があった。
(左辺は必ず自分自身となり、引数を一つしか取れないため。)
クラス外でオーバーロードを行うことで、この問題に対処できる。
class TestClass { private: int num; public: explicit TestClass(int n = 0) { num = n; } TestClass(const TestClass& n) { num = n.get(); } int get() const { return num; } // 加算代入演算子 TestClass& operator +=(const TestClass& r) { num += r.num; return *this; } TestClass& operator +=(int r) { num += r; return *this; } }; // 加算演算子(クラス外で定義) // TestClass + TestClass const TestClass operator +(const TestClass& l, const TestClass& r) { return TestClass(l) += r; } // TestClass + int const TestClass operator +(const TestClass& l, int r) { return TestClass(l) += r; } // int + TestClass const TestClass operator +(int l, const TestClass& r) { return TestClass(l) += r; }
クラス外で演算子関数を定義すると、クラス内で定義した場合と異なり、両辺のオペランドを引数として受け取ることができる。
ここで、加算対象となるメンバ変数 num は private であり、全てをクラス外で定義してしまうと num へのアクセスができない。
このため、クラス内で定義した加算代入演算子を num へのアクセス経路として用いている。

上記の実装は加算演算子を定義するのに加算代入演算子を用いるという手順を踏んでおり、些か煩雑である。
フレンド関数を用いることで簡潔にするというアプローチも存在するが、ここでは詳細は割愛する。