テンプレートのインスタンス化と特殊化 | Programming Place Plus 新C++編

トップページ新C++編

先頭へ戻る

このページの概要 🔗

このページでは、テンプレートのインスタンス化に関するルールと、特定のテンプレート実引数の組み合わせのときに異なる実装の実体を生成する、テンプレートの特殊化について説明します。テンプレートに関する少し高度な話題です。

このページの解説は C++14 をベースとしています

以下は目次です。要点だけをさっと確認したい方は、「まとめ」をご覧ください。



テンプレートのインスタンス化 🔗

クラステンプレート」のページや、「関数テンプレート」のページでも触れたように、テンプレートにテンプレート実引数を与えることによって、テンプレート仮引数で表現されたテンプレートの空欄部分が埋められて、本物のクラスや関数のコードが生成されます。これをテンプレートのインスタンス化(実体化、具現化) (template instantiation) といいます。

また、関数テンプレートのインスタンス化によって生成された関数を、インスタンス化された関数 (instantiated function) 、クラステンプレートのインスタンス化によって生成されたクラスを、インスタンス化されたクラス (instantiated class) といいます。そのほかのもの(変数やメンバ関数など)でも同様の呼び方をします。

暗黙的インスタンス化 🔗

コンパイラがテンプレートのインスタンス化をおこなうタイミングは、テンプレートの実体が実際に必要になったときです。クラステンプレートの場合なら、C<int> c; のように変数を宣言しようとすると、クラステンプレートC にテンプレート実引数int を与えた場合の実体が必要になるので、ここでインスタンス化が起きます。

template <typename T>
class C {
    // ...
};

C<int> c {};  // ここをコンパイルするときに、C<int> がインスタンス化される

一方で、ポインタを宣言したり、typedef名を宣言したりするような、まだ実体がなくても問題ない状況ではインスタンス化は起こりません。

using IntC = C<int>;  // インスタンス化は不要
C<int>* pc {};        // インスタンス化は不要

クラステンプレートがインスタンス化されたとしても、そのメンバ関数や静的データメンバ、Scoped enum などのように、インスタンス化されないものもあります[1]。こういったメンバ達は、実際に使おうとしなければ実体が必要にならないからです。

クラステンプレートのメンバ関数や、関数テンプレート、メンバ関数テンプレートでは、実際に呼び出しをおこなうコードがあるとインスタンス化されます。

template <typename T>
class C {
public:
    void f();
};

template <typename T>
T f(T v)
{
    // ...
}

int main()
{
    C<int> c {};              // C<int> がインスタンス化される。ここでは C<int>::f() はインスタンス化されない
    c.f();                    // C<int>::f() がインスタンス化される

    auto res1 = f<int>(100);  // f<int> がインスタンス化される
    auto res2 = f(3.33);      // f<double> がインスタンス化される 
}

インスタンス化が行われるきっかけとなるプログラム上の位置を、インスタンス化位置(具現化位置) (point of instantiation) と呼びます。

このように、必要にならないかぎりインスタンス化を行わないことによって、生成されるコード量を削減し、プログラムサイズを小さく抑えたり、コンパイルにかかる時間を短く抑えたりしています。

必要になるまでインスタンス化が行われないため、プログラマーからみると文法上おかしなところがあるようにみえても、エラーが検出されないことがあります。たとえば次のプログラムでは、C<T>::fメンバ関数の本体で m_value という変数を使っていますが、そのような変数はどこにもありません。しかし、C<T>::fメンバ関数がインスタンス化されることがないため、プログラムはコンパイルできます。

template <typename T>
class C {
public:
    void f()
    {
        m_value *= 2;  // m_value はどこにもない
    }
};

int main()
{
    C<int> c {};  // C<T> はインスタンス化されるが、C<T>::f() はインスタンス化されない
}

C<T>::fメンバ関数を呼び出すコードが追加された途端にコンパイルエラーになります。

template <typename T>
class C {
public:
    void f()
    {
        m_value *= 2;  // エラー。m_value はどこにもない
    }
};

int main()
{
    C<int> c {};
    c.f();  // C<int>::f() がインスタンス化される
}

ここまでに説明した方法で行われるインスタンス化は、暗黙的インスタンス化 (implicit instantiation) と呼ばれます。

明示的インスタンス化 🔗

同じ翻訳単位📘に同じテンプレートのインスタンス化が存在することは、定義の重複によるエラーとなります。また、異なる翻訳単位に同じインスタンスが作られることは、プログラムサイズの面でも、ビルドにかかる時間の面でも無駄になります。このような無駄を回避する方法として、明示的インスタンス化 (explicit instantiation)が存在します。

明示的インスタンス化とは、文字通り明示的にインスタンス化を行わせる方法で、次のような文法になっています。

template テンプレートの宣言;
extern template テンプレートの宣言;

extern がないほうは明示的インスタンス化の定義 (explicit instantiation definition)、extern があるほうは明示的インスタンス化の宣言 (explicit instantiation declaration) と呼びます。「テンプレートの宣言」のところにテンプレート実引数を付けた宣言を書きます。

具体的なコードは次のようになります。

// 以下は定義
// C<T> を明示的インスタンス化
template class C<int>;

// C<T>::f() を明示的インスタンス化
template void C<int>::f();

// f<T>() を明示的インスタンス化
template int f<int>(int v);
template int f(int v);  // 引数から判断できるので <int> を省略しても構わない


// 以下は上記に対応する宣言
extern template class C<int>;
extern template void C<int>::f();
extern template int f<int>(int v);
extern template int f(int v);

【上級】クラステンプレートのメンバ関数や、関数テンプレートを明示的インスタンス化するときには、inline や constexpr を付加できません[2]

明示的インスタンス化の定義によって、指定したテンプレート実引数を使ってテンプレートがインスタンス化されます。明示的インスタンス化の宣言は、明示的インスタンス化の定義によって生成される実体が存在することを宣言するだけで、インスタンス化は行われません。つまり普通の変数や関数における定義と宣言と同じことで、どこかに1つだけある定義(実体)と、0個以上の宣言という構図です。

これまでのページの例では、プログラム全体で使えるようなテンプレートを作るためには、クラステンプレートのメンバ関数の定義や、関数テンプレートの定義をヘッダファイルに記述する必要がありました。ソースファイル(.cpp) の側で明示的インスタンス化の定義をおこなうことによって、そのテンプレートの定義をソースファイル(.cpp) の側に置くことが可能になります。ただし、テンプレート実引数の組み合わせは限定されることになります。

//template.h
#ifndef TEMPLATE_H_INCLUDED
#define TEMPLATE_H_INCLUDED

template <typename T>
class C {
public:
    C(T value);
    T get_value() const;

private:
    T  m_value;
};

#endif
//template.cpp
#include "template.h"

template <typename T>
C<T>::C(T value) : m_value {value}
{}

template <typename T>
T C<T>::get_value() const
{
    return m_value;
}


// C<int> を明示的インスタンス化(定義)
template class C<int>;
//main.cpp
#include <iostream>
#include "template.h"

int main()
{
    C<int> c {123};
    std::cout << c.get_value() << "\n";
}

実行結果:

123

template.h で定義しているクラステンプレートのメンバ関数の定義は、template.cpp の側にありますが、main.cpp で C<int> をインスタンス化し、get_valueメンバ関数を呼び出すこともできています。これを可能にしたのは、template.cpp にある template class C<int>; という明示的インスタンス化の定義の記述です。これがなければ(コンパイルエラーではなく)リンクエラーになります。また、明示的インスタンス化の定義は C<int> についてのものであるため、main.cpp で使おうとするものがたとえば C<double> であったら、やはりリンクエラーとなります。必要なだけ明示的インスタンス化の定義を書くことはできますが、新しい定義が必要になるたびに書き足していかなければなりません。

明示的インスタンス化は、同じ実体が複数作られることを防ぐためにも使えます。暗黙的インスタンス化では、C<int> を main.cpp と sub.cpp で使おうとすると、それぞれの翻訳単位に実質的に同じ実体が別個に作られるため、プログラムサイズを無駄に増やす結果になります。明示的インスタンス化を使って、1つの実体(定義)と、その存在を示す宣言を用意することで、実体はプログラム全体でただ1つになり無駄をなくせます。

//template.h
#ifndef TEMPLATE_H_INCLUDED
#define TEMPLATE_H_INCLUDED

template <typename T>
class C {
public:
    C(T value);
    T get_value() const;

private:
    T  m_value;
};

// 明示的インスタンス化の宣言
extern template class C<int>;

#endif
//template.cpp
#include "template.h"

template <typename T>
C<T>::C(T value) : m_value {value}
{}

template <typename T>
T C<T>::get_value() const
{
    return m_value;
}


// C<int> を明示的インスタンス化(定義)
template class C<int>;
//main.cpp
#include <iostream>
#include "template.h"

extern void sub();

int main()
{
    // C<int> は、template.h にある明示的インスタンス化の宣言を通して、
    // template.cpp にある実体を使う
    C<int> c {123};

    std::cout << "main: " << c.get_value() << "\n";
    sub();
}
//sub.cpp
#include <iostream>
#include "template.h"

void sub()
{
    // C<int> は、template.h にある明示的インスタンス化の宣言を通して、
    // template.cpp にある実体を使う
    C<int> c {15};

    std::cout << "sub: " << c.get_value() << "\n";
}

実行結果:

main: 123
sub: 15

template.h に明示的インスタンス化の宣言を追加したことによって、main.cpp と sub.cpp が必要としている C<int> の実体は、template.cpp にある明示的インスタンス化の定義で作られる実体を共通して使用することになります。

特殊化 🔗

このページのもう1つのテーマが、(テンプレートの)特殊化 (template specialization) です。特殊化とは、テンプレートのインスタンス化か、明示的特殊化によって生成された実体のことです[3]明示的特殊化についてはこのあと説明します

雛形であるテンプレートにはまだ定まっていない部分(テンプレート仮引数のままである部分)が残されています。これに対して、そうした未確定な部分が具体的ななにかで確定された結果(実体)のことを、特定の型や値に特化されたものであると捉えて、特殊化と呼んでいます。

特殊化は以下のように分類されます。リンクはこのあとの解説のところに移動します。

暗黙的特殊化 🔗

暗黙的特殊化 (implicit specialization) は、暗黙的インスタンス化による特殊化のことです。つまり、これといって特別なことを意識せず、テンプレート実引数を指定(あるいは推論)した結果、生成される特殊化です。

明示的特殊化(完全特殊化) 🔗

明示的特殊化 (explicit specialization) は、与えたテンプレート実引数の内容に応じて、元のテンプレートとは異なる内容の実体として作りだされる特殊化です。あとで取り上げる部分特殊化との対比で、完全特殊化 (full specialization) と呼ぶこともあります。

template <typename T> のようなテンプレート仮引数をもったテンプレートがあるとします。ここまでに解説してきたテンプレートでは、T の部分に具体的に何が指定されても、使われるテンプレートのコードは同じものでした。しかし、T に指定したものに応じてコード自体を変えられると便利な場合もあります。これを実現するのが明示的特殊化です。

明示的特殊化をおこなうには、まず元となるテンプレートが必要です。このテンプレートのことをプライマリテンプレート (primary template) と呼びます。そのうえでプライマリテンプレートと同じ名前のテンプレートを、template <> のように、空の <> を使って定義します。また、プライマリテンプレートのテンプレート仮引数に当てはめる型や、値(ノンタイプテンプレートの場合)を、クラス名や関数名の直後のところで指定します。

// プライマリテンプレート
template <typename T>
class C {
    // ...
};

// T == double の場合の明示的特殊化
template <>
class C<double> {
    // ...
};

関数テンプレートでも同様です。

// プライマリテンプレート
template <typename T>
T f()
{
    // ...
}

// T == int* の場合の明示的特殊化
template <>
int* f<int*>()
{
    // ...
}

変数テンプレートの場合も同じ方法で記述します。

// プライマリテンプレート
template <typename T>
T value {};

// T == std::string の場合の明示的特殊化
template <>
std::string value<std::string> {};

いずれの場合も、テンプレート仮引数 T に対して特定の型を与えられたときに、明示的特殊化されたバージョンがインスタンス化されます。ほかの型を与えたときはプライマリテンプレートのほうがインスタンス化されます。ノンタイプテンプレートなら、特定の値を与えられたときに明示的特殊化されたバージョンがインスタンス化されるようにできます。

プライマリテンプレートにデフォルトテンプレート実引数があれば(template <typename T = int>)、明示的特殊化のテンプレート実引数の指定を <> にすることは可能です(T == int なので、int のときに明示的特殊化される)。また、関数テンプレートの場合、テンプレート実引数推論による推論があるので、template <typename T> void f(T v); というプライマリテンプレートに対して、template <> void f(int v); のように書けます。

このように明示的特殊化を使うと、同じ名前のテンプレートでありながら、与えた型や値に応じて異なる実体(実装)を使えます。これはテンプレートにおけるオーバーロードであると考えると理解しやすいかもしれません(「オーバーロード」のページを参照)。たとえば、2つの引数の一致を調べる equal関数テンプレートを作るとき、引数が const char* で表現される生の文字列のときには == によるポインタとしての比較ではなく、std::strcmp関数(「配列」のページを参照)による実際の文字列の中身による比較を使いたいと考えたとします。これは次のように const char*型の場合を特別バージョンとした明示的特殊化を行って実現できます。

#include <cstring>
#include <iostream>

template <typename T>
bool equal(const T a, const T b)
{
    return a == b;
}

template <>
bool equal(const char* a, const char* b)
{
    return std::strcmp(a, b) == 0;
}

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

    std::cout << std::boolalpha
              << equal(100, 100) << "\n"
              << equal(100, 120) << "\n"
              << equal(s1, s2) << "\n"
              << equal(s1, s1) << "\n";
}

実行結果:

true
false
true
true

もし、T == char として作られた明示的特殊化バージョンが存在しなければ、s1s2 の比較はメモリアドレスの一致を調べるものになりますから、3つ目の equal関数テンプレートの呼び出しの結果は false になります。

部分特殊化 🔗

部分特殊化 (partial specialization) もテンプレート実引数の内容に応じて、元のテンプレートとは異なる内容のコードを使うようにするものですが、明示的特殊化(完全特殊化)と違って、あらたに定義する特別バージョンにまだテンプレート仮引数が残っている場合を指します。テンプレート仮引数が残っているので、これはまだテンプレートであって、完全な特殊化とは呼べないため、部分特殊化といいます。

なお、部分特殊化ができるのはクラステンプレートだけです。

【C++23】変数テンプレートを部分特殊化ができるようになりました[4]

部分特殊化をおこなうには、明示的特殊化と同じようにプライマリテンプレートと同じ名前のテンプレートを定義します。ここで、テンプレート仮引数が1つでも残っていれば部分特殊化とみなされます。

// プライマリテンプレート
template <typename T1, typename T2>
class C {
    // ...
};

// T1 == double の場合の部分特殊化
template <typename T2>
class C<double, T2> {
    // ...
};

// T1 == double、T2 == double。これは完全特殊化
template <>
class C<double, double> {
    // ...
};

また、「テンプレート仮引数がポインタの場合」のような特殊化のしかたも許されます。

// プライマリテンプレート
template <typename T>
class C {
    // ...
};

// T がポインタ型の場合の部分特殊化
template <typename T>
class C<T*> {
    // ...
};

// T が参照型の場合の部分特殊化
template <typename T>
class C<T&> {
    // ...
};

引数の値を出力する関数を作ることを考えます。通常は単に値を出力するだけですが、ポインタ型のときには、指し示す先にある値を出力するために部分特殊化を利用してみます。

#include <iostream>

template <typename T>
class ValuePrinter {
public:
    void print(T value) const
    {
        std::cout << value << "\n";
    }
};

template <typename T>
class ValuePrinter<T*> {
public:
    void print(T* value) const
    {
        std::cout << *value << "\n";
    }
};

int main()
{
    ValuePrinter<int> int_printer {};
    int_printer.print(50);

    ValuePrinter<int*> pint_printer {};
    int value {123};
    pint_printer.print(&value);
}

実行結果:

50
123

関数テンプレートでは部分特殊化ができませんが、同様のことをしたければ関数のオーバーロードで代用します。

#include <iostream>

template <typename T>
void print(T value)
{
    std::cout << value << "\n";
}

template <typename T>
void print(T* value)
{
    std::cout << *value << "\n";
}

int main()
{
    print(50);

    int value {123};
    print(&value);
}

実行結果:

50
123

再帰テンプレート 🔗

テンプレートは再帰的にインスタンス化できます。

次の関数テンプレートは階乗を計算します。

#include <iostream>

template <int N>
int factorial()
{
    return N * factorial<N - 1>();
}

template <>
int factorial<0>()
{
    return 1;
}

int main()
{
    std::cout << factorial<5>() << "\n";
}

実行結果:

120

factorial<5>() というコードがあることにより、factorial<5> が暗黙的にインスタンス化されます。factorial<5>() の本体には factorial<N - 1>() がありますから、factorial<4> も必要だということになり、やはり暗黙的にインスタンス化されます。以降も同じ流れが続き、factorial<0> が必要となったとき、明示的特殊化されたバージョンのほうがインスタンス化され、ここで再帰的なインスタンス化は終わりになります。

注意が必要なのは、どれだけの回数の再帰が許されるかは処理系定義であることです。また、当然ながら無限に再帰することはできず、これは未定義動作となります[5]

std::vector<bool> 🔗

標準ライブラリの中でテンプレートの特殊化を使っている例として std::vector<T> があります。std::vector<T> のテンプレート仮引数 T に bool型を与えた場合に、部分特殊化された特別バージョンのコードが使用されます[6]

std::vector<int> vi {};   // 通常の std::vector
std::vector<bool> vb {};  // 明示的特殊化された std::vector

これが明示的特殊化でなく部分特殊化なのは、std::vector には2つのテンプレート仮引数があって、2つ目のほうが残っているからです。2つ目のテンプレート仮引数にはデフォルトテンプレート実引数が指定されているため、必要がなければその存在を気にせず使えます。

通常の std::vector<T> では要素の型が T になるので、T型の配列を内部に保持します。しかし、std::vector<bool> では、要素は 1ビットの大きさとして扱われます。bool型は最低でも 1バイトあるので、メモリ使用量が少なくとも 8分の1 にまで削減されます。

しかし、基本的な使用感は変わらないものの、通常の std::vector<T> から挙動が変わってしまっている部分もあります。たとえば、ポインタはバイト単位でしか動作しないため、要素を指し示すポインタを使うことができません。その結果、std::vector<T> を受け取るように作られている関数テンプレートは、std::vector<bool> の場合にだけうまく動作しない可能性があります。

また、vec[n] で nビット目だけを、しかも参照型として取り出せなければならないため、工夫を凝らした実装が必要になっており、処理速度の面にも問題があります。

このような問題があることから、std::vector<bool> は使わないように勧められることがあります。要素数が固定で構わないのならば std::bitset(「ノンタイプテンプレート」のページを参照)を選んだほうがいいでしょう。

もし、メモリ使用量を極限まで減らすことを意図せずに、単に要素が bool型である std::vector が欲しいと思うのなら、std::vector<unsigned char> で代替する手もあります。あるいは std::vector をあきらめて、std::deque<bool> を使う選択肢もあります。

まとめ 🔗


新C++編の【本編】の各ページには、末尾に練習問題があります。ページ内で学んだ知識を確認する簡単な問題から、これまでに学んだ知識を組み合わせなければならない問題、あるいは更なる自力での調査や模索が必要になるような高難易度な問題をいくつか掲載しています。


参考リンク 🔗


練習問題 🔗

問題の難易度について。

★は、すべての方が取り組める入門レベルの問題です。
★★は、自力でプログラミングができるようなるために、入門者の方であっても取り組んでほしい問題です。
★★★は、本格的にプログラマーを目指す人のための問題です。

問題1 (基本★★)

渡された値を出力するテンプレートを作成してください。明示的特殊化を使って、文字を出力するときは '' で、文字列を出力するときは "" で囲むようにします。クラステンプレートと関数テンプレートのそれぞれで実装してください。

解答・解説

問題2 (基本★★)

任意のコンテナのすべての要素を出力する関数テンプレートを作成してください。そのコンテナの要素がポインタ型の場合、そのポインタが指し示す先にある値を出力します。ヌルポインタのときには “null” を出力してください。

解答・解説

問題3 (応用★★)

再帰テンプレート」で階乗を求める関数テンプレートの例を取り上げましたが、この方法では実行中に再帰的な関数呼び出しが行われるので、計算に時間がかかります。クラステンプレートを用いて、Factorial<N>::value に階乗の結果が生成されるようにすれば、計算をコンパイル時点で終わらせることができます。そのような実装を作成してください。

解答・解説

問題4 (調査★★★)

std::vector<bool> は使用を避けたほうがいいとされるものの、その実現方法は理解しておく価値があります。要素を 1ビット単位で扱えるようにするために、どのような工夫がなされているか調べてみてください。

解答・解説


解答・解説ページの先頭



更新履歴 🔗




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