コンテンツにスキップ

特殊化

テンプレートでは実引数に応じて関数やクラスを生成します。

// 関数テンプレート
template <typename T>
T Sum(T a, T b) {
    return a + b;
}

// 関数テンプレートの関数の呼び出し
Sum<int>(1, 2);

Sum<int>(1, 2) という関数テンプレートの関数の呼び出しによって Tint である関数が必要と判断され、次の関数が生成されます。

int Sum(int a, int b) {
    return a + b;
}

このようにテンプレートを使用する箇所において、 関数テンプレートから関数を生成することおよび クラステンプレートからクラスを生成することを特殊化 (または暗黙的インスタンス化) といいます。

特殊化はコンパイラによって行われるため、 ヘッダファイルで関数テンプレートを使用する場合にはそのヘッダファイルで定義も行います。

#ifndef SUM_H_
#define SUM_H_

template <typename T>
inline T Sum(T a, T b) {  // inline 指定が必要
    return a + b;
}

#endif  // SUM_H_
#include <iostream>

#include "sum.h"

int main() {
    std::cout << Sum(1, 2) << std::endl;
    return 0;
}
テンプレートの明示的インスタンス化とextern template

ヘッダーファイルなどのテンプレートを使用する箇所で関数やクラスを生成すると、 コンパイル速度が低下してしまいます。 ヘッダファイルでは宣言だけ行い、ソースファイルで明示的に関数やクラスを生成することで 予めtemplateを実体化できるのでコンパイル速度が向上します。

#ifndef SUM_H_
#define SUM_H_

template <typename T>
T Sum(T a, T b);  // 宣言だけ行う (inline もつけない)

#endif  // SUM_H_
#include "sum.h"

// 関数テンプレートの定義
template <typename T>
T Sum(T a, T b) {
    return a + b;
}

// 明示的インスタンス化
template int Sum<int>(int, int);

こうした構成にすると使用可能な型はソースファイルで明示的な生成を行う型のみとなってしまいます。 たとえば Sum<double>(double, double) は生成されていないため Sum(1.2, 3.4) のように関数テンプレートの関数を呼び出すとリンクエラーになります。 かといって明示的なインスタンス化を増やしていくと、それを共有ライブラリにすることを考えた場合、 ライブラリサイズが肥大化してしまいます。

こうした問題を避けるためには、extern templateを利用します。 通常通りヘッダファイルでtemplate関数/クラスの定義を行うのですが 実体化する頻度が高いものだけをexternし、ソースファイルで実体化させます

#ifndef SUM_H_
#define SUM_H_

// 関数テンプレートの定義
template <typename T>
T Sum(T a, T b) {
    return a + b;
}

//暗黙的実体化を阻止
extern template int Sum<int>(int, int);
#endif  // SUM_H_
#include "sum.h"

//よく使うものだけ実体化させる
template int Sum<int>(int, int);

完全特殊化

関数テンプレートやクラステンプレートでは、すべてのパラメータが確定した時に別の定義を書くことができます。 これによって特定のテンプレート引数に対する挙動を変更することができます。 これを完全特殊化 (または明示的特殊化) といいます。

関数テンプレートの完全特殊化は次のようにします。

template <typename T>
T DoSomething(T a, T b) {
    return a + b;
}

template <>
double DoSomething<double>(double a, double b) {
    return a * b;
}

std::cout << DoSomething(2, 3) << std::endl;  // 5
std::cout << DoSomething(2.0, 3.0) << std::endl;  // 6

関数の前に template <> を付けて完全特殊化を行うことを指定し、 関数名の後に < ... > で対象となるテンプレート引数を指定します。

クラステンプレートの完全特殊化も同様です。

template <typename T>
class Array {
 public:
    explicit Array(int size)
        : size_(size),
          data_(new T[size_]) {}

    ~Array() {
        delete[] data_;
    }

    int Size() const {
        return size_;
    }

 private:
    const int size_;
    T* data_;
};

template <>
class Array<bool> {
 public:
    explicit Array(int size)
        : size_(size),
          data_size_((size - 1) / 8 + 1),
          data_(new uint8_t[data_size_]) {}

    ~Array() {
        delete[] data_;
    }

    int Size() const {
        return size_;
    }

 private:
    const int size_;
    const int data_size_;
    uint8_t* data_;
};

この例では8個の bool 値を1個の uint8_t で扱って省メモリ化するために、 bool に対する完全特殊化を行っています。

部分特殊化

クラステンプレートの一部のテンプレート引数を確定させたり、制限することができます。これを部分特殊化といいます。

詳細は テンプレートの部分特殊化 - cppreference.com

を参照してください。

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

部分特殊化はクラステンプレートに対してのみ行なえます。しかし関数テンプレートに対しても行いたいことがあります。
その場合はSFINAE(Substitution Failure Is Not An Error)を利用した書き方をします

template <bool cond, typename T>
using enable_if_t = typename std::enable_if<cond, T>::type;
template <typename T, enable_if_t<(なんか条件式), std::nullptr_t> = nullptr>
void foo(T t) {}
template <typename T, enable_if_t<!(なんか条件式), std::nullptr_t> = nullptr>
void foo(T t) {}

std::enable_ifを使ってオーバーロードする時、enablerを使う? - Qiita

あるいはC++17で追加されたconstexpr if 文を利用することができます。