演算子のオーバーロード

演算子の「上書き」

演算子とは「+」や「*」などの演算(計算)を行うための記号の事です。
「+」は当たり前のように「足し算」を表す記号として使っていますが、何でもかんでも足すことができるわけではありません。


#include <iostream>

class TestClass
{
    int num;

public:
    TestClass(int x = 0) { num = x; }
    int get() { return num; }
    void set(int x) { num = x; }
};

int main()
{
    TestClass tc1(10), tc2(20);

    //エラー
    TestClass tc3 = tc1 + tc2;
}

このような自作クラスのインスタンス同士では単純に足し算をすることはできません。
メンバ変数num同士を足した結果が欲しい場合は、ゲッターでそれぞれの値を取得してから足し算をし、セッターで値を戻す、などの手順が必要です。

クラスのインスタンス同士を「+」記号で演算できれば、プログラムを簡潔に、見た目にもわかりやすく書くことができます。
そのためには演算子のオーバーロードという機能を利用します。

※いくつかを除いて大部分の演算子はオーバーロードが可能です。
全部を解説していては大変なので、ここではよく使われるであろうものをいくつか紹介します。
全部覚える必要もありません。

また、ここに示した方法が必ずしも正解というわけではありません。
クラスの機能によっては別の方法の方が扱いやすくなるかもしれません。

算術演算子のオーバーロード


#include <iostream>

class TestClass
{
    int num;

public:
    //+記号をオーバーロード
    TestClass operator +(TestClass r)
    {
        TestClass tc;
        tc.num = this->num + r.num;
        return tc;
    }
    //-記号をオーバーロード
    TestClass operator -(TestClass r)
    {
        TestClass tc;
        tc.num = this->num - r.num;
        return tc;
    }

    TestClass(int x = 0) { num = x; }
    int get() { return num; }
    void set(int x) { num = x; }
};


int main()
{
    TestClass tc1(10), tc2(20);

    TestClass tc3 = tc1 + tc2;

    std::cout << tc3.get() << std::endl; //30

    std::cin.get();
}

「TestClass operator + (TestClass r)」という箇所で+演算子をオーバーロードしています。
operator(オペレーター)というのは演算子のことです。
オペレーターで演算される値をオペランドといい、オペレーターの左側にある値を左オペランド、右側を右オペランドといいます。
単に「左辺」「右辺」と呼ばれることも多いです。

引数は自身のクラス型をひとつ指定します。
ここで仮引数に入っているのは+記号の右辺です。
(上の例では「20」が入っている「tc2」)
左辺値(tc1)は自分自身、つまり「this」が表すインスタンスです。
サンプルコードではthisを通してメンバ変数にアクセスしていますが、普通は省略します。

オーバーロードの定義内の処理自体は単純なもので、新しいインスタンスを作成し、それに目的のメンバ変数同士を足し算したものを代入し、returnで返すだけです。
メンバ変数が複数ある場合はコピーしたいだけ同じように記述します。

これで、インスタンス同士を「+」で演算した時に、メンバ変数num同士を足し算した値を持つ新しいインスタンスを得ることができるようになります。

ちなみに戻り値の型は自由に決めることができます。
もしint型で戻り値が欲しい場合は「int」と記述し、return文でもint型の値を指定するだけです。


int operator +(TestClass r)
{
	TestClass tc;
	return num + r.num;
}

サンプルコードでは「+」と「-」しか定義していませんが、「*」「/」「%」も同じように書くことができます。

なお、オーバーロードの定義はできるだけ元の記号の意味と同じものにすべきです。
例えば「+」記号をオーバーロードして割り算にすることなども可能ですが、そんなことをしたらコードの意味が理解不能になってしまいます。

引数を参照で受け取る

上記の手順で演算子のオーバーロードはできますが、この定義方法では通常の関数と同じように呼び出し時に値のコピーが行われます。
サンプルのような単純なクラスなら大した問題ではありませんが、巨大なクラスではコピーのコストはバカになりません。
そのため、引数は参照を利用して受け取るようにします。


TestClass operator +(const TestClass &r)
{
    //rは書き換えできなくなる
    //r.num = 1;  //エラー
	TestClass tc;
	tc.num = num + r.num;
	return tc;
}

引数に「&」を付けることで参照で受け取ります。
これで右辺の実体に直接アクセスできるようになり、コピーのコストが無くなります。

しかし、参照を通して値を書き換えてしまうのはマズいので、同時にconstを指定することで書き換えをできなくします。

constメンバ関数にする

さらに、演算子オーバーロードの関数内ではメンバ変数の書き換えが行われても困ります。
もちろんそういう処理を書かなければ済む話なのですが、ミスすることは誰にでもあり得ます。
そういったミスはconstメンバ関数にすれば防げます。


TestClass operator +(const TestClass &r) const
{
	//メンバ変数の書き換えができなくなる
	//num = 1;  //エラー
	TestClass tc;
	tc.num = num + r.num;
	return tc;
}

引数指定の丸括弧の後にconstと記述することで、その関数内からはメンバ変数の書き換えができなくなります。

このように、constは意図しない書き換えを防ぐには非常に有用な機能です。
constできるところは積極的にconstを書いておくことが推奨されます。

他のデータ型との演算について

実は上記のオーバーロード方法は算術演算子のオーバーロード方法としてはまだ不十分です。

オーバーロードした関数には引数がひとつしかなく、受け取れるのはオペレーターの右辺です。
そして、関数のオーバーロードが働くのは、左辺にそのクラスのインスタンスを置いた場合です。

つまり、以下のような他のデータ型との直接の演算ができません。


TestClass tc1(10);

//整数との演算ができない
TestClass tc2 = 1 + tc1;

ゲッターなどで値を取得してから普通に演算しても良いですが、他のデータ型と直接演算する方法もあります。
その方法は次ページで説明します。

代入演算子のオーバーロード

代入演算子は「=」による代入を行う演算子です。
「=」をオーバーロードすると簡便な記述でインスタンス同士のコピーが実現できます。


#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」を指定することです。

代入という処理の都合上、これは決まり文句として覚えてしまいましょう。

もちろん、コピーしたくないメンバ変数があれば意図的に省いても構いません。

コピーコンストラクタと合わせて定義しておく

コピーコンストラクタを定義した場合、同じ処理を代入演算子のオーバーロードでも記述しておきましょう。


#include <iostream>

class TestClass
{
    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);  //コピーコンストラクタ
    TestClass tc3 = tc1; //コピーコンストラクタ
    TestClass tc4;
    tc4 = tc1;           //演算子のオーバーロード

    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

    std::cin.get();
}

コピーコンストラクタによるインスタンスのコピーと、代入によるインスタンスのコピーは同じ結果になった方が良いです。
36~38行目の結果はすべて同じとなります。

複合代入演算子のオーバーロード

複合代入演算子とは「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;
    }
 
    //後置インクリメント/デクリメント
    const TestClass operator++(int)
    {
        TestClass tc = *this;
        ++(*this);
        return tc;
    }
    const TestClass operator--(int)
    {
        TestClass tc = *this;
        --(*this);
        return tc;
    }
};

前置インクリメント/デクリメントの処理は特に難しいことはありません。

オペレーターの記号だけでは前置も後置も同じですから、そのままでは見分けることができません。
そこで、引数に「int」と記述した方が後置になると決められています。
ただの「目印」に過ぎず、関数の処理内ではこの引数は使用しません。

後置インクリメント/デクリメントは「値を返した後に増減処理をする」という処理です。
そのため、増減処理の前にインスタンスをコピーしておき、それを返します。

増減処理は「*this」、つまり自分自身に対して、先ほど定義した前置インクリメント/デクリメント演算子を適用します。
できるだけまとめられる処理はまとめてしまったほうが、コードの修正時に楽になり、また修正ミスなども防ぐことができます。

添字演算子のオーバーロード

添字演算子は配列の要素へのアクセスに使用される演算子です。
クラスの内部に配列やポインタでアクセスできるメンバ変数がある場合に有効です。


#include <iostream>

class TestClass
{
    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];
    }
    int &operator [](int index)
    {
        return arr[index];
    }
};

//const付きで呼び出せる
void func(TestClass const &c)
{
    //エラー
    //c[0] = 10;

    //値の取り出しは可能
    for (int i = 0; i < 5; i++)
        std::cout << c[i] << std::endl;
}

int main()
{
    TestClass tc(5);
    for (int i = 0; i < 5; i++)
        tc[i] = i + 1;

    func(tc);

    std::cin.get();
}

添字演算子をオーバーロードする場合はconst版と非const版のふたつを用意しておきます。
そうすると引数をconstで取る関数でも使用することができます。