こんにちは。田原です。

クラスやクラス・テンプレートの中で定義するテンプレートのことをメンバ・テンプレートと呼び、関数テンプレートとクラス・テンプレートがあります。そして、クラス・テンプレートの中で定義するクラス・テンプレートやその中の関数テンプレートをクラス外定義するなど意外にややこしいです。今回はこのあたりに焦点を当てて解説してみます。

1.今回の解説対象

今回解説するテンプレートは以下のメンバ・テンプレートです。

関数型 グローバル関数 関数テンプレート
メンバ関数 メンバ・テンプレート
クラス型 クラス(他のクラスに含まれないクラス) クラス・テンプレート
他のクラスの中で定義されたクラス メンバ・テンプレート
その他 typedef(usingキーワードを使う) エイリアス(別名)テンプレート

メンバ・テンプレートはにクラスやクラス・テンプレートの中で定義された「関数テンプレート」と「クラス・テンプレート」です。
クラス内で全て定義する場合は大差ありません。しかし、クラス外で定義する場合は、組み合わせがあっという間に増えるので、ややこしいです。

  1. メンバ・テンプレートの中身をクラス外で定義する場合
    一般のメンバ関数やクラス内定義クラスを外部で定義するのと同じですが、テンプレート・パラメータの分複雑です。

  2. メンバ・テンプレートの明示的特殊化行う場合
    こちらはクラス内ではできないと決められています。

  3. クラス内のクラス・テンプレートの部分的特殊化行う場合
    こちらはクラス内、クラス外の両方でできるようです。

2.クラス外定義その1

関数テンプレートやクラス・テンプレートの中身の定義をクラス外で行うケースです。

2-1.外側のクラスが通常クラスの場合

テンプレートでない普通のクラスの中でメンバ・テンプレートの宣言だけ行い、その定義を外でする場合です。
これは普通のメンバ関数やクラス内クラスを外で定義するのと同じです。

以下はメンバ関数テンプレート(foo)とクラス・テンプレート(Bar, Baz)をクラス外で定義する例です。
クラス・テンプレートはクラス・テンプレート自身の定義を外部で行うケース(Bar)と、クラス・テンプレートの定義はクラス内で行い、そのメンバ関数をクラスの外で行うケース(Baz)を示します。後者は第3回で解説したクラス・テンプレートのメンバ関数をクラス外で定義する時の記述方法と同じです。また、内側のクラス(Baz)のメンバ関数をBazの外で定義する場合、Outerの中では書けません。つまり、自分の中で定義しないなら他のクラスの中でも定義できません。すべてのクラスの外で書く必要があります。

#include <iostream>
#include "typename.h"

struct Outer
{
    template<typename T>
    void foo(T t);

    template<typename T>
    struct Bar;

    template<typename T>
    struct Baz
    {
        Baz(T t);
    };
};

template<typename T>
void Outer::foo(T t)
{
    std::cout << "Outer::foo<" << TYPENAME(t) <<  ">(" << t << ")\n";
}

template<typename T>
struct Outer::Bar
{
    Bar(T t)
    {
        std::cout << "Outer::Bar<" << TYPENAME(t) <<  ">::Bar(" << t << ")\n";
    }
};

template<typename T>
Outer::Baz<T>::Baz(T t)
{
    std::cout << "Outer::Baz<" << TYPENAME(t) <<  ">::Baz(" << t << ")\n";
};

int main()
{
    Outer aOut;
    aOut.foo(123);
    Outer::Bar<int> aBar(456);
    Outer::Baz<short> aBaz(789);
}
Outer::foo<int>(123)
Outer::Bar<int>::Bar(456)
Outer::Baz<short>::Baz(789)

2-2.外側のクラスがクラス・テンプレートの場合

あまり使うケースはないとは思いますが、ルールは単純ですので簡単に示します。

上記の例のOuterをクラス・テンプレートにしてみます。
単純にルールは第3回で解説したクラス・テンプレートのメンバ関数をクラス外で定義する時の記述方法と同じです。その結果、template<テンプレート・パラメータ・リスト>の記述が2つ並びます。

#include <iostream>
#include "typename.h"

template<typename tType>
struct Outer
{
    template<typename T>
    void foo(T t);

    template<typename T>
	struct Bar;

    template<typename T>
	struct Baz
    {
        Baz(T t);
    };
};

template<typename tType>
template<typename T>
void Outer<tType>::foo(T t)
{
    std::cout << TYPENAME(*this) << ">::foo<" << TYPENAME(t) <<  ">(" << t << ")\n";
}

template<typename tType>
template<typename T>
struct Outer<tType>::Bar
{
    Bar(T t)
    {
        std::cout << TYPENAME(*this) << "::Bar(" << t << ")\n";
    }
};

template<typename tType>
template<typename T>
Outer<tType>::Baz<T>::Baz(T t)
{
    std::cout <<  TYPENAME(*this) << "::Baz(" << t << ")\n";
};

int main()
{
    Outer<char> aOut;
    aOut.foo(123);
    Outer<long>::Bar<int> aBar(456);
    Outer<long long>::Baz<short> aBaz(789);
}
Outer<char>>::foo<int>(123)
Outer<long>::Bar<int>::Bar(456)
Outer<long long>::Baz<short>::Baz(789)

Outer<tType>::Baz<T>::Baz(T t)はちょっとややこしいと思います。これは、Bazクラス・テンプレートのコンストラクタを定義しています。ですので、thisはBazクラスを実体化したもの(Baz<short>)となります。従って、thisが指す先の型はOuterクラス・テンプレートを実体化したもの(Outer<long long>)の中で定義されていますので、Outer<long long>::Baz<short>です。

3.クラス外定義その2

関数テンプレートやクラス・テンプレートの明示的特殊化や部分的特殊化を行うケースです。「2.クラス外定義その1」のケースとは異なり、これらはクラス内で記述できませんから必然的にクラス外で定義します。

ただし、関数テンプレートは「部分的特殊化」はできないので、通常は「オーバーロード」します。オーバーロードは複数のプライマリ・テンプレートを書くことですので、クラス内で全部定義することもできますし「2-1.」の例のように中身をクラス外で定義することもできます。

3-1.外側のクラスが通常クラスの場合

簡単なサンプルを示します。
このサンプルでは、関数テンプレートはテンプレート・パラメータそのままの型とそのポインタ型についてオーバーロードを定義しchar const*型を明示的特殊化しています。
クラス・テンプレートは、テンプレート・パラメータそのままの型をプライマリー定義し、そのポインタ型を部分的特殊化し、char const*型を明示的特殊化しています。
なお、どちらともプライマリー・テンプレートはクラス内定義しています。

ところで、関数テンプレートは振る舞いが判りにくいので第5回目で説明したように、明示的特殊化ではなく通常関数でオーバーロードした方が良いです。

#include <iostream>
#include <type_traits>
#include "typename.h"

struct Outer
{
    // オーバーロード0
    template<typename T>
    void foo(T t) { std::cout << "Outer::foo<" << TYPENAME(T) <<  ">(" << t << ")\n"; }
    // オーバーロード1
    template<typename T>
    void foo(T* t) { std::cout << "Outer::foo<" << TYPENAME(t) <<  ">(" << *t << ")\n"; }
    // オーバーロード(非テンプレート)
    void foo(long t) { std::cout << "Outer::foo(long " << t << ")\n"; }
    
    template<typename T>
    struct Bar
    {
        Bar(T t) { std::cout << TYPENAME(*this) << "::Bar(" << t << ")\n"; }
    };

    // 部分的特殊化
#if 0 
    #define PARTIAL_INNER
    template<typename T>
    struct	Bar<T*>
    {
        Bar(T* t) { std::cout << TYPENAME(*this) << "::Bar(" << *t << ") Inner\n"; }
    };
#endif
};

// 明示的特殊化
template<>
void Outer::foo<char const*>(char const* t)
{
    std::cout << "Outer::foo<" << TYPENAME(t) <<  ">(" << reinterpret_cast<void const*>(t) << ")\n";
}

// 明示的特殊化
template<>
struct	Outer::Bar<char const*>
{
    Bar(char const* t) { std::cout << TYPENAME(*this)
        << "::Bar(" << reinterpret_cast<void const*>(t) << ")\n"; }
};

// 部分的特殊化
#ifndef PARTIAL_INNER
template<typename T>
struct	Outer::Bar<T*>
{
    Bar(T* t) { std::cout << TYPENAME(*this) <<  "::Bar(" << *t << ") Outer\n"; }
};
#endif

int main()
{
    Outer aOut;
    aOut.foo(123);
    aOut.foo("abc");
    int x=789;
    aOut.foo(&x);
    aOut.foo(12l);
    
    Outer::Bar<int> aBar0(456);
    Outer::Bar<char const*> aBar1("def");
    Outer::Bar<int*> aBer2(&x);
}

Wandboxで確認する。

3-2.外側のクラスがクラス・テンプレートの場合

どうしても必要な時以外、使わない方が良いと思います。ルール自体は単純ですが、結果の複雑さは凄いです。

基本的なルールは第3回で解説したクラス・テンプレートのメンバ関数をクラス外で定義する時の記述方法と同じです。ただし、内側のテンプレートを明示的特殊化する時は、外側のクラスも明示的特殊化が必要です。部分的特殊化の場合は外側のクラスは明示的特殊化していなくても大丈夫です。(恐らく部分的特殊化してもよいだろうと思いますが、未確認です。)

上記の例でOuterをクラス・テンプレートにしてみました。
Wandboxで確認する。

clangだと結果が異なる!!
上記の3-2.のサンプルの外部定義について、clangでテストするとOuter::Bar<int*> aBer2(&x);で部分的特殊化ではなくプライマリー・テンプレートが実体化されました。しかし、内部定義だとgccと同じ結果です。
恐らくclangのバグと思いますが、たぶん誰も使わない機能だからバグが残っているのだと思います。
ここまで複雑な定義はできるだけ避けた方が良さそうです。

4.全部クラス内定義したい時

基本的にクラス外定義は可能ですね。しかし、全てクラス内定義したい時の方が実は多いと思います。
見ても分かるようにクラス内で書けた方が記述が簡単ですから。

メンバ関数テンプレートは、そもそも使わない方がよい明示的特殊化を使わなければOKですね。通常関数でオーバーロードしましょう。
クラス内のクラス・テンプレートは、部分的特殊化を使って事実上の明示的特殊化をすればOKです。ちょっと面倒ですけど、クラス外で書くよりは楽かも知れません。

#include <iostream>
#include <type_traits>
#include "typename.h"

template<typename tType>
struct Outer
{
    // オーバーロード0
    template<typename T>
    void foo(T t) { std::cout << TYPENAME(*this) << "::foo<" << TYPENAME(T) <<  ">(" << t << ")\n"; }
    // オーバーロード1
    template<typename T>
    void foo(T* t) { std::cout << TYPENAME(*this) << "::foo<" << TYPENAME(t) <<  ">(" << *t << ")\n"; }
    // オーバーロード(非テンプレート1)
    void foo(long t) { std::cout << TYPENAME(*this) << "::foo(long " << t << ")\n"; }
    // オーバーロード(非テンプレート2)
    void foo(char const* t) { std::cout << TYPENAME(*this) << "::foo(char const* " << t << ")\n"; }
   
    template<typename T, class tEnable=void>
    struct Bar
    {
        Bar(T t) { std::cout << TYPENAME(*this) << "::Bar<" << TYPENAME(T) <<  ">(" << t << ")\n"; }
    };
    
    // 擬似的な明示的特殊化
    template<typename T>
    struct Bar<T, typename std::enable_if<std::is_same<T, char const*>::value>::type>
    {
        Bar(char const* t) { std::cout << TYPENAME(*this) << "::Bar("
            << reinterpret_cast<void const*>(t) << ")\n"; }
    };
    
    // 部分的特殊化
    #define PARTIAL_INNER
    template<typename T>
    struct	Bar<T*, typename std::enable_if<!std::is_same<T, char const>::value>::type>
    {
        Bar(T* t) { std::cout << TYPENAME(*this) << "::Bar(" << *t << ") Inner\n"; }
    };
};

int main()
{
    Outer<int> aOut;
    aOut.foo(123);
    aOut.foo("abc");
    int x=789;
    aOut.foo(&x);
    aOut.foo(12l);
    
    Outer<short>::Bar<int> aBar0(456);
    Outer<short>::Bar<char const*> aBar1("def");
    Outer<short>::Bar<int*> aBer2(&x);
}

Wandboxで確認する。

5.特記事項:メンバ関数テンプレートは仮想関数に出来ません

テンプレートを使いこなしていくと、その内にメンバ関数テンプレートを仮想関数として基底クラスへのポインタ経由で呼び出したくなることがあります。でも、残念ながらこれはできません。

関数テンプレートは実体化(テンプレート・パラメータを全て確定)する度に、異なるコードが生成されます。
ですので、仮想関数にする場合それらのすべての異なるコード毎に仮想関数テーブル(vtable)へ登録する必要があります。
しかし、どの型で関数テンプレートが実体化されるのかは、実体化されるまで分かりませんから、事前に関数テーブルのエントリーを生成することができません。

では、明示的実体化しておけばよいと思いますね。しかし、そもそも実体化した関数テンプレートは、通常のオーバーロード関数群と事実上同じものです。通常の(テンプレートでない)オーバーロード関数群は仮想関数にできますから、これで対処すれば良いのです。

6.まとめ

今回はクラスの中で定義するメンバ・テンプレートについて解説しました。しかし、クラス内で定義するクラス・テンプレートはあまり使わないと思います。そもそもクラスないでクラスを定義すること自体が少ないですし。
メンバ関数テンプレートはそこそこ使います。結構便利です。しかし、そのプライマリー・テンプレートのオーバーロードや明示的特殊化することはまず無いと思います。明示的特殊化するなら普通にオーバーロードした方がよいですし。

ところで、今日はクリスマス・イブですね。イブって「前日」って意味があるのだなと思ってたのですが、Wikipediaを見るとそうでもなさそうです。「ユダヤ暦およびそれを継承する教会暦では、日没をもって日付の変り目とする」から前日の日没からがクリスマスだとか。つまりクリスマスの夜という意味らしいです。びっくり。

さて、次回は関数テンプレートに渡す引数について少し解説したいと思います。テンプレートならば配列の要素数を失わずに渡せます。また右辺値参照と左辺値参照について特別ルールがあります。私にとってちょっと鬼門のstd::forwardにチャレンジです。

来週は大晦日です。年末は何かと忙しいのでお休みし、次回は年明けの1月7日を予定しています。それでは、また来年もよろしく。良いお年を!!