C++ template の(部分)特殊化ができるとき、できないとき

STL はまあ普通に使う、くらいの軟弱な知識で C++ を使っていると、クラス内で関数テンプレートの特殊化をしようとしてコンパイルエラーになって、なんでここで特殊化できないんだよ〜と愚痴を言っていたら、さらに恐ろしいことに、特殊化はできないのに部分特殊化はできる場合に遭遇したりして、 C++ の闇に触れた気分になるのだけど、実はそうでもないという話(少なくとも、ユーザの側から理解するだけなら、ね)。

(部分)特殊化できる条件

ややこしく感じる理由は、特殊化できる条件と、部分特殊化できる条件が全く別だから。実はそんなに複雑ではない。

特殊化できる
OK: 名前空間スコープで定義
NG: クラススコープで定義
部分特殊化できる
OK: クラステンプレート
NG: 関数(⊃メンバ関数)テンプレート

template<class T> inline void func     () {}
template<class T> inline void func<T*> () {} // error
template<>        inline void func<int>() {} // OK

struct Outer {
    template<class T> class Inner      {};
    template<class T> class Inner<T*>  {}; // OK
    template<>        class Inner<int> {}; // error

    template<class T> void member     () {}
    template<class T> void member<T*> () {} // error
    template<>        void member<int>() {} // error
};

template<class T> struct Outer::Inner<T*>  {}; // OK
template<>        struct Outer::Inner<int> {}; // OK

template<class T> inline void Outer::member<T*> () {} // error
template<>        inline void Outer::member<int>() {} // OK

クラステンプレート内テンプレート

クラステンプレート内で宣言されたテンプレートも部分特殊化が可能。特殊化は、囲っている(外の)クラステンプレートが全て特殊化されている場合のみ可能。

template<class T> struct Outer {
    template<class U> class Inner {};
};
template<class T> template<class U> struct Outer<T>  ::Inner<U*>  {}; // OK
template<class T> template<>        struct Outer<T>  ::Inner<int> {}; // error
template<>        template<>        struct Outer<int>::Inner<int> {}; // OK

回避策

定義位置を変えただけでは対処できないのは、まとめると

の二つ(たぶん)。でもこれらに相当する挙動が実現不可能かというとそうではなくて、適当に wrapper をこしらえて委譲すればいい。

例えば、関数テンプレートを部分特殊化したいとき、

template<int X, int Y> inline void func      () {}
template<int Y>        inline void func<0, Y>() {} // error

こういうのは、

template<int X, int Y> struct Func {
    static int f() {}
};
template<int Y> struct Func<0, Y> {
    static int f() {}
};

template<int X, int Y> inline void func() {
    Func<X, Y>::f();
}

こんな風にクラステンプレートに委譲すれば等価なことができる。

クラステンプレート内テンプレートの特殊化も、

template<class T> struct Outer {
    template<class U> void member() {}
};
template<class T> template<> inline void Outer<T>::member<int>() {} // error

これは例えば

template<class T> struct Outer {
    template<class U> void member() {
        Member<U>::f();
    }   

private:
    template<class U, class = void> struct Member {
        static void f() {}
    };  
    template<class V> struct Member<int, V> {
        static void f() {}
    };  
};

こうしてクラステンプレートの部分特殊化に持ち込める。方法は他にも色々あって、 boost::type<T> 的なものを使って関数のオーバーロードで解決することもできる。でもこのへんの包括的な話を書けるほどよく知らない。

Two-phase name look up

テンプレートの特殊化を活用する上で頭の隅に入れておきたい非自明な挙動の一つに、 two-phase name look up なるものがある。

テンプレートでない普通の関数やクラスでは、その中で使う名前(関数名とか型名とか)は、定義する場所より前に宣言されている必要がある。これはテンプレートを活用する上で不便だ。例えば std::swap を自分の定義した型に対し特殊化した場合を考えてみればいい。

そこで two-phase name look up が登場する。これは大ざっぱに言って、テンプレート内で使われる名前について、 (0)テンプレートパラメータに依存しない名前は(通常通り)定義された時点で探索を行い、 (1)テンプレートパラメータに依存する名前はテンプレートが実体化される時点で探索を行う、というもの。なにそれこわい。

依存(する|しない)の定義は、 Standard Features Missing From VC++ 7.1. Part III: Two-Phase Name Lookup に具体例つきで載っているのでそちらに任せるとして、例だけ引用しておくと、

依存しない名前 (non-dependent name)
依存する名前 (dependent name)

template<class T, int Size>
class Example: public Base<T> {
    int myArray_[Size];
    std::vector<int> myVector_;
    typename T::iterator iterator;
    void func (T* arg) {
        using namespace std;
        int size = arg->GetSize();
        for (int i = 0; i < size; ++i)
            cout << i;
    }
};

※一部引用元で抜け落ちている部分を補完しています。筆者は仕様レベルで理解して
 いるわけではないので、この色分けは参考程度にお願いします。

なんだそう。おおまかには、テンプレートパラメータに依存している型や変数や式を再帰的に辿る感じ。仕様書的な複雑さはともかく、直感に反する感じではないので、使う側としてはそんなに怯える必要はない、のかな。

このへん、いち C++ ユーザがどこまで理解しているべきなのか…。

戻る

y.fujii <y-fujii at mimosa-pudica.net>