第8章:テンプレート
はじめに
テンプレートはC++の最も強力な機能の一つであり、ジェネリックプログラミングを可能にします。Alexander Stepanovは、STL(Standard Template Library)の設計を通じて、テンプレートの真の力を示しました。
本章では、テンプレートの基礎から、関数テンプレート、クラステンプレート、そして高度なテンプレート技法まで学びます。
---
1. テンプレートの歴史と概念
1.1 ジェネリックプログラミングの起源
Alexander Stepanovは、1970年代からジェネリックプログラミングの概念を研究していました:
> "Generic programming is about generalizing software components so that they can easily be reused in a wide variety of situations." > > (ジェネリックプログラミングとは、ソフトウェアコンポーネントを汎用化し、様々な状況で容易に再利用できるようにすることである)
テンプレートは以下の問題を解決します:
- コードの重複: 同じアルゴリズムを異なる型で使いたい
- 型安全性: void*のような型安全でない解決策を避けたい
- パフォーマンス: 実行時のオーバーヘッドを避けたい
1.2 マクロとの比較
C言語ではマクロでジェネリック的なコードを書いていました:
/* C言語のマクロ */
#define MAX(a, b) ((a) > (b) ? (a) : (b))
int x = MAX(3, 5); // OK
int y = MAX(3++, 5); // 危険!3++が複数回評価される可能性
C++のテンプレートはより安全:
// C++のテンプレート
template <typename T>
T max(T a, T b) {
return (a > b) ? a : b;
}
int x = max(3, 5); // OK
int y = max(3, 5); // 安全:引数は一度だけ評価
---
2. 関数テンプレート
2.1 基本構文
// テンプレート宣言
template <typename T>
T add(T a, T b) {
return a + b;
}
// typenameの代わりにclassを使うことも可能(意味は同じ)
template <class T>
T subtract(T a, T b) {
return a - b;
}
int main() {
std::cout << add(3, 5) << std::endl; // 8 (int)
std::cout << add(3.14, 2.71) << std::endl; // 5.85 (double)
std::cout << add<float>(3, 5) << std::endl; // 8 (明示的にfloat)
return 0;
}
2.2 テンプレート引数の推論
コンパイラは引数から型を推論します:
template <typename T>
void print(T value) {
std::cout << value << std::endl;
}
int main() {
print(42); // T = int と推論
print(3.14); // T = double と推論
print("Hello"); // T = const char* と推論
// 明示的な型指定
print<std::string>("Hello"); // T = std::string
return 0;
}
2.3 複数のテンプレートパラメータ
template <typename T, typename U>
auto multiply(T a, U b) -> decltype(a * b) {
return a * b;
}
// C++14以降では戻り値型の推論が可能
template <typename T, typename U>
auto divide(T a, U b) {
return a / b;
}
int main() {
std::cout << multiply(3, 4.5) << std::endl; // 13.5 (int * double = double)
std::cout << divide(10, 3) << std::endl; // 3 (int / int = int)
std::cout << divide(10.0, 3) << std::endl; // 3.333... (double / int = double)
return 0;
}
2.4 非型テンプレートパラメータ
型以外の値もテンプレートパラメータにできます:
template <typename T, int Size>
class Array {
T data[Size];
public:
int size() const { return Size; }
T& operator[](int index) { return data[index]; }
};
template <int N>
int factorial() {
return N * factorial<N - 1>();
}
template <>
int factorial<0>() {
return 1;
}
int main() {
Array<int, 10> arr;
std::cout << "Size: " << arr.size() << std::endl; // 10
std::cout << "5! = " << factorial<5>() << std::endl; // 120
return 0;
}
2.5 関数テンプレートのオーバーロード
// 汎用版
template <typename T>
void process(T value) {
std::cout << "Generic: " << value << std::endl;
}
// ポインタ用の特殊化
template <typename T>
void process(T* ptr) {
std::cout << "Pointer: " << *ptr << std::endl;
}
// 非テンプレート版(最優先)
void process(int value) {
std::cout << "Int specific: " << value << std::endl;
}
int main() {
int x = 42;
process(x); // Int specific: 42
process(&x); // Pointer: 42
process(3.14); // Generic: 3.14
process("Hello"); // Generic: Hello
return 0;
}
---
3. クラステンプレート
3.1 基本構文
template <typename T>
class Container {
private:
T value;
public:
Container(T v) : value(v) {}
T getValue() const { return value; }
void setValue(T v) { value = v; }
};
int main() {
Container<int> intContainer(42);
Container<std::string> strContainer("Hello");
std::cout << intContainer.getValue() << std::endl; // 42
std::cout << strContainer.getValue() << std::endl; // Hello
return 0;
}
3.2 メンバ関数の外部定義
template <typename T>
class Stack {
private:
std::vector<T> elements;
public:
void push(const T& element);
T pop();
bool empty() const;
};
// メンバ関数の外部定義
template <typename T>
void Stack<T>::push(const T& element) {
elements.push_back(element);
}
template <typename T>
T Stack<T>::pop() {
if (elements.empty()) {
throw std::runtime_error("Stack is empty");
}
T top = elements.back();
elements.pop_back();
return top;
}
template <typename T>
bool Stack<T>::empty() const {
return elements.empty();
}
3.3 テンプレートのデフォルト引数
template <typename T = int, int Size = 10>
class FixedArray {
T data[Size];
public:
int size() const { return Size; }
T& operator[](int i) { return data[i]; }
};
int main() {
FixedArray<> arr1; // int, 10
FixedArray<double> arr2; // double, 10
FixedArray<char, 100> arr3; // char, 100
return 0;
}
3.4 メンバテンプレート
クラス内にテンプレートメンバを持つことも可能:
class Converter {
public:
template <typename T, typename U>
T convert(U value) {
return static_cast<T>(value);
}
};
template <typename T>
class Container {
T value;
public:
Container(T v) : value(v) {}
// 異なる型のContainerからのコピーを許可
template <typename U>
Container(const Container<U>& other) : value(other.getValue()) {}
T getValue() const { return value; }
};
int main() {
Converter conv;
double d = conv.convert<double>(42); // 42.0
Container<double> dc(3.14);
Container<int> ic(dc); // double → int への変換
std::cout << ic.getValue() << std::endl; // 3
return 0;
}
---
4. テンプレートの特殊化
4.1 完全特殊化
特定の型に対して異なる実装を提供:
// プライマリテンプレート
template <typename T>
class Storage {
T value;
public:
Storage(T v) : value(v) {}
void print() const {
std::cout << "Generic: " << value << std::endl;
}
};
// bool型の完全特殊化
template <>
class Storage<bool> {
bool value;
public:
Storage(bool v) : value(v) {}
void print() const {
std::cout << "Boolean: " << (value ? "true" : "false") << std::endl;
}
};
// char*の完全特殊化
template <>
class Storage<const char*> {
const char* value;
public:
Storage(const char* v) : value(v) {}
void print() const {
std::cout << "C-string: \"" << value << "\"" << std::endl;
}
};
int main() {
Storage<int> si(42);
Storage<bool> sb(true);
Storage<const char*> sc("Hello");
si.print(); // Generic: 42
sb.print(); // Boolean: true
sc.print(); // C-string: "Hello"
return 0;
}
4.2 部分特殊化
一部のパラメータを特殊化:
// プライマリテンプレート
template <typename T, typename U>
class Pair {
public:
void describe() const {
std::cout << "Generic pair" << std::endl;
}
};
// 両方が同じ型の場合の部分特殊化
template <typename T>
class Pair<T, T> {
public:
void describe() const {
std::cout << "Same type pair" << std::endl;
}
};
// 2つ目がポインタの場合の部分特殊化
template <typename T, typename U>
class Pair<T, U*> {
public:
void describe() const {
std::cout << "Second is pointer" << std::endl;
}
};
// 両方がポインタの場合
template <typename T, typename U>
class Pair<T*, U*> {
public:
void describe() const {
std::cout << "Both are pointers" << std::endl;
}
};
int main() {
Pair<int, double> p1;
Pair<int, int> p2;
Pair<int, double*> p3;
Pair<int*, double*> p4;
p1.describe(); // Generic pair
p2.describe(); // Same type pair
p3.describe(); // Second is pointer
p4.describe(); // Both are pointers
return 0;
}
4.3 関数テンプレートの特殊化
// プライマリテンプレート
template <typename T>
void print(T value) {
std::cout << value << std::endl;
}
// 完全特殊化
template <>
void print<bool>(bool value) {
std::cout << (value ? "true" : "false") << std::endl;
}
// 注意: 関数テンプレートには部分特殊化がない
// 代わりにオーバーロードを使用
// ポインタ用のオーバーロード
template <typename T>
void print(T* ptr) {
std::cout << "Pointer to: " << *ptr << std::endl;
}
---
5. SFINAE(Substitution Failure Is Not An Error)
5.1 SFINAEの概念
テンプレート引数の置換が失敗しても、それはエラーではなく、そのテンプレートが候補から除外されるだけです:
#include <type_traits>
// std::enable_ifを使ったSFINAE
template <typename T>
typename std::enable_if<std::is_integral<T>::value, void>::type
process(T value) {
std::cout << "Integer: " << value << std::endl;
}
template <typename T>
typename std::enable_if<std::is_floating_point<T>::value, void>::type
process(T value) {
std::cout << "Floating point: " << value << std::endl;
}
int main() {
process(42); // Integer: 42
process(3.14); // Floating point: 3.14
// process("Hi"); // エラー:どちらのテンプレートも適用不可
return 0;
}
5.2 型特性(Type Traits)
C++11以降、ヘッダで多くの型特性が提供されています:
#include <type_traits>
template <typename T>
void checkType() {
std::cout << "is_integral: " << std::is_integral<T>::value << std::endl;
std::cout << "is_floating_point: " << std::is_floating_point<T>::value << std::endl;
std::cout << "is_pointer: " << std::is_pointer<T>::value << std::endl;
std::cout << "is_const: " << std::is_const<T>::value << std::endl;
}
// 条件付きコンパイル
template <typename T>
void safeDelete(T* ptr) {
static_assert(std::is_pointer<T*>::value, "Must be a pointer");
delete ptr;
}
int main() {
checkType<int>();
// is_integral: 1
// is_floating_point: 0
// is_pointer: 0
// is_const: 0
checkType<const double>();
// is_integral: 0
// is_floating_point: 1
// is_pointer: 0
// is_const: 1
return 0;
}
5.3 C++17のif constexpr
コンパイル時条件分岐:
template <typename T>
void process(T value) {
if constexpr (std::is_integral_v<T>) {
std::cout << "Integer: " << value << std::endl;
} else if constexpr (std::is_floating_point_v<T>) {
std::cout << "Float: " << value << std::endl;
} else {
std::cout << "Other: " << value << std::endl;
}
}
---
6. 可変引数テンプレート(C++11)
6.1 基本構文
// 可変引数テンプレート
template <typename... Args>
void print(Args... args) {
// sizeof...で引数の数を取得
std::cout << "Number of arguments: " << sizeof...(args) << std::endl;
}
int main() {
print(); // 0
print(1); // 1
print(1, 2.0, "three"); // 3
return 0;
}
6.2 再帰的展開
// 基底ケース
void printAll() {
std::cout << std::endl;
}
// 再帰ケース
template <typename T, typename... Rest>
void printAll(T first, Rest... rest) {
std::cout << first << " ";
printAll(rest...); // 残りの引数で再帰呼び出し
}
int main() {
printAll(1, 2.5, "Hello", 'X');
// 出力: 1 2.5 Hello X
return 0;
}
6.3 フォールド式(C++17)
// 加算フォールド
template <typename... Args>
auto sum(Args... args) {
return (args + ...); // 右フォールド
}
// 出力フォールド
template <typename... Args>
void printAll(Args... args) {
(std::cout << ... << args) << std::endl; // 左フォールド
}
// 論理フォールド
template <typename... Args>
bool allTrue(Args... args) {
return (args && ...);
}
int main() {
std::cout << sum(1, 2, 3, 4, 5) << std::endl; // 15
printAll("Hello", " ", "World", "!"); // Hello World!
std::cout << allTrue(true, true, false) << std::endl; // 0
return 0;
}
---
7. テンプレートメタプログラミング
7.1 コンパイル時計算
// コンパイル時フィボナッチ
template <int N>
struct Fibonacci {
static const int value = Fibonacci<N-1>::value + Fibonacci<N-2>::value;
};
template <>
struct Fibonacci<0> {
static const int value = 0;
};
template <>
struct Fibonacci<1> {
static const int value = 1;
};
// constexprを使った現代的なアプローチ(C++11以降)
constexpr int fibonacci(int n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
int main() {
// コンパイル時に計算
std::cout << Fibonacci<10>::value << std::endl; // 55
std::cout << fibonacci(10) << std::endl; // 55
// コンパイル時定数として使用
constexpr int fib10 = fibonacci(10);
int arr[fib10]; // 配列サイズとして使用可能
return 0;
}
7.2 型リスト
// 型リストの実装
template <typename... Types>
struct TypeList {};
// 型リストの長さ
template <typename List>
struct Length;
template <typename... Types>
struct Length<TypeList<Types...>> {
static const int value = sizeof...(Types);
};
// 最初の型を取得
template <typename List>
struct Front;
template <typename Head, typename... Tail>
struct Front<TypeList<Head, Tail...>> {
using type = Head;
};
int main() {
using MyList = TypeList<int, double, char>;
std::cout << Length<MyList>::value << std::endl; // 3
static_assert(std::is_same_v<Front<MyList>::type, int>);
return 0;
}
7.3 タグディスパッチ
// タグ型
struct InputIteratorTag {};
struct RandomAccessIteratorTag : InputIteratorTag {};
// イテレータカテゴリに応じた実装
template <typename Iterator>
void advanceImpl(Iterator& it, int n, InputIteratorTag) {
std::cout << "Slow advance (input iterator)" << std::endl;
while (n-- > 0) ++it;
}
template <typename Iterator>
void advanceImpl(Iterator& it, int n, RandomAccessIteratorTag) {
std::cout << "Fast advance (random access)" << std::endl;
it += n;
}
template <typename Iterator>
void advance(Iterator& it, int n) {
advanceImpl(it, n, typename std::iterator_traits<Iterator>::iterator_category());
}
---
8. テンプレートのベストプラクティス
8.1 ヘッダファイルでの定義
テンプレートは通常、ヘッダファイルで完全に定義する必要があります:
// my_template.hpp
#ifndef MY_TEMPLATE_HPP
#define MY_TEMPLATE_HPP
template <typename T>
class MyClass {
T value;
public:
MyClass(T v) : value(v) {}
T getValue() const { return value; }
void setValue(T v) { value = v; }
};
// 関数テンプレートもヘッダで定義
template <typename T>
T maximum(T a, T b) {
return (a > b) ? a : b;
}
#endif
8.2 明示的インスタンス化
コンパイル時間を短縮するため、明示的にインスタンス化できます:
// my_template.hpp
template <typename T>
class MyClass {
// 宣言のみ
T getValue() const;
};
// my_template.cpp
#include "my_template.hpp"
template <typename T>
T MyClass<T>::getValue() const {
return value;
}
// 明示的インスタンス化
template class MyClass<int>;
template class MyClass<double>;
template class MyClass<std::string>;
8.3 static_assertによるエラーメッセージ
template <typename T>
class NumericContainer {
static_assert(std::is_arithmetic<T>::value,
"T must be a numeric type");
T value;
public:
NumericContainer(T v) : value(v) {}
};
int main() {
NumericContainer<int> nc1(42); // OK
NumericContainer<double> nc2(3.14); // OK
// NumericContainer<std::string> nc3("Hi"); // コンパイルエラー
return 0;
}
8.4 テンプレートのデバッグ
// 型名を取得するヘルパー
#include <typeinfo>
#include <cxxabi.h>
template <typename T>
std::string typeName() {
int status;
char* demangled = abi::__cxa_demangle(typeid(T).name(), 0, 0, &status);
std::string result(demangled);
free(demangled);
return result;
}
template <typename T>
void debugType(T value) {
std::cout << "Type: " << typeName<T>() << std::endl;
std::cout << "Value: " << value << std::endl;
}
---
まとめ
本章で学んだこと:
次章では、STL(Standard Template Library)について学びます。
---
練習問題
- 2つの型が同じかどうかをコンパイル時に判定するメタ関数
IsSameを実装してください:
template <typename T, typename U>
struct IsSame {
// 実装
};
static_assert(IsSame<int, int>::value == true);
static_assert(IsSame<int, double>::value == false);
- 可変引数テンプレートを使って、任意の数の引数の平均値を計算する関数を実装してください。