テンプレートの特殊化 | Programming Place Plus C++編【言語解説】 第23章

トップページC++編

C++編で扱っている C++ は 2003年に登場した C++03 という、とても古いバージョンのものです。C++ はその後、C++11 -> C++14 -> C++17 -> C++20 -> C++23 と更新されています。
なかでも C++11 での更新は非常に大きなものであり、これから C++ の学習を始めるのなら、C++11 よりも古いバージョンを対象にするべきではありません。特に事情がないなら、新しい C++ を学んでください。 当サイトでは、C++14 をベースにした新C++編を作成中です。

この章の概要 🔗

この章の概要です。


特殊化 🔗

テンプレートでは、テンプレート仮引数に当てはめる具体的な型に応じて、普通のクラスや関数をインスタンス化します。こうして生成されるクラスや関数のことを、特殊化と呼びます。

ところで、あるクラステンプレートの設計を考えたとき、そのテンプレート仮引数に当てはめられる型の種類によっては、実装上、都合が悪いケースがあります。よくあるのは、次のような処理です。

template <typename T>
class DataStore {
pubilc:

    // 無関係のメンバは省略

    inline bool operator==(const DataStore& rhs) const
    {
        return mValue == rhs.mValue;
    }

private:
    T  mValue;
};

DataStore<>::operator== は、メンバ変数 mValue の値を ==演算子を使って比較します。基本的に間違っていませんが、もし、テンプレート仮引数 T の具体的な型が const char*型のように、C言語の文字列表現だったらどうでしょうか? この場合、std::strcmp関数によって比較される方が適切かもしれません。

この例のように、テンプレート仮引数の具体的な型によって、処理内容を変更したいことがあります。これを可能にするための仕組みとして、完全特殊化(明示的特殊化)部分特殊化が使えます。

完全特殊化や部分特殊化は、テンプレート仮引数に特定の型が当てはめられたときにだけ使われる、特別版を定義する機能です


完全特殊化 🔗

それではまず、完全特殊化の例を見ていきましょう。クラステンプレートの場合と、関数テンプレートの場合とがあるので、それぞれ個別に解説します。

クラステンプレートの完全特殊化 🔗

先ほどの DataStoreクラステンプレートの例を解決してみます。

#include <iostream>
#include <cstring>

template <typename T>
class DataStore {
public:
    explicit DataStore(const T& value) :
        mValue(value)
    {}

    inline bool operator==(const DataStore& rhs) const
    {
        return mValue == rhs.mValue;
    }

private:
    T    mValue;
};

template <>
class DataStore<const char*> {
public:
    explicit DataStore(const char* value) :
        mValue(value)
    {}

    inline bool operator==(const DataStore& rhs) const
    {
        return std::strcmp(mValue, rhs.mValue) == 0;
    }

private:
    const char*    mValue;
};


int main()
{
    const char s1[] = "abc";
    const char s2[] = "abc";

    DataStore<const char*> ds1(s1);
    DataStore<const char*> ds2(s2);

    std::cout << std::boolalpha
              << (ds1 == ds2)
              << std::endl;
}

実行結果:

true

完全特殊化を行う際には、まず、通常のテンプレートの定義を用意しておきます。これはこれまでの章とまったく同じように作ればいいです。こうして用意したテンプレートのことを、1次テンプレート(プライマリテンプレート)と呼びます。

完全特殊化のためのクラス定義を別途用意します。これは、1次テンプレートが見える位置に書きます

1次テンプレートと同じ名前のクラスを定義しますが、このとき冒頭に「template <>」を付けることで、完全特殊化を行っていることを表す必要があります。また、クラス名の後ろに、テンプレート仮引数に当てはめられる具体的な型名を指定します。これを記述することで、この型がテンプレート仮引数に当てはめられたときにだけ、この完全特殊化されたクラスが使用されるようになります。

あとは、1次テンプレートと同じ意味合いの内容になるように、メンバを書いていきます。感覚として意識して欲しいのは、1次テンプレート側と違い、完全特殊化のためのクラスは、具体的な型を使って記述されるという点です。テンプレート仮引数に当てはめられる型は、すでに決定しているのですから、T のようなテンプレート仮引数は登場しません。

テンプレートを使用する側は、特に何も意識する必要がありません。テンプレート実引数に const char*型を指定した場合にだけ、完全特殊化された DataStoreクラスが使用され、それ以外の型を指定した場合には、DataStoreクラステンプレートをその型を使ってインスタンス化したクラスが使用されます。

実行結果を見ると分かるように、内容は同一だがメモリアドレスが異なる2つの文字列の比較で、true という結果になっています。これは、==演算子ではなく std::strcmp関数が使用されているからです。試しに、完全特殊化のコードだけをコメントアウトして試すと、結果は false に変わります。

関数テンプレートの完全特殊化 🔗

今度は、関数テンプレートの完全特殊化の例を挙げます。

#include <iostream>
#include <string>
#include <cstring>

template <typename T>
inline std::size_t Length(const T* str)
{
    return str->length();
}

template <>
inline std::size_t Length<char>(const char* str)
{
    return std::strlen(str);
}


int main()
{
    const std::string s1("aaa");
    const char s2[] = "xxxx";

    std::cout << Length(&s1) << "\n"
              << Length(s2) << std::endl;
}

実行結果:

3
4

このサンプルプログラムは、文字列の長さを調べる Length関数テンプレートを定義しています。実引数に指定したポインタ経由で、lengthメンバ関数を呼び出して結果を得ようとしていますが、const char*型で表現された文字列の場合には、当然ながらメンバ関数がありません。そこで、完全特殊化を利用して、const char*型の場合にだけ、std::strlen関数を使わせようとしています。

関数テンプレートの場合の完全特殊化は、クラステンプレートの場合とほぼ同様で、1次テンプレートとなる関数テンプレートを定義し、これが見える位置に、完全特殊化のための関数を定義します関数名を同じにすることや、冒頭に「template <>」が必要である点も、クラステンプレートの場合と同様です

完全特殊化のための定義の方には、関数名の後ろに、テンプレート仮引数に当てはめる具体的な型名を指定します。これも、クラステンプレートの場合と同様ではありますが、関数テンプレートの場合は、実引数から型を推測しますから(第9章)、それによってコンパイラが判断可能であれば省略できます。省略する場合は、次のようになります。

template <>
inline std::size_t Length(const char* str)
{
    return std::strlen(str);
}

省略しなかった場合の指定は「<const char*>」ではなくて「<char>」であることに注目してください。1次テンプレートの方の仮引数の型は「const T*」ですが、ここに const char*型の実引数を与えると、テンプレート仮引数 T は「char」と判断されます。「const」も「*」もすでに T の外側に付いていますから、T に当てはめられる型はあくまで「char」なのです。したがって、完全特殊化の側のテンプレート実引数は「char」が正解です。

ところで、Length関数テンプレートの仮引数がポインタなのが少し気にならないでしょうか? 恐らく、参照の方がシンプルで安全だと思われます。しかし、仮に以下のように参照に直すと、コンパイルが通らなくなります(呼び出し側も修正したとしても)。

template <typename T>
inline std::size_t Length(const T& str)
{
    return str.length();
}

template <>
inline std::size_t Length<char>(const char* str)
{
    return std::strlen(str);
}

これは、完全特殊化の方ではポインタが使われているため、引数の型が一致しないためです。完全特殊化はあくまでも、テンプレート仮引数の部分を具体化するだけですから、それ以外の部分が変わってはいけません

このケースでは、完全特殊化ではなくて、単に関数をオーバーロードすればいいでしょう。

template <typename T>
inline std::size_t Length(const T& str)
{
    return str.length();
}

inline std::size_t Length(const char* str)
{
    return std::strlen(str);
}

これなら問題ありません。関数テンプレートと通常の関数があり、どちらにも適合するときには、通常の関数の方が優先される第9章)ので、きちんと動作します。


部分特殊化 🔗

少し分かりづらいのですが、部分特殊化にはいくつかのパターンがありますが、どれも一言で言えば、「テンプレート仮引数の一部だけを特殊化する」ということです。

テンプレート仮引数のうちの一部分だけしか特殊化されないので、言い換えると、特殊化されていないテンプレート仮引数が残っているということです。そのため、完全特殊化する場合と違い、部分特殊化によって作られるものはクラスではなく、クラステンプレートのままです

1つ目のパターンは、テンプレート仮引数の型の具体性を高めるような特殊化で、たとえば、T型のテンプレート仮引数に対して、T*型で特殊化するというものです。この場合、指定されたテンプレート実引数がポインタだった場合にだけ、特殊化されるということになります。

2つ目のパターンは、2個以上あるテンプレート仮引数のうちの1個以上に関して、具体的な型を想定するもので、たとえば、T型と U型のテンプレート仮引数に対して、T は何でも構わないが、U が double のときには特殊化するというものです。

3つ目のパターンは、テンプレート仮引数の個数自体を変えてしまうもので、たとえば、T型のテンプレート仮引数に対して、T[N]型で特殊化するとき、N を新たなノンタイプテンプレート仮引数(第22章)に追加できます。

なお、部分特殊化は、クラステンプレートにのみ可能であり、関数テンプレートに対しては行えません。関数テンプレートの場合の一部は、オーバーロードを使うことで代替できます。

【上級】関数の戻り値の型を指定するためにテンプレート仮引数を使うような関数テンプレートの場合、オーバーロードでは引数の型や個数でしか区別を付けられないので、対応できません。この場合は、クラステンプレートを用意して、そちらで部分特殊化し、そのメンバ関数を呼ぶように実装すれば対応可能です。

それでは、部分特殊化の例を見ていきましょう。

T型を T*型で部分特殊化する 🔗

まず、1つ目のパターンの例として、T型を T*型で部分特殊化します。

#include <iostream>

// 1次テンプレート
template <typename T>
class DataStore {
public:
    explicit DataStore(const T& value) :
        mValue(value)
    {}

    void Print() const;

private:
    T    mValue;
};

template <typename T>
void DataStore<T>::Print() const
{
    std::cout << mValue << std::endl;
}


// 部分特殊化
template <typename T>
class DataStore<T*> {
public:
    explicit DataStore(T* value) :
        mValue(value)
    {}

    void Print() const;

private:
    T*    mValue;
};

template <typename T>
void DataStore<T*>::Print() const
{
    std::cout << *mValue << std::endl;
}



int main()
{
    int num = 20;

    DataStore<int> ds1(10);
    DataStore<int*> ds2(&num);

    ds1.Print();
    ds2.Print();
}

実行結果:

10
20

DataStore<>::Printメンバ関数は、保持している値を出力しますが、ポインタ型を保持しているときには、指し示す先の値を出力します。

前述したとおり、部分特殊化が作り出すのはクラステンプレートなので、classキーワードの手前の「template <typename T>」はそのまま必要です。「template <>」だと、完全特殊化を意味するので注意してください。

複数個あるテンプレート仮引数の一部を特殊化する 🔗

次の例は、2つあるテンプレート仮引数のうちの1つだけを特殊化するものです。

#include <iostream>
#include <cassert>


// 1次テンプレート
template <typename T, std::size_t CAPACITY>
class Stack {
public:
    static const std::size_t CAPACITY = CAPACITY;

public:
    Stack();
    ~Stack();

    void Push(const T& data);
    void Pop();
    inline const T Top() const
    {
        return mData[mSP - 1];
    }

    inline std::size_t GetSize() const
    {
        return mSP;
    }

private:
    T                      mData[CAPACITY];
    int                    mSP;
};


template <typename T, std::size_t CAPACITY>
Stack<T, CAPACITY>::Stack() :
    mSP(0)
{
}

template <typename T, std::size_t CAPACITY>
Stack<T, CAPACITY>::~Stack()
{
}

template <typename T, std::size_t CAPACITY>
void Stack<T, CAPACITY>::Push(const T& data)
{
    assert(static_cast<std::size_t>(mSP) < CAPACITY);
    mData[mSP] = data;
    mSP++;
}

template <typename T, std::size_t CAPACITY>
void Stack<T, CAPACITY>::Pop()
{
    assert(mSP > 0);
    mSP--;
}



// 部分特殊化
template <std::size_t CAPACITY>
class Stack<bool, CAPACITY> {
public:
    static const std::size_t CAPACITY = CAPACITY;

public:
    Stack();
    ~Stack();

    void Push(const bool& data);
    void Pop();
    inline const bool Top() const
    {
        return (mData[GetDataIndex(mSP - 1)] & (1 << GetDataBit(mSP - 1))) != 0;
    }

    inline std::size_t GetSize() const
    {
        return mSP;
    }

private:
    typedef unsigned int data_t;

    static const unsigned int DATA_BITS = sizeof(data_t) * 8;
    static const std::size_t DATA_ARRAY_SIZE = (CAPACITY / DATA_BITS) + 1;

    inline std::size_t GetDataIndex(int sp) const
    {
        return sp / DATA_BITS;
    }
    inline std::size_t GetDataBit(int sp) const
    {
        return sp % DATA_BITS;
    }

private:
    data_t                 mData[DATA_ARRAY_SIZE];
    int                    mSP;
};


template <std::size_t CAPACITY>
Stack<bool, CAPACITY>::Stack() :
    mSP(0)
{
}

template <std::size_t CAPACITY>
Stack<bool, CAPACITY>::~Stack()
{
}

template <std::size_t CAPACITY>
void Stack<bool, CAPACITY>::Push(const bool& data)
{
    assert(static_cast<std::size_t>(mSP) < CAPACITY);

    if (data) {
        mData[GetDataIndex(mSP)] |= (1 << GetDataBit(mSP));
    }
    else {
        mData[GetDataIndex(mSP)] &= ~(1 << GetDataBit(mSP));
    }
    mSP++;
}

template <std::size_t CAPACITY>
void Stack<bool, CAPACITY>::Pop()
{
    assert(mSP > 0);
    mSP--;
}



int main()
{
    static const int SIZE = 5;

    typedef Stack<int, SIZE> IntStack;
    typedef Stack<bool, SIZE> BoolStack;

    IntStack iStack;
    BoolStack bStack;

    for (std::size_t i = 0; i < SIZE; ++i) {
        iStack.Push(static_cast<int>(i));
        bStack.Push((i & 1) ? true : false);
    }

    for (std::size_t i = 0; i < SIZE; ++i) {
        std::cout << iStack.Top() << std::endl;
        iStack.Pop();
        std::cout << std::boolalpha << bStack.Top() << std::endl;
        bStack.Pop();
    }
}

実行結果:

4
false
3
true
2
false
1
true
0
false

要素数の上限が固定化された Stackクラステンプレートです。

クラステンプレートは2つあり、1つは要素の型、もう1つは要素数の上限値です。ここで、要素の型を表すテンプレート仮引数を bool型と指定した場合に、内部配列の管理を工夫して、1ビットに 1要素の情報を持つように特殊化しています。

標準ライブラリの vector が、vector<bool> の場合にしていることと、考え方は同じです(【標準ライブラリ】第5章)。

テンプレート仮引数の個数が増えるパターン 🔗

次の例は、テンプレート仮引数の個数が変わるパターンです。

#include <iostream>


// 1次テンプレート
template <typename T>
class DataStoreArray {
public:
    explicit DataStoreArray(std::size_t size) :
        mValueArray(new T[size])
    {}

    ~DataStoreArray()
    {
        delete [] mValueArray;
    }

    inline T operator[](std::size_t index) const
    {
        return mValueArray[index];
    }

    inline T& operator[](std::size_t index)
    {
        return mValueArray[index];
    }

private:
    T*    mValueArray;
};


// 部分特殊化
template <typename T, std::size_t SIZE>
class DataStoreArray<T[SIZE]> {
public:
    DataStoreArray()
    {}

    ~DataStoreArray()
    {}

    inline T operator[](std::size_t index) const
    {
        return mValueArray[index];
    }

    inline T& operator[](std::size_t index)
    {
        return mValueArray[index];
    }

private:
    T    mValueArray[SIZE];
};




int main()
{
    static const int SIZE = 5;

    DataStoreArray<int> iStoreArray(SIZE);
    DataStoreArray<int[SIZE]> iStoreArray2;

    for (int i = 0; i < SIZE; ++i) {
        iStoreArray[i] = i * 10;
        iStoreArray2[i] = i * 10;
    }

    for (int i = 0; i < SIZE; ++i) {
        std::cout << iStoreArray[i] << " " << iStoreArray2[i] << "\n";
    }
    std::cout << std::endl;
}

実行結果:

0 0
10 10
20 20
30 30
40 40

複数の要素を配列管理するクラステンプレートです。通常は、指定された型の要素を動的な配列を使って管理しますが、要素数が分かっている配列型を指定した場合には、静的な配列を使った実装を使用します。

コンテナ 🔗

標準ライブラリには、STL (Standard Template Library) と呼ばれる、テンプレートを活用した機能群があります。この章までに、関数テンプレートやクラステンプレートに関する基本的な知識を得られたので、STL を少しずつ理解していける段階に来たはずです。

まずは、コンテナ(STLコンテナ)について学んでみましょう。【標準ライブラリ】第4章に進み、概要を知ったら、第5章第13章まで進めてください。

続けて、第14章に進み、イテレータという概念について理解してください。

なお、【標準ライブラリ】編では、ほぼ全機能を網羅するリファレンスのようなページ作りをしているので、すべてを理解するのではなく、ざっくりとした概要を学ぶ程度で良いでしょう。特に、【言語解説】編の本章までに解説されていない概念が登場することもあるので、その場合は、単に読み飛ばして良いです。


練習問題 🔗

問題① 部分特殊化の1つ目のパターン「T型を T*型で部分特殊化する」において、ポインタであるかそうでないかによって、printメンバ関数の実装を変えられることを示しました。同様のことを、関数テンプレートで行うとすれば、どう実装しますか?

問題② 標準ライブラリに含まれる STLコンテナ、「vector」「list」「deque」「set」「map」は、それぞれどんなデータ構造を提供しますか?


解答ページはこちら

参考リンク 🔗


更新履歴 🔗

 新規作成。



前の章へ (第22章 テンプレート仮引数)

次の章へ (第24章 入れ子クラスとローカルクラス)

C++編のトップページへ

Programming Place Plus のトップページへ



はてなブックマーク に保存 Pocket に保存 Facebook でシェア
X で ポストフォロー LINE で送る noteで書く
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー
先頭へ戻る