非メンバの演算子オーバーロード | Programming Place Plus C++編【言語解説】 第35章

トップページC++編

C++編で扱っている C++ は 2003年に登場した C++03 という、とても古いバージョンのものです。C++ はその後、C++11 -> C++14 -> C++17 -> C++20 -> C++23 と更新されています。
なかでも C++11 での更新は非常に大きなものであり、これから C++ の学習を始めるのなら、C++11 よりも古いバージョンを対象にするべきではありません。特に事情がないなら、新しい C++ を学んでください。 当サイトでは、C++14 をベースにした新C++編を作成中です。

この章の概要

この章の概要です。


非メンバの演算子オーバーロード

第19章で、演算子オーバーロードにより、演算子の挙動を変更できることを取り上げました。第19章の時点では、メンバ関数としてオーバーロードする方法だけを取り上げましたが、非メンバの関数としてもオーバーロードできます。本章では、その方法を取り上げます。

非メンバ関数としてオーバーロードする場合には、メンバ関数としてオーバーロードする場合と比べて、引数の個数が1つ増えることになります。

後者の場合だと、演算子の適用対象(オペランド)の1つは、operator を定義したクラスのオブジェクトであることが分かっているので、引数は、2つ目のオペランドか、void のいずれかになります。前者の場合は、どこかのクラスに属していないので、それぞれのオペランドを引数で受け取るように定義しなければなりません。

// メンバ関数としてオーバーロードする場合
class MyClass {
public:
    bool operator==(const MyClass& rhs) const;
};

// 非メンバ関数としてオーバーロードする場合
bool operator==(const MyClass& lhs, const MyClass& rhs);

引数が2つある、非メンバ関数の演算子オーバーロードでは、第1引数が第1オペランドに、第2引数が第2オペランドに対応します。たとえば、以下のようにオーバーロードを行ったとします。

const DataStore operator+(const DataStore& lhs, int rhs);

この場合、+演算子の左側のオペランドが DataStore型で、右側のオペランドが int型の場合にのみ機能します。逆になっていると、この演算子オーバーロードは使用できません。

DataStore ds;
ds = ds + 10;  // OK
ds = 10 + ds;  // エラー

DataStore型と int型の2つが絡む加算演算子では、引数の組み合わせが4通り考えられます。

左オペランド 右オペランド
DataStore DataStore
DataStore int
int DataStore
int int

このうち、両オペランドとも int型のパターンは、オーバーロードできません。引数がすべて、int型のような組み込み型の場合には、混乱を避けるため、演算子オーバーロードは禁止されているためです。

他の3つの組み合わせはいずれも、演算子オーバーロード可能です。

const DataStore operator+(const DataStore& lhs, const DataStore& rhs);
const DataStore operator+(const DataStore& lhs, int rhs);
const DataStore operator+(int lhs, const DataStore& lhs);

+演算子で加算できるのに、+=演算子でできなかったらおかしいので、通常は両方オーバーロードするべきです。その際、一般的には、+=演算子のような複合代入演算子はメンバとしてオーバーロードし、+演算子は非メンバとしてオーバーロードします

また、+演算子の実装の際に、クラスの「非公開」な部分が必要になることがあり、そのために「公開」のメンバを増やしたり、フレンド関数を使ったりしそうになりますが、+=演算子があれば、これを呼ぶように +演算子を実装することで避けられます。

以下に実装例を挙げます。DataStoreクラスのようにシンプル極まりないクラスでは効果が薄いですが、もっと大きいクラスならば、できるだけコードをまとめることに価値が生まれてきます。

#include <iostream>

class DataStore {
public:
    explicit DataStore(int value) :
        mValue(value)
    {}

    inline DataStore& operator+=(const DataStore& rhs)
    {
        (*this) += rhs.Get();  // operator+=(int) に移譲
        return *this;
    }

    inline DataStore& operator+=(int rhs)
    {
        mValue += rhs;
        return *this;
    }

    inline int Get() const
    {
        return mValue;
    }

private:
    int    mValue;
};

const DataStore operator+(const DataStore& lhs, const DataStore& rhs)
{
    return DataStore(lhs) += rhs;
}

const DataStore operator+(const DataStore& lhs, int rhs)
{
    return DataStore(lhs) += rhs;
}

const DataStore operator+(int lhs, const DataStore& rhs)
{
    return DataStore(lhs) += rhs;
}

int main()
{
    DataStore ds(0);

    ds = ds + 10;
    std::cout << ds.Get() << std::endl;

    ds = 10 + ds;
    std::cout << ds.Get() << std::endl;

    ds = ds + ds;
    std::cout << ds.Get() << std::endl;

    ds += 10;
    std::cout << ds.Get() << std::endl;

    ds += ds;
    std::cout << ds.Get() << std::endl;
}

実行結果:

10
20
40
50
100

演算子のオーバーロードは、メンバとしてでも、非メンバとしてでも可能であることが分かりました(一部、後述するように例外はあります)。使い分けはどのようにすれば良いでしょうか。

算術演算子(+-*/%) や、関係演算子(<,<=,>,>=)、等価演算子(==、!=) などのように、オペランドを2つ伴う演算子は、異なる型の引数を、任意の順番・組み合わせで使えるようにするため、非メンバとしてオーバーロードしてください。このとき、+演算子の実装に +=演算子を使ったり、!=演算子の実装に ==演算子を使ったりして、コードの重複を避け、保守性を高めることを目指してください。

オペランドが1つだけの演算子は、基本的にメンバとしてオーバーロードしてください(いくつかの演算子については、そもそも選択の余地無く、メンバにしなければならないルールがあります)。
クラス定義から離れたところでオーバーロードされていると、単純に分かりづらい(存在を認識しづらい)ですし、通常、「非公開」なメンバ変数へのアクセスが必要になるはずなので、メンバ関数の方が実装しやすいはずです。


名前空間内の演算子オーバーロード

非メンバ関数の演算子オーバーロードは、任意の名前空間の内側で定義しても構いません。

前の項で登場した DataStoreクラスの例で確認してみましょう。DataStoreクラスと、それに関係する演算子オーバーロードを、MyLib名前空間に入れてみます。

#include <iostream>

namespace MyLib {

    class DataStore {
    public:
        explicit DataStore(int value) :
            mValue(value)
        {}

        inline DataStore& operator+=(const DataStore& rhs)
        {
            (*this) += rhs.Get();  // operator+=(int) に移譲
            return *this;
        }

        inline DataStore& operator+=(int rhs)
        {
            mValue += rhs;
            return *this;
        }

        inline int Get() const
        {
            return mValue;
        }

    private:
        int    mValue;
    };

    const DataStore operator+(const DataStore& lhs, const DataStore& rhs)
    {
        return DataStore(lhs) += rhs;
    }

    const DataStore operator+(const DataStore& lhs, int rhs)
    {
        return DataStore(lhs) += rhs;
    }

    const DataStore operator+(int lhs, const DataStore& rhs)
    {
        return DataStore(lhs) += rhs;
    }

}

int main()
{
    MyLib::DataStore ds(0);

    ds = ds + 10;
    std::cout << ds.Get() << std::endl;

    ds = 10 + ds;
    std::cout << ds.Get() << std::endl;

    ds = ds + ds;
    std::cout << ds.Get() << std::endl;

    ds += 10;
    std::cout << ds.Get() << std::endl;

    ds += ds;
    std::cout << ds.Get() << std::endl;
}

実行結果:

10
20
40
50
100

このプログラムはコンパイルできますし、正しく動作します。MyLib名前空間に入っているのに、グローバル名前空間にいる main関数内からの呼び出しが行えていることに注目してください。

このプログラムがコンパイルできる理由は、呼び出そうとしている関数に与えた実引数が名前空間に所属している場合、その名前空間内からも、呼び出そうとしている関数を探そうとするためです。このルールは、実引数依存探索(ADL。Argument Dependent Lookup)と呼ばれています。

今回のケースでいうと、<<演算子の呼び出しは、実際には「operator+」という関数の呼び出しです。呼び出し個所から見える範囲には、そのような関数はありませんから、普通はコンパイルエラーになるはずですが、実引数として「MyLib::DataStore型の値」を渡しているため、MyLib名前空間内も探すようになります。その結果、「MyLib::operator+」を見つけ出してくれます。

実引数が複数あれば、それぞれが異なる名前空間に属する値を渡す可能性もあります。いずれにせよ、呼び出そうとしている関数を複数の名前空間内から探した結果、複数の候補が見つかった場合は、型がもっともよく適合するものを選び出します。

オーバーロードできない演算子

第19章でも取り上げていますが、以下に挙げる演算子については、非メンバ関数としてはオーバーロードすることはできず、メンバ関数としてしかオーバーロードできません。

また、以下の演算子は、非メンバ関数、メンバ関数のいずれとしてでもオーバーロードできません。


ユーザ定義型をストリームへ渡す

非メンバ関数としての演算子オーバーロードの活用例として、ユーザ定義型の値を、std::cout のようなストリームへ渡せるようにするというものがあります。

たとえば、次のようなプログラムはコンパイルエラーになってしまいます。

#include <iostream>

class DataStore {
public:
    explicit DataStore(int value) : mValue(value)
    {}

private:
    int    mValue;
};

int main()
{
    DataStore ds(100);

    std::cout << ds << std::endl;  // コンパイルエラー
}

std::cout の型は、std::ostream というクラスですが、この型を左オペランドに、DataStore型を右オペランドに渡せるような <<演算子の定義が見当たらないため、コンパイルエラーが起きています。

std::ostreamクラスに、引数が DataStore型の <<演算子を追加できればいいのですが、標準ライブラリ内のクラスなので、メンバ関数を追加できません。そこで、非メンバ関数としての演算子オーバーロードで解決します。

#include <iostream>

class DataStore {
public:
    explicit DataStore(int value) : mValue(value)
    {}

    inline int Get() const
    {
        return mValue;
    }

private:
    int    mValue;
};

std::ostream& operator<<(std::ostream& lhs, const DataStore& rhs)
{
    lhs << rhs.Get();
    return lhs;
}

int main()
{
    DataStore ds(100);

    std::cout << ds << std::endl;
}

実行結果:

100

左オペランドに std::ostream型を、右オペランドに DataStore型を受け取れる、非メンバ関数としての operator<< を定義しました。これがあれば、「std::cout << ds」のように書いたとき、この operator<< が呼び出されるようにコンパイルされます。

戻り値の型を ostream の参照にし、第1引数をそのまま返すことで、<<演算子を連続的に使えるようにすることも重要です。

ところで、今回は「非公開」なメンバ変数へアクセスするため、ゲッター関数を追加しましたが、場合によっては、あまり「公開」されたメンバを増やさない方が良いかもしれません。その場合には、フレンド関数(第25章)を用いる方法もあります。

#include <iostream>

class DataStore {
    friend std::ostream& operator<<(std::ostream& lhs, const DataStore& rhs);

public:
    DataStore(int value) : mValue(value)
    {}

private:
    int    mValue;
};

std::ostream& operator<<(std::ostream& lhs, const DataStore& rhs)
{
    lhs << rhs.mValue;
    return lhs;
}

int main()
{
    DataStore ds(100);

    std::cout << ds << std::endl;
}

実行結果:

100

なお、標準ライブラリの各種ストリームクラスについての詳細は、【標準ライブラリ】第27章第31章で解説しているので、そちらを参照してください。


練習問題

問題① 「幅」と「高さ」をまとめて「大きさ」を表現する Sizeクラスを定義し、必要と思われる演算子をオーバーロードしてください。「幅」と「高さ」は int型で表現されるものとします。


解答ページはこちら

参考リンク


更新履歴

’2017/2/15 新規作成。



前の章へ (第34章 関数オブジェクト)

次の章へ (第36章 operator new/delete)

C++編のトップページへ

Programming Place Plus のトップページへ



はてなブックマーク に保存 Pocket に保存 Facebook でシェア
X で ポストフォロー LINE で送る noteで書く
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー
先頭へ戻る