ゲーム業界エンジニアの独り言

クライアント、サーバ、インフラなどなど興味あることなんでも紹介

C++テンプレートテクニック

今、「C++のためのAPIデザイン」を読んでます。 テンプレートを使用する際の、テクニックについてまとめてみました。

C++のためのAPIデザイン

C++のためのAPIデザイン

目次

テンプレートとは?

テンプレートとは、コンパイル時にコードを生成する機能です。 型だけが異なる大量のコードを、生成する時など特に役に立ちます。

良く知られている例として、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という場合は、intがテンプレート引数です。

特殊化

テンプレートがインスタンス化されて、結果として生成される クラス、メソッド、関数は特殊化と呼ばれます。

とはいえ、特殊化という用語は、テンプレートパラメータ対して 具体的な型を特定する事で、関数にカスタム実装を提供する時にも使われます。

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を作成した場合、このテンプレートは、 int、short、float、doubleのみの型を提供すれば問題ありません。 これ以上の型をユーザーに作成させる必要は無いと考えられます。

この場合は、明示的テンプレートのインスタンス化を使う事で、 .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;