非メンバ関数による演算子オーバーロード

演算子のオーバーロードの項では、演算子のオーバーロードについて一通り説明しました。
しかし、他のデータ型との演算を行いたい場合はもう少しコードを追加する必要があります。


class TestClass
{
    int num;
public:
    explicit TestClass(int n = 0)
        :num(n) {}

    //+演算子のオーバーロード
    //これが呼ばれるのは
    //左辺、右辺の両方がこのクラスのインスタンスのとき
    TestClass operator +(const TestClass& r) const
    {
        TestClass tc;
        tc.num = num + r.num;
        return tc;
    }

    //これが呼ばれるのは
    //左辺がインスタンス、右辺がint型のとき
    TestClass operator +(int r) const
    {
        TestClass tc;
        tc.num = num + r;
        return tc;
    }

    //左辺がint型、右辺がインスタンスのときに呼ばれる
    //+演算子のオーバーロードはここには書けない
};

int main()
{
    TestClass tc1(10);

    //コンパイルエラー
    //左辺がint型だと演算ができない
    TestClass tc2 = 1 + tc1;
}

例えばクラスのインスタンスとint型との演算は、「インスタンス + インスタンス」「インスタンス + int型」「int型 + インスタンス」の三種類があります。
クラスのメンバ関数で演算子をオーバーロードする場合、引数はひとつしか受け取ることができません。
この引数には演算の右辺が渡されます。

つまり「int型 + インスタンス」はそのままでは定義することができません。
これを定義するにはクラス内ではなくクラス外関数としてオーバーロードを定義する必要があります。

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

複合代入演算子

まずは複合代入演算子をオーバーロードします。
これは演算子のオーバーロードでの方法と同じで構いません。


class TestClass
{
    int num;

public:
	explicit TestClass(int n = 0)
        :num(n) {}

    //+=演算子をオーバーロード
    TestClass &operator +=(const TestClass &r)
    {
        num += r.num;
        return *this;
    }
    TestClass &operator +=(int r)
    {
        num += r;
        return *this;
    }
};

int型からインスタンスを生成できるように変換コントラスタも必要です。

算術演算子

次に、算術演算子のオーバーロードを非メンバ関数で行います。
privateメンバ変数へは先ほどオーバーロードした複合代入演算子を利用してアクセスします。


//TestClass + TestClass
TestClass operator+(const TestClass& l, const TestClass& r)
{
    return TestClass(l) += r;
}

//TestClass + int
TestClass operator+(const TestClass& l, int r)
{
    return TestClass(l) += r;
}

//int + TestClass
TestClass operator+(int l, const TestClass& r)
{
    return TestClass(l) += r;
}

前述した通り、演算子のオーバーロードの組み合わせは三種類あるので、すべて定義します。
前ふたつはメンバ関数でも定義できますが、定義場所が散らばると見づらくなるので同じ場所に定義することにします。
(好みの問題です)

クラス内でオーバーロードした+=演算子を利用することで、privateメンバにアクセスしつつオーバーロードを定義しています。

これで以下のような演算が可能になります。


TestClass tc1(10);

TestClass tc2 = tc1 + 1;
TestClass tc3 = 2 + tc2;
TestClass tc4 = tc1 + tc2;

tc1 += 10;
tc2 += tc1;

比較演算子のオーバーロード

比較演算子も、privateメンバ変数にアクセスするために必要な演算子はメンバ関数として定義しておきます。

==演算子、!=演算子

これらは演算子のオーバーロードでの説明と同じです。


class TestClass
{
    int num;
public:
    bool operator ==(const TestClass &r) const
    {
        return num == r.num;
    }
    bool operator !=(const TestClass &r) const
    {
        return !(*this == r);
    }

<、>、<=、>=演算子

これもクラス内関数の処理は演算子のオーバーロードと同じです。


class TestClass
{
    int num;
public:
    bool operator <(const TestClass &r) const
    {
        return num < r.num;
    }
    bool operator >(const TestClass &r) const
    {
        return  r < *this;
    }
    bool operator <=(const TestClass &r) const
    {
        return !(r > *this);
    }
    bool operator >=(const TestClass &r) const
    {
        return !(r < *this);
    }
};

それぞれのオーバーロードの定義方法を見てください。
最初の<演算子のみが実際にメンバ変数にアクセスしており、後は既にオーバーロードした演算子を利用して処理を実装しています。

こうすることで、比較するメンバ変数を変えるなどの修正があった場合でも、ひとつのメンバ関数を変更するだけで済み、手間がかかりません。

なお、最初の<演算子以外はクラス外に書くことも可能です。
(その場合は引数にインスタンスを二つ取り、thisの代わりにする)

クラス外関数の==演算子、!=演算子

後は算術演算子のクラス外関数でのオーバーロードと同じです。
引数パターンの異なる関数を二つずつ定義するだけです。
(一つはすでにメンバ関数として存在するため、これで計三つずつ)


//比較演算子のオーバーロード
bool operator ==(const TestClass &l, int r)
{
    return TestClass(r) == l;
}
bool operator ==(int l, const TestClass &r)
{
    return TestClass(l) == r;
}
bool operator !=(const TestClass &l, int r)
{
    return TestClass(r) != l;
}
bool operator !=(int l, const TestClass &r)
{
    return TestClass(l) != r;
}

クラス外関数の<、>、<=、>=演算子


bool operator <(const TestClass &l , int r)
{
    return TestClass(r) > l;
}
bool operator >(const TestClass &l, int r)
{
    return TestClass(r) < l;
}
bool operator <(int l, const TestClass &r)
{
    return TestClass(l) < r;
}
bool operator >(int l, const TestClass &r)
{
    return TestClass(l) > r;
}
bool operator <=(const TestClass &l, int r)
{
    return !(TestClass(r) < l);
}
bool operator >=(const TestClass &l, int r)
{
    return !(TestClass(r) > l);
}
bool operator <=(int l, const TestClass &r)
{
    return !(TestClass(l) > r);
}
bool operator >=(int l, const TestClass &r)
{
    return !(TestClass(l) < r);
}

既にオーバーロードした演算子を利用して別の演算子をオーバーロードしていますので、見た目がかなりややこしいことになっています。
しかし、既存のオーバーロード定義を再利用することで、これらの比較演算子は最初の==演算子や<演算子の処理を変えるだけですべて同じ処理になるように変更されます。

なお、比較演算子に関しては他のデータ型と比較すること自体があまりないため、実用性は低いかもしれません。

フレンド関数を利用した演算子のオーバーロード

上記の例では、非メンバ関数から利用するために、いくつかの演算子のオーバーロードはメンバ関数で行う必要があります。
これは非メンバ関数からはprivate(protected)メンバにアクセスできないためですが、フレンド関数はprivateメンバにアクセスできる外部関数なので、これを利用して演算子をオーバーロードすることもできます。


class TestClass
{
    int num;

	//フレンド関数
    friend TestClass operator+(const TestClass&, const TestClass&);
    friend TestClass operator+(const TestClass&, int);
    friend TestClass operator+(int l, const TestClass&);

public:
    explicit TestClass(int n = 0)
        :num(n) {}
};

//フレンド関数の定義
//TestClass + TestClass
TestClass operator+(const TestClass& l, const TestClass& r)
{
    TestClass tc;
    tc.num = l.num + r.num;
    return tc;
}

//TestClass + int
TestClass operator+(const TestClass& l, int r)
{
    TestClass tc;
    tc.num = l.num + r;
    return tc;
}

//int + TestClass
TestClass operator+(int l, const TestClass& r)
{
    TestClass tc;
    tc.num = l += r.num;
    return tc;
}

int main()
{
    TestClass tc1(10);

	//OK
    TestClass tc2 = 1 + tc1;
}

フレンド関数を使用しない場合、比較演算子のオーバーロードは一度新しいインスタンスを生成してから比較していました。
フレンド関数を使用する場合は既存のインスタンスのprivateメンバに直接アクセスできるため、こちらの方が効率が良いです。


//フレンド関数を使用しない場合
bool operator ==(const TestClass& l, int r)
{
    return TestClass(r) == l;
}

//フレンド関数を使用する場合
bool operator ==(const TestClass& l, int r)
{
    return l.num == r;
}