C++での演算子多重定義


例えば複素数 x + yi は、普通の実数に加えて虚部という付加情報を持つ。 虚部が 0 なら実数と変わらないが、付加情報を活用することによって 実数計算をしたのでは得られない様々な有益な情報を得ることが出来る。

自動微分法も、複素数と同様に、付加情報を持った演算を行うことによって 勾配と言う有益な情報を得る。 区間演算も、丸め誤差を得たり、値域を得たりすることが出来る。

このような「インテリジェントな数値型」を使った演算を行うには、 演算子多重定義の活用が便利である。 C++においてそれを行う方法について解説する。

例題として、ごく普通の複素数型を実現するクラスの実装例を挙げる。 簡単のため、加算、減算、乗算のみ。 complex.hppがクラスの定義部で、 complex.cppがその使用例。

#ifndef COMPLEX_HPP
#define COMPLEX_HPP
...
#endif // COMPLEX_HPP

は、ヘッダファイルの多重読み込み対策。複数回includeした場合、2度目以降は 読み込まれないようにする。

class complex {
...
};

でクラス定義。クラスは、構造体の定義とそれを操作する関数(メソッド)群の定義を 同時に行うようなもの。 なお、ヘッダファイルではメソッドのプロトタイプ宣言のみを行い、その実装は 別の.cc(.cpp)ファイルで行うのが行儀がいいとされるが、 後述するテンプレートを使う場合は実装を別に書くのは難しく、 ここではヘッダファイルに全ての実装を書く流儀で統一する。

    public:
    double re;
    double im;

reとimがメンバ変数。いわゆる構造体のメンバに当たる。 なお、ここではpublic:指定でreとimが外部から丸見えになっているが、 オブジェクト指向の思想では適当なアクセサ(アクセスするためのメソッド)を準備し メンバ変数そのものはprivate:指定で隠蔽するのが普通。

    complex() {
    }

    complex(const double& x) {
        re = x;
        im = 0.;
    }
    complex(const double& x, const double& y) {
        re = x;
        im = y;
    }

戻り値の無いこれら3つのメソッドは、コンストラクタ。 それぞれ、引数無し、引数がdouble1つ、引数がdouble2つでインスタンスが 生成されたときの初期化のために呼び出される。 インスタンスの生成の仕方は、

    complex x, y, z;

    x = complex(1.);
    y = complex(1., 2.);
    z = 1.;

    complex p(1.);
    complex q(1., 2.);

    complex r = complex(1.);
    complex s = complex(1., 2.);
    complex t = 1.;

このようにいろいろな方法で行うことが出来る。

引数がdouble1つのコンストラクタは、「変換コンストラクタ」と呼ばれ、 double型からcomplex型への型変換に用いられる。 使用例で単に数値(1.)を代入できているのは、この変換コンストラクタの 働きによる。

なお、コンストラクタの引数をconst double&にしているのは、

という理由による。

    friend complex operator+(const complex& x, const complex& y) {
        complex r;

        r.re = x.re + y.re;
        r.im = x.im + y.im;

        return r;
    }

で、二項演算子「+」を定義している。 すなわち、

    complex x, y, z;
    z = x + y;

のようなcomplexとcomplexの加算において、この関数が呼ばれる。

次の、

    friend complex operator+(const complex& x, const double& y) {
        complex r;

        r.re = x.re + y;
        r.im = x.im;

        return r;
    }

    friend complex operator+(const double& x, const complex& y) {
        complex r;

        r.re = x + y.re;
        r.im = y.im;

        return r;
    }

は、

    z = x + 1.;
    z = 2. + x;

のように片方がdoubleの場合用の加算を定義している。 これは定義しなくても、変換コンストラクタが働くので一応実行可能である。 但し、その場合、x+1.は1を変換コンストラクタで1+0iに変換し、それをxに 加えるという動作になるので、無駄な計算が生じてしまう。このような場合は、 個別に定義した方がよい。

なお、これらの定義に付いている「friend」というキーワードは、これらが 「メンバ関数では無いがメンバ変数にアクセスが可能」という指定を行っている。 例えばcomplex同士のoperator+をメンバ関数として定義すると、

    complex operator+(const complex& y) const {
        complex r;

        r.re = re + y.re;
        r.im = im + y.im;

        return r;
    }

のようになる。すなわち、「z = x + y;」と書いたときに左側のxを経由してoperator+が 呼ばれる。しかし、このような書き方だと、「z = 2. + x;」のように演算子の左側が complexで無かった場合に困る。 このため、二項演算子はあえてメンバ関数ではなく一般の関数とし、friend指定で アクセスを許可するという書き方をするのが一般的である。

    friend complex& operator+=(complex& x, const complex& y) {
        x = x + y;
        return x;
    }

    friend complex& operator+=(complex& x, const double& y) {
        x.re += y;
        return x;
    }

これは、演算子+=を実装している。xを書き換えるので、xは参照かつconstではない。 また、xの参照を返すのは、

    p = (z += 2.);
のように+=演算子の結果を更に参照するような書き方に対応するため。

    friend complex operator-(const complex& x, const complex& y) {
        complex r;

        r.re = x.re - y.re;
        r.im = x.im - y.im;

        return r;
    }

    friend complex operator-(const complex& x, const double& y) {
        complex r;

        r.re = x.re - y;
        r.im = x.im;

        return r;
    }

    friend complex operator-(const double& x, const complex& y) {
        complex r;

        r.re = x - y.re;
        r.im = - y.im;

        return r;
    }

    friend complex& operator-=(complex& x, const complex& y) {
        x -= y;
        return x;
    }

    friend complex& operator-=(complex& x, const double& y) {
        x.re = x.re - y;
        return x;
    }

このへんは減算。基本的に加算と同じ。

    friend complex operator-(const complex& x) {
        complex r;

        r.re = - x.re;
        r.im = - x.im;

        return r;
    }

これは単項演算子の「-」。「p = -z」とか。

    friend complex operator*(const complex& x, const complex& y) {
        complex r;

        r.re = x.re * y.re - x.im * y.im;
        r.im = x.re * y.im + x.im * y.re;

        return r;
    }

    friend complex operator*(const complex& x, const double& y) {
        complex r;

        r.re = x.re * y;
        r.im = x.im * y;

        return r;
    }

    friend complex operator*(const double& x, const complex& y) {
        complex r;

        r.re = x * y.re;
        r.im = x * y.im;

        return r;
    }

    friend complex& operator*=(complex& x, const complex& y) {
        x = x * y;
        return x;
    }

    friend complex& operator*=(complex& x, const double& y) {
        x.re *= y;
        x.im *= y;
        return x;
    }

乗算も一応定義した。基本的に加減算と同じ。

    friend complex sqr(const complex& x) {
        complex r;

        r.re = x.re * x.re - x.im * x.im;
        r.im = 2. * x.re * x.im;

        return r;
    }

演算子では無いが、数学関数の実装の例としてのsqr(自乗)。

    friend std::ostream& operator<<(std::ostream& s, const complex& x) {
        s << '(' << x.re << '+' << x.im << "i)";
        return s;
    }

これは、<<演算子。

    std::cout << z << "\n";

のように普通に書けるようになる。