2013/12/14

これぞC++の真骨頂! タプルを実装してみた

今のVC++2013には標準ライブラリとしてtupleがあります。

std::tuple<int, double> t(100, 10.5);
std::cout << std::get<0>(t) << std::endl;
std::cout << std::get<1>(t) << std::endl;

こんな感じでstd::tuple<型リスト>という複数の型を持ったインスタンスを生成できます。
これは構造体と同じなのですが、構造体の場合は宣言しなければアクセスできませんでした。

まとまった2つの値を返したい場合、新しい構文の初期化リストの書き方を使えば、

struct S {
    int n;
    double d;
};

struct S func(void) {
    return S { 10, 10.5 };
}

と書けばいいのですが、この構造体を頻繁に使う場面でない場合、あるいは結果を取得だけしてその後は構造体が要らない場合、組み合わせから新しい組み合わせを作りたい場合には不便です。

(複数の構造体を宣言して使わなければならない例)
struct S1 {
    int n;
    double d;
};

struct S2 {
    char* c;
    const int* p;
};

struct S1 func1(void) {
    return S1 { 10, 10.5 };
}

struct S2 func2(void) {
    const int* p = new int(100);
    return S2 { "string", p };
}
int main() {
    S1 s = func1();
    s.n; // なんちゃら
    s.d; // なんちゃら

    S2 s2 = func2();
    s2.c; // なんちゃら
    s2.p; // なんちゃら
    return 0;
}

(組み合わせから新しい組み合わせを作りたい例)
struct S1 {
    int n;
    double d;
    char* c;
};

struct S2 {
    char* c;
    int n;
};

int main() {
    S1 s1 = { 10, 10.5, "string" };
    S2 s2 = { s1.c, s1.n };

    return 0;
}

さて、tupleを使うと次のようにシンプルになります。

(複数の組み合わせの例)
std::tuple<int, double> func1(void) {
    return std::tuple<int, double> { 10, 10.5 };
}

std::tuple<char*, const int*> func2(void) {
    const int* p = new int(100);
    return std::tuple<char*, const int*> { "string", p };
}
int main() {
    std::tuple<int, double> s = func1();
    std::get<0>(s); // なんちゃら
    std::get<1>(s); // なんちゃら
    std::tuple<char*, const int*> s2 = func2();
    std::get<0>(s2); // なんちゃら
    std::get<1>(s2); // なんちゃら
    return 0;
}

(組み合わせから新しい組み合わせを作りたい例)
std::tuple<int, double, const char*> s { 10, 10.5, "string" };
std::tuple<const char*, int> s2 { std::get<2>(s), std::get<0>(s) };

結局、後でメンバの変更が必要のない場合は上記のタプルで置き換えられると言うことです。ただ、参照を使うことで、元の変数を変更することはできます。

int a, b;
std::tuple<int&, int&> s { a, b };

std::get<0>(s) = 20; // aが変更される
std::get<1>(s) = 20; // bが変更される

これは実質参照の配列として機能することになります。

これらは静的な片付けがなされており、コンパイル時に展開してくれます。そのため、tupleのsizeofは構造体で定義した場合と同じサイズを返し、getも構造体へのアクセスと同程度となる(はず)です。これがSTLの魅力ですね!

このようにtupleは便利なものなのですが、getがタプルのメンバ関数でない点が気持ち悪いですね。そこでtupleもどきを自分で実装してみたいと思います。とりあえずいろいろ試行錯誤した結果以下のソースができあがりました! (新しい機能を使っていますのでVC++2012以下ではコンパイルできません)

template <class T, class ...Ts>
class tuple {
private:
    template <int n, class U, class ...Us>
    struct id {
        typedef typename id<n - 1, Us...>::type type;
    };
    template <class U, class ...Us>
    struct id<1, U, Us...> {
        typedef U type;
    };

public:
    tuple(T t, Ts ...args) : t(t), tc(args...) { }

    template <int n>
    typename id<n, T, Ts...>::type get() {
        return tc.get<n - 1>();
    }

    template <>
    T get<1>() {
        return t;
    }

private:
    T t;
    tuple<Ts...> tc;
};

template <class T>
class tuple<T> {
private:
    template <int>
    struct id {
        typedef T type;
    };

public:
    tuple(T t) {
        this->t = t;
    }
    template <int>
    T get() {
        return t;
    }

private:
    T t;
};

どうやって使うかというと、
tuple<int, int> t(10, 10);
と宣言して、
t.get<1>();
t.get<2>();
として使います。ね、簡単でしょ! もちろん様々な演算子を定義してもいいですし、構造上ある位置以降をタプルとして新しく生成することもできます。その参照を返すことだって。

使い方とかはそんなところなのですが、このソースを書くのにはえらく苦労しました。初めてのテンプレートメタプログラミングですからね。

どういうことか?

通常プログラミングをするときは、アルゴリズムを実装し、それに入力を与えることで目的の結果を返します。その際に、構築しやすいようにデータ構造を工夫したり、最近ではクラスを使って実装を明確分割したりします。

ところがC++にはテンプレートという超高級マクロ機能があって、コンパイル時に展開して書房のプログラムを作ってくれます。これはアルゴリズムを作るためのプログラムと言うことです。

1. プリプロセッサがあって
2. テンプレートを展開して
3. クラス/関数を実装する

という流れになっています。つまり、テンプレートはクラスなどよりもまた一つ上のレイヤーであり、メタプログラミングと呼ばれています。

試しにメタプログラミングのハロワを実行してみましょう。以下はコンパイル時に階乗を計算するテンプレートクラスです。結局、メタプログラミングの基本は再帰処理となります。いくつかコツがあるので見ていきましょう。

template <int n>
struct S {
    static const int N = n * S<n - 1>::N;
};

template <>
struct S<1> {
    static const int N = 1;
};

まずどちらもSという名前の構造体です。ところが、templateを見るとint nがあるものと何も無いものがあります。
int nは型を受け取るのではなく、数値を受け取ります。従って、S<5>::Nと呼び出すと、まず初めに
static const int N = 5 * S<4>::N;
が実行されます。staticなconstを指定することにより、コンパイラはコンパイル時にのみこの値を使います。
そしてS<4>::Nを呼び出すと、
static const int N = 4 * S<3>::N;
が実行されます。次は
static const int N = 3 * S<2>::N;
そして
static const int N = 2 * S<1>::N;
ですね。コンパイラはこれらの値をコンパイル時には保持しています。言い換えると、S<5>::N, S<4>::N, S<3>::N, S<2>::Nの定数を生成します。

さて、最後にS<1>::Nですがこれはテンプレートの特殊化により定義されています。テンプレート特殊化とは、tenmplate <>で定義されたもので、予めのテンプレートtemplate <int n>のint nに具体的な型や値を入れて定義するものです。

今回の場合は1で終了したいので、template <> struct S<1>を定義しました。これによってS<1>::Nは再帰処理から抜けて、static const int N = 1;が実行されます。従って、S<1>::N = 1であり、それによってS<2>::N = 2, S<3>::N = 6, S<4>::N = 24, S<5>::N = 120となるのです。

なお、これらの実行ではstaticですから、インスタンスが生成されません。もしもstaticを付けないとすると、インスタンスを生成していきますので、次のようになります。

template <int n>
struct S {
public:
    S() {
        std::cout << N << std::endl;
    }
    const unsigned int N = n * S<n - 1>().N;
};

template <>
struct S<1> {
    const unsigned int N = 1;
};

しかしこれでは余り意味がないのです。インスタンスを生成しないからこそコンパイル時に計算できるのですから。

以上のようにテンプレートを使った値の保存方法はこんな感じです。これで繰り返し(テンプレートの再帰)と選択(テンプレートの特殊化)が達成できました。これはほとんど、あらゆるメタプログラミングができることを意味しているのです。(だって、whileとifがあるってことですよ)

次に型の保存について触れます。以下の例では、S<true, int, char>::typeだとint型になり、S<false, int, char>::typeだとchar型になるというものです。条件によって型を変えるわけですね。

template <bool b, class T1, class T2>
struct S;

template <class T1, class T2>
struct S<true, T1, T2> {
    typedef T1 type;
};

template <class T1, class T2>
struct S<false, T1, T2> {
    typedef T2 type;
};

このように型の保存にはtypedefが有効です。型の再帰処理について検討してみましょう。
今回はS<int, 5>::typeとしたら、int*****を返すテンプレートを考えます。

まず、

template <class T, int n>
struct S {
    typedef なんとか type;
};

が浮かびます。

次に((int*)*)*……の構造を浮かべて、次のように書きます。

template <class T, int n>
struct S {
    typedef typename S<T*, n - 1>::type type;
};

ここでtypenameは、S<T*, n - 1>::typeがコンパイル時に型なのか判別が付かないために、型であることを明示するキーワードです。

最後に1になったところで終了するために、特殊化を行います。

template <class T, int n>
struct S {
    typedef typename S<T*, n - 1>::type type;
};

template<class T>
struct S<T, 1> {
    typedef T* type;
};

これで完成です。

最後にテンプレートの可変長引数について考えます。

template <class ...Ts>

とすることで可変長引数テンプレートが宣言できます。これはC++の新しい機能です。

そして、...演算子は引数の前に付くとpack, 後ろに付くとunpackされます。つまり、複数のものをまとめたり(pack)、まとめたものを展開したりする(unpack)わけです。

次に実際の引数で受け取ります。

template <class ...Ts>
class C {
public:
    C(Ts ...args) {

    }
};

ただ、これでは全ての引数に対して処理ができないため、再帰処理を検討しますつまり、関数テンプレートを作り、第1引数のみを受け取り、それ以降をまたこの関数テンプレートに投げるものを考えます。すると次のようになります。

template <class ...Ts>
class C {
public:
    C(Ts ...args) {
        f(args...);
    }

    template <class T, class ...Ts>
    void f(T t, Ts ...ts) {
        f(t);
        f(ts...);
    }
    template <class T>
    void f(T t) {
        std::cout << t << std::endl;
    }
};


これは、まずf(args...)で渡されたpackされた引数argsに対して、unpackして引数に渡します。これはargs...と書きます。

次に複数の引数がある場合はvoid f(T t, Ts ...ts)が実行されます。
例えばC<int, char, double>だった場合Tがintになり、Tsはchar, doubleになります。ここでtの値を受け取り、残りは再びこの関数にf(ts...)で投げます。f(t)としたものは、次のvoid f(T t)に渡され、今回の場合はstd::coutでコンソール画面に出力しました。

なお、今回の例でのfはテンプレートの特殊化ではなく、関数のオーバーロードである点に注意して下さい。

以上の事柄を使ってtupleを実装したわけですが、時間がかかってしまったのは、getの部分です。はじめ、getする関数を、次のgetの関数の戻り値の型を戻り値の型にして再帰的に定義していました。

template <int n>
decltype(tc.get<n - 1>()) get() {
    return tc.get<n - 1>();
}

これはいいのですが、次の特殊化でコンパイルエラーと成りコンパイルできません。

template <>
T get<1>() {
    return t;
}

error C2912: 明示的な特殊化 'int tuple<int,int>::get(void)' は関数テンプレートの特殊化ではありません

このエラーは何でしょう? 探っていきます。

まず、メソッドの呼び出し時に明示的にテンプレートを選択することができます。

template <class T>
void f() {
    std::cout << "f<T>" << std::endl;
}

template <class S, class T>
void f() {
    std::cout << "f<S, T>" << std::endl;
}

f<int>(); // f<T>
f<int, double>(); // f<S, T>

そして、テンプレート関数は部分特殊化ができません。

template <class S, class T>
void f() {
    std::cout << "f<S, T>" << std::endl;
}
template <class T>
void f<int, T>() { // error C2768: 'f' : 明示的なテンプレート引数を使用することはできません。
    std::cout << "f<int, T>" << std::endl;
}

これは今回のエラーとは関係なさそうです。今回のエラーは、

void f() {

}

template<>
void f() {

}

のようにテンプレート化されていない関数を特殊化しようとするときに発生するようです。そんなことしたっけ??

挙動を探ります。

実はtuple<int>だけだとエラーにならなくて、tuple<int, int>のように2つ以上だとエラーになりました。
つまり、

template<int n>
decltype(f<n - 1>()) f() {
    return f<n - 1>();
}

template<>
int f<1>() {
    return 1;
}

という構造にエラーがあることが分かりました。decltype(f<n - 1>())をintに返ると成功します。これは……?

template <int n>
int g() {

}

template <>
void g<1>() {

}

これも同様のエラーとなります。やはり、引数の型が分からないために異なった型と認識されているみたいですね。こうなれば、型を返すテンプレートが必要です。結果的に、typedef typename id<n - 1, Us...>::type type;で型を再帰定義してそれを取得する形にしました。これでうまくいきました。

このようにC++は大変恐ろしい言語です。自分で世界を作り上げてしまうのですから、複数人で行うプロジェクトでは何かしら制約を設けたくなるのも分かります。個人的にはこの何でもありなC++が大好きですが!

0 件のコメント:

コメントを投稿