C++テンプレートテクニック
今、「C++のためのAPIデザイン」を読んでます。 テンプレートを使用する際の、テクニックについてまとめてみました。
- 作者: マーティン・レディ,Martin Reddy,三宅陽一郎,ホジソンますみ
- 出版社/メーカー: ソフトバンククリエイティブ
- 発売日: 2012/11/02
- メディア: 大型本
- 購入: 4人 クリック: 106回
- この商品を含むブログ (11件) を見る
目次
テンプレートとは?
テンプレートとは、コンパイル時にコードを生成する機能です。 型だけが異なる大量のコードを、生成する時など特に役に立ちます。
良く知られている例として、C++のコンテナクラスがあります。
vector<int> int_vector; // int型を扱うVectorクラスのインスタンス vector<float> float_vector; // float型を扱うVectorクラスのインスタンス vector<string> string_vector; // string型を扱うVectorクラスのインスタンス
テンプレートの用語
テンプレートはC++の機能の中でも、誤解されることが多い機能です。 そこで、テンプレートに関する用語の定義から説明します。
以下のテンプレート定義を使って、用語を定義していきます。
template <typename T> class Stack { public: void Push(T value); T Pop(); bool IsEmpty(); private: std::vector<T> stack_; };
インスタンス化します。
Stack<int> int_stack;
Stack<string> string_stack;
このクラステンプレートは、要素Tの型を格納できる、一般的なスタッククラスです。
・テンプレートパラメータ templateキーワードの後に記載される名前。 例えば、Tは上のスタックの例で指定された、テンプレートパラメータです。
・テンプレート引数
特殊化の過程でテンプレートパラメータと置き換わる実体。
Stack
特殊化
テンプレートがインスタンス化されて、結果として生成される クラス、メソッド、関数は特殊化と呼ばれます。
とはいえ、特殊化という用語は、テンプレートパラメータ対して 具体的な型を特定する事で、関数にカスタム実装を提供する時にも使われます。
template <> void Stack<int>::Push(int value) { // int型専用のPushを実装 }
このように、具体的な型を特定してメソッドを定義する事で、 その型で特殊化された際の、専用メソッドが定義出来ます。
部分的な特殊化
部分的な特殊化は、元となる汎用なテンプレート引数のうちの、 一部だけを特殊化する方法です。
プログラマが1つの機能を特殊化し、他の機能の指定はユーザーに許可します。
複数パラメータを受け取るテンプレートの例
このパラメータ一つに対して、具体的な型を指定して一つのケースを定義する事で、 部分的な特殊化を行う事が出来ます。
下記の例では、Setの一番目のパラメータをintで作成した際に、 特別な処理を記述する事が可能になります。
template <typename KEY, VALUE> class Set { // 共通の処理 bool Insert(KEY key, VALUE value); };
template <typename VALUE> class Set<int, VALUE> { // Set<int, VALUE>で特殊化された際の特別処理 bool Insert(int key, VALUE value); };
単一パラメータを受け取るテンプレートの例
単一のテンプレートパラメータを持つStackの例では、 テンプレートを部分的に特殊化して、任意の型Tへのポインタを 厳密に処理する事が可能です。
下記の例では、ユーザーがポインタのStackを作成する際に、 特別な処理を記述する事が可能になります。
template <typename T> class Stack<T *> { public: void Push(T *value); T *Pop(); bool IsEmpty(); private: std::vector<T *> stack_; };
非明示的なインスタンス化のAPI設計
クライアント独自の型で、クラステンプレートのインスタンス化を許可する場合は、 非明示的なインスタンス化を使用する必要があります。
例えば、クラステンプレートのStack
そこで、コンパイラはテンプレートの定義にアクセスする必要があるため、 ヘッダファイルにテンプレートの定義を記述する必要があります。 それは強固なAPIにとっては、大きなデメリットになります。
こうした状況では、実装の詳細は隠蔽出来なくても、隔離する努力を必要になります。
実装ヘッダへの格納
ヘッダファイルにテンプレート定義を格納する必要がある場合は、 クラス宣言部に直接インライン化するのが簡単なため、そうしたくなると思います。
しかし、これは強固なAPIにとっては悪い設計です。
これに代わる手段は、テンプレート実装の詳細を別ヘッダに格納する事です。
Stackクラスを例を使えば、メインのパプリックヘッダは以下になります。
// stack.h #ifndef STACK_H #define STACK_H #include <vector> template <typename T> class Stack { public: void Push(T value); T Pop(); bool IsEmpty(); private: std::vector<T> stack_; }; // 全ての実装の詳細をヘッダに格納する #include "stack_private.h" #endif
// stack_private.h #ifndef STACK_PRIVATE_H #define STACK_PRIVATE_H template <typename T> void Stack<T>::Push(T value) { // 実装 } template <typename T> T Stack<T>::Pop() { // 実装 } template <typename T> bool Stack<T>::IsEmpty() { // 実装 } #endif
これは、Boostヘッダなど、多くの高品質なテンプレートベースのAPIで 使われているテクニックです。
メインのパプリックヘッダの実装の詳細を乱雑にせず、 必要な内部詳細の記述は、プライベートな詳細を格納する事が可能です。
ヘッダファイルにテンプレート定義を含めるテクニックは、 インクルードモデルと呼ばれています。
明示的なインスタンス化のAPI設計
事前に定義したテンプレート型のみの作成を提供し、 ユーザーに作成を許可したくない場合は、テンプレートの定義をヘッダに記述せず、 プライベートコードを完全に隠蔽する方法があります。
例えば、Vector3D
この場合は、明示的テンプレートのインスタンス化を使う事で、 .cppにテンプレート定義を格納する事が出来ます。
Stackの例を使って、明示的テンプレートのインスタンス化の例を示します。
template class Stack<int>;
これでコンパイラは、コード内のこの時点でint型のコードを生成するので、 その結果、コード内の別の場所で、明示的インスタンスを試みる事は無くなります。 明示的的なインスタンス化を行う事で、ビルド時間の短縮を図る事も出来ます。
では、この機能を活用するために、どのようなコードを組めばいいかを Stackクラスを例に見ていきます。
ヘッダファイルはほぼ同じです。 「#include "stack_private.h"」が無くなっているだけです。
// stack.h #ifndef STACK_H #define STACK_H #include <vector> template <typename T> class Stack { public: void Push(T value); T Pop(); bool IsEmpty(); private: std::vector<T> stack_; }; #endif
これで、関連するcppファイルにテンプレートの定義を全て格納できます。
// stack_private.cpp #include "stack.h" #include <string> template <typename T> void Stack<T>::Push(T value) { // 実装 } template <typename T> T Stack<T>::Pop() { // 実装 } template <typename T> bool Stack<T>::IsEmpty() { // 実装 } // 明示的テンプレートのインスタンス化 template class Stack<int>; template class Stack<double>; template class Stack<std::string>;
ここで大事なのは最後の3行です。
template class Stack<int>; template class Stack<double>; template class Stack<std::string>;
この3行で、Stackクラステンプレートの明示的なインスタンス化を行っています。
実装の詳細は.cppファイルの格納されているため、ユーザーはこれ以上の 特殊化を行う事は出来ません。 これで実装の詳細は.cppファイルに完全に隠蔽された事になります。
ユーザーが使えるテンプレートの特殊化(ユーザーが使えるテンプレート引数)を 明示的にどれかを示すために、パプリックヘッダの最後にtypedefを追加します。
typedef Stack<int> IntStack; typedef Stack<double> DoubleStack; typedef Stack<std::string> StringStack;