1. 数値型のプログラミング

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

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

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

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

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

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

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

    complexsample() {
    }

    complexsample(const double& x) {
        re = x;
        im = 0.;
    }
    complexsample(const double& x, const double& y) {
        re = x;
        im = y;
    }
戻り値の無いこれら3つのメソッドは、コンストラクタ。 それぞれ、引数無し、引数がdouble1つ、引数がdouble2つでインスタンスが 生成されたときの初期化のために呼び出される。 インスタンスの生成の仕方は、
    complexsample x, y, z;

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

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

    complexsample r = complexsample(1.);
    complexsample s = complexsample(1., 2.);
    complexsample t = 1.;
このようにいろいろな方法で行うことが出来る。

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

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

という理由による。

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

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

        return r;
    }
で、二項演算子「+」を定義している。 すなわち、
    complexsample x, y, z;
    z = x + y;
のようなcomplexsampleとcomplexsampleの加算において、この関数が呼ばれる。

次の、
    friend complexsample operator+(const complexsample& x, const double& y) {
        complexsample r;

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

        return r;
    }

    friend complexsample operator+(const double& x, const complexsample& y) {
        complexsample 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 complexsample& operator+=(complexsample& x, const complexsample& y) {
        x = x + y;
        return x;
    }

    friend complexsample& operator+=(complexsample& x, const double& y) {
        x.re += y;
        return x;
    }
これは、演算子+=を実装している。xを書き換えるので、xは参照かつconstではない。 また、xの参照を返すのは、
    p = (z += 2.);
のように+=演算子の結果を更に参照するような書き方に対応するため。

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

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

        return r;
    }

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

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

        return r;
    }

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

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

        return r;
    }

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

    friend complexsample& operator-=(complexsample& x, const double& y) {
        x.re = x.re - y;
        return x;
    }
このへんは減算。基本的に加算と同じ。

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

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

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

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

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

        return r;
    }

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

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

        return r;
    }

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

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

        return r;
    }

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

    friend complexsample& operator*=(complexsample& x, const double& y) {
        x.re *= y;
        x.im *= y;
        return x;
    }
乗算も一応定義した。基本的に加減算と同じ。

    friend complexsample sqr(const complexsample& x) {
        complexsample 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 complexsample& x) {
        s << '(' << x.re << '+' << x.im << "i)";
        return s;
    }
これは、<<演算子。
    std::cout << z << "\n";
のように普通に書けるようになる。

2. テンプレート

上の例は、内部のreとimの型はdoubleであった。 例えばこれをfloatに変更したいとか、あるいは自作のinterval型にしたいという 場合、全て書き直す必要があった。あるいはトリッキーにマクロで置き換えるか。

テンプレートは、クラス定義時には型を空欄にしておき、 変数の使用時に空欄部の型を指定することで、自動的にその型に対応する クラスが生成されるという機能である。 コンパイル時に中に入れられた型の種類数に応じて、その数だけのクラスが生成される。

上の例をテンプレートを用いて書き直してみた。 complexsampleT.hppがクラスの定義部で、 complexsampleT.ccがその使用例。 非常に単純に、

しただけ。これで、中がdoubleの複素数や中がintの複素数など、無限種類の 複素数型を一度に定義したことになる。

3. 数値型の入れ子

複素数型の内部型として「doubleを両端に持つ区間型」を使うなど、 テンプレートを入れ子に使ったときの問題点について考察する。

そのため、サンプルとして、加算、減算、乗算のみ、丸めの向きの変更も行わない 簡単な区間演算のクラスを作成した。テンプレートで両端点の型は任意。 複素数の例とほとんど同じ作り方。 intervalsampleT.hppがクラスの定義部で、 intervalsampleT.ccがその使用例。

また、同じくサンプルとして、加算、減算、定数乗算のみ、大きさを2に限定した 簡単なベクトル計算のクラスを作成した。テンプレートで内部の型は任意。 vectorsampleT.hppがクラスの定義部で、 vectorsampleT.ccがその使用例。

3.1 普通に組み合わせると

#include <iostream>

#include "complexsampleT.hpp"
#include "intervalsampleT.hpp"
#include "vectorsampleT.hpp"

int main()
{
    complexsample < intervalsample<double> > x;

    x = complexsample< intervalsample<double> >(intervalsample<double>(1., 2.), intervalsample<double>(2. ,3.));
    x = intervalsample<double>(1.);
    x = complexsample< intervalsample<double> >(1.);
    // error
    // x = 1.;

    x += 1.;

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

    vectorsample< complexsample < intervalsample<double> > > y;

    y(0) = intervalsample<double>(1.);
    y(1) = complexsample< intervalsample<double> >(1.);

    y *= x;
    y *= intervalsample<double>(1.);
    // error
    // y *= 2.;

    std::cout << y << "\n";
}
main1.cc
このように組み合わせて使ってみると、「// error」となっている部分は コンパイル出来ない。 エラー部に共通の特徴は、ともに の2つの変換コンストラクタを経由する型変換が必要とされている点である。 C++では変換コンストラクタによる自動型変換は一回だけであり、 このように複数回の適用が必要な場合は自動型変換を行ってくれない。

3.2 定数のテンプレート化

そこで、定数が絡むメソッド(引数に内部型Tを含むメソッド)について、 それらを関数テンプレート化し、様々な型を定数として受け付けられるようにしてみる。 例えば、
    complexsample(const T& x) {
        re = x;
        im = 0.;
    }
を、
    template <class C> complexsample(const C& x) {
        re = x;
        im = 0.;
    }
に、
    friend complexsample operator+(const complexsample& x, const T& y) {
        complexsample r;

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

        return r;
    }
    template <class C> friend complexsample operator+(const complexsample& x, const C& y) {
        complexsample r;

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

        return r;
    }
に変更、といった感じ。このポリシーで変更したものが、 である。

これらを組み合わせて使ってみる。
#include <iostream>

#include "complexsampleT2.hpp"
#include "intervalsampleT2.hpp"
#include "vectorsampleT2.hpp"

int main()
{
    complexsample < intervalsample<double> > x;

    x = complexsample< intervalsample<double> >(intervalsample<double>(1., 2.), intervalsample<double>(2. ,3.));
    x = intervalsample<double>(1.);
    x = complexsample< intervalsample<double> >(1.);
    x = 1.;

    x += 1.;

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

    vectorsample< complexsample < intervalsample<double> > > y;

    y(0) = intervalsample<double>(1.);
    y(1) = complexsample< intervalsample<double> >(1.);

    // error
    // y *= x;
    // error
    // y *= intervalsample<double>(1.);
    y *= 2.;

    std::cout << y << "\n";
}

main2.cc
今度は、先ほどとは違う部分でエラーとなっている。 異なる型同士の乗算において、定数を任意にしたためにマッチする乗算が 多過ぎ、コンパイルエラーになってしまう。

3.3 定数の安全なテンプレート化

定数のテンプレート化を行った場合、「template <class C>」のCの 範囲が広すぎることが問題になってしまう。 そこで、Cの範囲を「CからTに型変換が可能なもの」に限定することを考える。

これは、boostに含まれる is_convertibleとenable_ifを組み合わせることによって実現できる。

is_convertible<A, B>は、AをBに変換可能なら真、そうでなければ偽。 「#include <boost/type_traits.hpp>」が必要。

enable_ifには2通りの使い方があり、

「#include <boost/utility/enable_if.hpp>」が必要。

これらを使ってテンプレート化の制限を行う場合、2通りの方法がある。

のようなテンプレート関数のCをTに変換可能なものに制限する場合、 のように、ダミー引数を加える方法と、戻り値に細工する方法がある。 どちらの場合でも、enable_ifが未定義になるようなCに対しては 関数の定義そのものをしなかったことになる(エラーにはならない)ことを利用している。

fがコンストラクタの場合は戻り値が無いので前者を使うしかなく、 二項演算子の場合は引数の追加が出来ないので後者を使うしか無い。

以上を元に変更したものが、

である。

これらを組み合わせて使ってみる。
#include <iostream>

#include "complexsampleT3.hpp"
#include "intervalsampleT3.hpp"
#include "vectorsampleT3.hpp"

int main()
{
    complexsample < intervalsample<double> > x;

    x = complexsample< intervalsample<double> >(intervalsample<double>(1., 2.), intervalsample<double>(2. ,3.));
    x = intervalsample<double>(1.);
    x = complexsample< intervalsample<double> >(1.);
    x = 1.;

    x += 1.;

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

    vectorsample< complexsample < intervalsample<double> > > y;

    y(0) = intervalsample<double>(1.);
    y(1) = complexsample< intervalsample<double> >(1.);

    y *= x;
    y *= intervalsample<double>(1.);
    y *= 2.;

    std::cout << y << "\n";
}

main3.cc
これで、ようやく正しくコンパイルできるようになった。