演算子のオーバーロード
演算子の「上書き」
演算子とは+
や*
などの演算(計算)を行うための記号の事です。
+
演算子は当たり前のように「足し算」として使用していますが、どのようなデータでも加算処理ができるわけではありません。
#include <iostream>
class TestClass
{
int num;
public:
TestClass(int n = 0)
:num(n) {};
int get() { return num; }
void set(int n) { num = n; }
};
int main()
{
TestClass tc1(10), tc2(20);
//エラー
TestClass tc3 = tc1 + tc2;
}
このような自作クラスのインスタンス同士では足し算をすることはできません。
メンバ変数num
同士を足した結果が欲しい場合は、ゲッターでそれぞれの値を取得してから加算し、セッターで値を戻す、といった手順が必要です。
クラスのインスタンス同士を+
記号で演算できれば、プログラムを簡潔に、見た目にもわかりやすく書くことができます。
そのためには演算子のオーバーロードという機能を利用します。
いくつかを除いて大部分の演算子はオーバーロードが可能です。
全部を解説していては大変なので、ここではよく使われるであろうものをいくつか紹介します。
全部覚える必要もありません。
また、ここに示した方法が必ずしも正解というわけではありません。
クラスの機能によっては別の方法の方が扱いやすくなるかもしれません。
算術演算子のオーバーロード
#include <iostream>
class TestClass
{
int num;
public:
//+演算子をオーバーロード
TestClass operator +(const TestClass &r) const
{
TestClass tc;
tc.num = this->num + r.num;
return tc;
}
//-演算子をオーバーロード
TestClass operator -(const TestClass &r) const
{
TestClass tc;
tc.num = this->num - r.num;
return tc;
}
TestClass(int n = 0)
:num(n) {};
int get() { return num; }
void set(int n) { num = n; }
};
int main()
{
TestClass tc1(10), tc2(20);
TestClass tc3 = tc1 + tc2;
std::cout << tc3.get() << std::endl; //30
std::cin.get();
}
operator
というキーワードが付けられている関数が演算子のオーバーロードの定義です。
//+演算子をオーバーロード
TestClass operator +(const TestClass &r) const
{}
最初のTestClass
は戻り値の型です。
operator
(オペレーター)というのは演算子のことで、オーバーロードする演算子を続けて記述します。
(スペースがあっても良い)
ちなみにオペレーターで演算される値をオペランド(被演算子)といい、オペレーターの左側にある値を左オペランド、右側を右オペランドといいます。
単に「左辺」「右辺」と呼ばれることも多いです。
引数は自身のクラス型をひとつ指定します。
ここで仮引数に入るのは+
演算子の右辺です。
(上の例では「20」が入っているtc2
)
左辺(tc1
)は自分自身、つまりthis
が指すインスタンスです。
サンプルコードではthis
を通してメンバ変数にアクセスしていますが、これは省略可能です。
オーバーロードの定義内の処理自体は単純です。
新しいインスタンスを作成し、それに目的のメンバ変数同士を足し算したものを代入し、return
で返しています。
メンバ変数が複数ある場合はコピーしたいだけ同じように記述します。
これで、インスタンス同士を+
で演算した時に、メンバ変数num
同士を足し算した値を持つ新しいインスタンスを得ることができるようになります。
ちなみに戻り値の型は自由に決めることができます。
もしint型で戻り値が欲しい場合はint
と記述し、return文にもint型の値を指定するだけです。
int operator +(const TestClass r) const
{
TestClass tc;
return num + r.num;
}
サンプルコードでは+
と-
しか定義していませんが、
*
、/
、%
演算子も同じように書くことができます。
なお、オーバーロードの定義はできるだけ元の記号の意味と同じものにすべきです。
例えば+
演算子をオーバーロードして割り算にすることなども可能ですが、そんなことをしたらコードの意味が理解不能になってしまいます。
引数を参照とconstにする
引数は参照で受け取るようにしておきます。
参照にしなくても演算子のオーバーロードは可能ですが、その場合は演算のたびにコピーが行われることになります。
サンプルコードのような単純なクラスでは大した問題にはなりませんが、クラスのサイズが大きくなると無視できないコストになります。
また、算術演算子は左辺も右辺は書き換えませんから、引数にもconst
を指定しておきます。
const
が無くてもやはり演算子のオーバーロードは可能ですが、その場合は定数の演算ができなくなってしまいます。
特に理由がなければconst
を指定しておくべきです。
class TestClass
{
int num;
public:
TestClass(int n = 0) :num(n) {};
//引数にconstの指定がない
TestClass operator +(TestClass& r) const
{
TestClass tc;
tc.num = this->num + r.num;
return tc;
}
};
int main()
{
TestClass tc1(10);
const TestClass tc2(20);
//右辺にconst定数を指定できない
//TestClass tc3 = tc1 + tc2;
}
constメンバ関数にする
算術演算子のオーバーロードではメンバ変数の書き換えは行わない(行うべきではない)ので、constメンバ関数にしておきます。
関数をconst化しないと、左辺に定数を指定できなくなります。
class TestClass
{
int num;
public:
TestClass(int n = 0) :num(n) {};
//非const関数
TestClass operator +(const TestClass& r)
{
TestClass tc;
tc.num = this->num + r.num;
return tc;
}
};
int main()
{
const TestClass tc1(10);
TestClass tc2(20);
//左辺にconst定数を指定できない
//TestClass tc3 = tc1 + tc2;
}
他のデータ型との演算について
算術演算子のオーバーロードは引数がひとつしかなく、引数で受け取れるのはオペレーターの右辺です。
演算子のオーバーロードが働くのは左辺にそのクラスのインスタンスを置いた場合です。
つまりこの方法で可能なのは、左辺に演算子オーバーロードをしたクラスのインスタンス、右辺にその他のデータ型、というパターンの演算です。
その逆の、左辺にその他のデータ型、右辺にクラスインスタンス、というパターンでは演算ができません。
class TestClass
{
int num;
public:
TestClass(int n = 0) :num(n) {};
//int型との+演算子のオーバーロード
TestClass operator +(const int& r) const
{
TestClass tc;
tc.num = this->num + r;
return tc;
}
};
int main()
{
TestClass tc(10);
int n = 20;
TestClass tc2 = tc + n; //OK
//TestClass tc3 = n + tc; //NG
}
ゲッターなどで値を取得してから普通に演算しても良いですが、直接の演算を可能にする方法もあります。
それは次ページで説明します。
代入演算子のオーバーロード
代入演算子は=
による代入を行う演算子です。
これをオーバーロードすると簡便な記述でインスタンス同士のコピーが実現できます。
#include <iostream>
class TestClass
{
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;
std::cin.get();
}
算術演算子のオーバーロードとほとんど同じですが、注意点が二つあります。
ひとつは、戻り値の型は必ず自分自身のクラスの参照を返すことです。
そして、return文には*this
を指定することです。
代入という処理の都合上、これは決まり文句として覚えてしまいましょう。
ポインタなどのコピーしたくないメンバ変数があれば意図的に省くこともできます。
なお、メンバ変数を書き換えるのでconstメンバ関数にはできません。
コピーコンストラクタと合わせて定義しておく
コピーコンストラクタを定義した場合、同じ処理を代入演算子のオーバーロードでも記述しておきましょう。
この二つの処理が異なると混乱の元となります。
#include <iostream>
//このクラスのコピー処理では
//ポインタメンバ変数はコピーしない
class TestClass
{
int number;
int* pointer;
public:
TestClass(int num = 0, int* p = nullptr)
:number(num), pointer(p) {};
//コピーコンストラクタ
TestClass(const TestClass& tc)
{
number = tc.number;
pointer = nullptr;
}
//代入演算子のオーバーロード
TestClass& operator =(const TestClass& r)
{
number = r.number;
pointer = nullptr;
return *this;
}
void show() const
{
std::cout << "number: " << number;
std::cout << " / pointer: " << pointer << std::endl;
}
};
int main()
{
int num = 10;
TestClass tc1(num, &num);
TestClass tc2(tc1); //コピーコンストラクタ
TestClass tc3 = tc1; //コピーコンストラクタ
TestClass tc4;
tc4 = tc1; //演算子のオーバーロードによるコピー
tc1.show();
tc2.show();
tc3.show();
tc4.show();
std::cin.get();
}
number: 10 / pointer: 004FFA5C number: 10 / pointer: 00000000 number: 10 / pointer: 00000000 number: 10 / pointer: 00000000
デフォルトの代入演算子の削除
代入演算子はクラスを定義すると自動的に作成されます。
処理はデフォルトコピーコンストラクタと同じく、全てのメンバ変数のコピーが行われます。
この自動生成はdeleteキーワードで削除することができます。
class TestClass
{
public:
//コピーコンストラクタの削除
TestClass(const TestClass&) = delete;
//代入演算子のオーバーロードの削除
TestClass& operator =(const TestClass&) = delete;
};
default/delete宣言と自動生成関数の生成ルールも参照してください。
複合代入演算子のオーバーロード
複合代入演算子はa += b
という記述で行う演算です。
class TestClass
{
int num;
public:
//+=演算子をオーバーロード
TestClass &operator +=(const TestClass &r)
{
num += r.num;
return *this;
}
TestClass &operator +=(int r)
{
num += r;
return *this;
}
};
算術演算子のオーバーロードと代入演算子のオーバーロードを組み合わせたような記述となります。
メンバ変数を書き換えるのでconstメンバ関数にはできません。
複合代入演算子は、引数に自身のインスタンスを受け取るものと、目的の型(例ではint)を受け取るものとのふたつを定義しておけば、以下のように両方との演算が可能になります。
TestClass tc1(10);
TestClass tc2(20);
tc += tc2; //インスタンス同士の演算
tc += 30; //インスタンスとint型との演算
比較演算子のオーバーロード
比較演算子は以下のようにオーバーロードします。
#include <iostream>
class TestClass
{
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);
//全部true
std::cout << ((tc1 == tc2) ? "true" : "false") << std::endl;
std::cout << ((tc1 != tc3) ? "true" : "false") << std::endl;
std::cout << ((tc1 < tc3) ? "true" : "false") << std::endl;
std::cin.get();
}
比較の結果はbool型(true
/false
)を返します。
!=
演算子(等しくない)については、すでにオーバーロードした==
演算子(等しい)を利用します。
==
演算子での演算結果を!
演算子(否定)で反転すれば、正しい結果を得られます。
他のデータ型との比較について
比較演算子も、算術演算子と同じく引数をひとつしか受け取れないという制限があります。
(数値などと直接比較できない)
これの解決方法は算術演算子の場合と同じなので、次ページで解説します。
単項+、単項-演算子のオーバーロード
単項+、単項-演算子とは、値の前に単独でつける+
、-
の記号の事です。
int numA = 10;
int numB = +numA; //単項+演算子
int numC = -numA; //単項-演算子
単項+演算子はほとんど使われませんが、単項-演算子はプラスマイナスの反転時に使います。
class TestClass
{
int num;
public:
//単項演算子のオーバーロード
TestClass operator+() const
{
return *this;
}
TestClass operator-() const
{
TestClass tc;
tc.num = -num;
return tc;
}
};
単項+の方は特に何もせず、自分自身を返します。
単項-の方は、新しいインスタンスを生成し、目的のメンバ変数に-演算子を適用してから返します。
インクリメント/デクリメント演算子のオーバーロード
インクリメント/デクリメント演算子には「前置」と「後置」の二種類があるのでそれらも全てオーバーロードします。
class TestClass
{
int num;
public:
//前置インクリメント/デクリメント
TestClass& operator++()
{
++num;
return *this;
}
TestClass& operator--()
{
--num;
return *this;
}
//後置インクリメント/デクリメント
TestClass operator++(int)
{
TestClass tc = *this;
++(*this);
return tc;
}
TestClass operator--(int)
{
TestClass tc = *this;
--(*this);
return tc;
}
};
前置インクリメント/デクリメントの処理は特に難しいことはないと思います。
自分自身の参照を返すようにすれば無駄なコピーが発生しません。
オペレーターの記号は前置も後置も同じですから、そのままでは見分けることができません。
これは引数にint
と記述した方が後置になると決められています。
このintはただの目印で、関数の処理内ではこの引数は使用しません。
(int型以外のデータ型に対する演算でも「int」を指定すると決められている)
後置インクリメント/デクリメントは「値を返した後に増減処理をする」という処理です。
なので増減処理の前にインスタンスをコピーしておき、それを返します。
増減処理は*this
、つまり自分自身に対して、先ほど定義した前置インクリメント/デクリメント演算子を適用します。
再利用できる処理はできるだけ利用したほうがコードの修正時に楽になり、修正ミスなども防ぐことができます。
添字演算子のオーバーロード
添字演算子は配列の要素へのアクセスに使用される演算子です。
クラスの内部に配列やポインタでアクセスできるメンバ変数がある場合に有効です。
#include <iostream>
class TestClass
{
int *arr;
public:
TestClass(size_t size)
{
if (size < 0)
size = 0;
arr = new int[size];
}
~TestClass()
{
delete[] arr;
}
//添字演算子をオーバーロード
//const版と通常版のふたつを定義しておく
int const &operator [](size_t index) const
{
return arr[index];
}
int &operator [](size_t index)
{
return arr[index];
}
};
//const付きで呼び出せる
void func(TestClass const &c)
{
//値の書き換えはコンパイルエラー
//c[0] = 10;
//値の取り出しは可能
for (size_t i = 0; i < 5; i++)
std::cout << c[i] << std::endl;
}
int main()
{
TestClass tc(5);
for (size_t i = 0; i < 5; i++)
tc[i] = i + 1;
func(tc);
std::cin.get();
}
添字演算子をオーバーロードする場合はconst版と非const版のふたつを用意しておきます。
こうすることで引数をconstで取る関数でも使用することができます。
ユーザ定義変換
演算子のオーバーロードとは少し異なりますが、同じoperator
というキーワードを使用するものにユーザ定義変換があります。
これは変換コンストラクタの反対の働きに近く、クラスのインスタンスを別のデータ型に変換します。
#include <string>
class TestClass
{
int n;
std::string s;
public:
TestClass(int n = 0, std::string s = "")
: n(n), s(s) {};
//ユーザ定義変換
operator int() const {
return n;
}
//ユーザ定義変換(明示的な呼び出しのみ有効、C++11以降)
explicit operator std::string() const {
return s;
}
};
int main()
{
TestClass tc(10, "abc");
//ユーザ定義変換でint型に変換
int num = tc;
//explicitなのでstring型への暗黙的な変換は許可されない
//std::string s = tc;
//ユーザ定義変換の明示的な呼び出し
std::string s = static_cast<std::string>(tc);
//"abc"
std::string s2 = (std::string)tc; //←これでも一応OK
}
ユーザ定義変換の定義では目的のデータ型を記述しますが、戻り値と引数は指定しません。
const関数にしておけば、constインスタンス変数からの変換も可能です。
C++11以降では、ユーザ定義変換にexplicit
キーワードを付けて暗黙的な呼び出しを禁止することができます。
明示的に呼び出すには目的のデータ型にキャストします。
(→static_cast)
なお、文法上の制限として角括弧[]
は型の定義部分に記述できないため、配列型への変換はできません。
配列のポインタ型への変換はtypedef
やusingで別名を与えることで可能です。
#include <iostream>
class TestClass
{
//int[3]型の別名
using arr_t = int[3];
arr_t arr = { 1, 2, 3 };
public:
//int[3]のポインタへのユーザー定義変換
operator arr_t* () {
return &arr;
}
//const版
operator const arr_t* () const {
return &arr;
}
};
int main()
{
TestClass tc;
int (*arr)[] = tc;
std::cout << (*arr)[0] << std::endl;
//1
}
ユーザ定義変換は変換コンストラクタに比べてそれほど便利な機能とは言えません。
インスタンスから任意の値が欲しければ通常のメンバ関数を実装するだけで十分足りますし、そのほうが意図しない変換が発生することもないからです。
もし使用するならばexplicit
を付けて明示的なユーザ定義変換の呼び出しのみを許可したほうが良いでしょう。