概要
C++11で追加されたtype_traitsが便利で、自分もそれに類するものが作れて楽しかったので、その喜びを分かち合いたいがために書かれた記事です。次のような方におすすめです。
- type_traitsの仕組みを理解したい
- 型に応じたパラメータや型情報を取得できる仕組みを作りたい
- 黒魔術にきょうみがある
テンプレートの普通の使い方は理解していることを前提に書いていきます。
用法用量は守って正しく使いましょう、を合い言葉にスタート。
追記
黒魔術師の先輩方がコメントしてくださった内容を反映しました。
原理
C++のテンプレートには特殊化というテクニックがあります。テンプレートクラスや関数に対して、特定の型が指定された場合の実装や定義を記述できるというものです。Traitsとはこれを 悪用 応用して、型を引数にした情報を得るためのテクニックです。
まず、次のようなテンプレート構造体を宣言します。
template<typename T>
struct Traits;
これだけでは実体化もできないただの前方宣言ですが、特殊化で型ごとに異なる(実際には役立つ)情報を得られる定義を書いてみます。
template<>
struct Traits<int> {
static constexpr auto Name = "int";
static constexpr int Value = 0;
};
template<>
struct Traits<float> {
static constexpr auto Name = "float";
static constexpr int Value = 1;
};
template<>
struct Traits<double> {
static constexpr auto Name = "double";
static constexpr int Value = 2;
これでTraits構造体は、int,float,double
型に対して、型名Nameと値Valueを返す型になりました。次のように利用できます。
printf("i:%s,%d\n", Traits<int>::Name, Traits<int>::Value);
printf("f:%s,%d\n", Traits<float>::Name, Traits<float>::Value);
printf("d:%s,%d\n", Traits<double>::Name, Traits<double>::Value);
次のような出力結果になります。
i:int,0
f:float,1
d:double,2
型名を片っ端から特殊化して名前を登録しておけば、静的なリフレクションが実装できそうですね。ライブラリで対象とする型をある程度絞れるのであれば、コードジェネレータなどを併用してTraitsコードを生成してもいいかもしれません。
type_traitsヘッダの中でやってることも本質的にはこれと同じですが、テンプレートの特殊化だけでは判別できないような情報を、コンパイラの拡張(通常使ってはあかんやつ)で取得している部分も多いので、実装を覗いて参考にするなどの深入りはしない方が良さそうです。
Traits実装上のポイント
Traitsを実現するには別段structである必要はありません。classでも良いのですが、テクニックの性質上staticな公開メンバを提供することが目的なので、デフォルトのアクセス制御がpublicであるstructを使うことがほとんどです。
Traitsで提供できるのは定数か定数式に限られます。文字列の場合は定数式での初期化が必要ですが、C++11からはchar*
で文字列リテラルを受けることが禁止されているので、autoを使って型推論する(文字列リテラルの長さがNならばchar[N]
として扱われる)ことで定数式として書けます。Visual Studio 2015ではナチュラルにコンパイルできてしまいましたので気をつけましょう。
Traitsをもっと極めるにはSFINAE(すふぃねー)と呼ばれるテクニックが重要になってきます。C++11以前では、type_traitsに相当する実装もこれを駆使して実現していたようです。今でも色々と使いでのあるテクニックなのですが、結構な黒魔術なので詳細はコメントしてくださっている魔術師の先輩方の記事などをご参照ください。
実用例
Traitsは色々な応用例が考えられますが、やはり一番輝くのはテンプレートクラスの実装時ではないかと思います。静的な文字列や値を返すのも良いのですが、型に応じた型を返すという使い方ができると、テンプレートでできることの幅が広がります。
例えばテンプレートクラスにおいて、型引数に渡された型に応じた異なる型をAPIで使用したい、というケースがあるとします。floatだったらVector3f、doubleだったらVector3d、といった具合です。
(本来ならVector<float, 3>
のように利用できる型を使うのが正しいでしょうが、そうもいかない場合を想定してください)
こんな時は、次のようなTraitsを用意します。
// floatの場合に使わなくてはいけない型
struct Vector3f {
float v[3];
};
// doubleの場合に使わなくてはいけない型
struct Vector3d {
double v[3];
};
template<typename T>
struct Traits;
template<>
struct Traits<float> {
using Vector3Type = Vector3f;
};
template<>
struct Traits<double> {
using Vector3Type = Vector3d;
};
このTraitsを利用すると、次のようなテンプレートクラスが定義できます。
template<typename T>
class Hoge {
public:
// usingでTraitsで定義した型エイリアスを定義し直す
using Vector3 = typename Traits<T>::Vector3Type;
// 引数や返値、フィールドにおいてTraitsから取得した型が使える
Vector3 GetVector() const {
return vec;
}
void SetVector(const Vector3& v) {
vec = v;
}
private:
Vector3 vec;
};
注意点は、テンプレートクラス内でusingする際にtypenameを付けることです。Visual Studioのコンパイラでは、typenameを省略するとTraits<T>::Vector3Type
が型名として認識されず、コンパイルエラーになります。
上記の例では型エイリアスをクラス内に制限していますが、その必要がない場合は次のように書くとクラス外からの利用が簡便になります。
template<typename T>
struct Traits;
template<>
struct Traits<float> {
using Vector3Type = Vector3f;
};
template<>
struct Traits<double> {
using Vector3Type = Vector3d;
};
template<typename T>
using Vector3 = typename Traits<T>::Vector3Type;
これで、Vector3<float>
とすれば、Vector3fを利用できます。Hoge<float>::Vector3
よりはだいぶ使いやすいでしょう。
Traitsを使わない場合は、Vector3に相当する型も型引数で渡さないといけませんが、Traitsを使うとだいぶスマートになります。しかも、Traitsで定義されていない型を渡された場合はコンパイルエラーになる(Vector3Typeが解決できない)ので、テンプレートで受ける型の制約にも役立ちます。
今回はシンプルな例に留めましたが、PImplイディオムと組み合わせるのも有効でしょう。マルチプラットフォームのライブラリにおいて、プラットフォームの実装ごとに固有の型をAPIで使いたい、といった場合にはかなりの威力を発揮すると思います。
まとめ
- Traitsとは、テンプレートの特殊化を応用したテクニック
- Traitsを使うと、型に応じた情報を取得できる仕組みが作れる
- Traitsとテンプレートクラスを組み合わせると、1つの型引数から対応する別の型を取得したり、型引数に対する制約をかけたりできる
それでは用法用量を守って、楽しいTraitsライフをお楽しみください。