【初心者C++er Advent Calendar 2016 11日目】C++ で型によるコンパイル時条件分岐技法まとめ
初心者C++er Advent Calendar 2016 11日目の記事です。
空いてるようなのでまた書きました。
さて、初心者用ということで型を使用した『コンパイル時条件分岐技法』を簡単に紹介してみます。
[注意]
本記事では C++11 で動作することを想定してるコードになります。
[多重定義]
まずは一番よく利用されている多重定義ですね。
C++ では引数の型を変えることで複数の関数を定義することが出来ます。
void print(int n){ std::printf("int 型:%d\n", n); } void print(float f){ std::printf("float 型:%f\n", f); } void print(char const* str){ std::printf("char const* 型:%s\n", str); } print(42); print(3.14f); print("homu"); /* output: int 型:42 float 型:3.140000 char const* 型:homu */
また、型以外に引数の数によっても複数の関数を定義することが出来ます。
int func(int n){ return n + n; } int func(int n, int m){ return n + m; } func(42); // 84 func(4, 6); // 10
まあ流石に C++ を書いてる人で多重定義を書いたことがない人はいないと思います。
[テンプレートの特殊化]
さて、次はテンプレートを使った条件分岐です。
いわゆる『テンプレートの特殊化』と言われるやつですね。
例えば、次のようなクラステンプレートがあったとします。
template<typename T> struct X{ int twice(){ return value + value; } T value; }; X<int> x{42}; x.twice(); // 84
この時にテンプレートが『任意の型』の場合に twice()
の実装を変えたいとします。
例えば、T = char
の場合は std::string
を返すようにしたい場合は X<char>
に対応するクラスを新しく定義します。
template<typename T> struct X{ int twice(){ return value + value; } T value; }; X<int> x{42}; x.twice(); // 84 // 新しくクラスを定義する // この template 引数は省略し template<> // ここに『任意の方』を記述する struct X<char>{ std::string twice(){ return std::string{value} + value; } // ここも T 型から char 型に書き直す char value; }; X<char> x2{'c'}; x2.twice(); // "cc"
こんな感じです。
まあこれは『こういうものだ!』という風に覚えるしかないですね。
また、『一部のテンプレート型に対して』も適用することが出来ます。
template<typename T, typename U> struct X{ }; template<typename U> // クラステンプレートの第一引数が int の場合のクラス struct X<int, U>{ };
ここで注意するのは template<>
の引数と X<int, U>
の引数ですね。
template<>
の引数は任意で構いませんが、X
に渡す引数の数は最初に定義したクラステンプレートと同じでなければなりません。
なので、例えば、次のように拡張することは出来ません。
template<typename T> // 引数の数が違うのでエラー struct X<int>{ };
こんな感じですね。
まあ使う機会は少ないかもしれませんが、覚えておくといざという時に利用できるかもしれません。
ちなみに標準ライブラリの悪名高い std::vector<bool>
もこのテンプレートの特殊化を利用してるクラスのひとつですね。
[SFINAE]
さて、SFINAE です。
「SFINAE (Substitution Failure Is Not An Errorの略称、スフィネェと読む)」は、テンプレートの置き換えに失敗した際に、即時にコンパイルエラーとはせず、置き換えに失敗した関数をオーバーロード解決の候補から除外するという言語機能である。
参照:任意の式によるSFINAE - cpprefjp C++日本語リファレンス
まあ、これ自体は難しい仕組みではなくて、例えば次のような関数テンプレートがあったとします。
template<typename T> typename T::value_type twice(T){ return value + value; }
これは、T 型に対して『::value_type
を要求してる』という関数になります。
なので、
struct Int{ using value_type = int; int value; }; // Int 型は ::value_type があるので問題なく動作する twice(Int{42});
というコードは問題ありませんが
struct Float{ float value; }; // Float は ::value_type を持っていないのでエラー twice(Float{42}); // int は ::value_type を持っていないのでエラー twice(42);
というような引数は ::value_type
がないのでエラーになります。
ここまではなんとなくわかると思います。
では次に『int
型を受け取る関数』を追加します。
template<typename T> typename T::value_type twice(T t){ return t.value + t.value; } // 新しく関数を追加 int twice(int n){ return n + n; } struct Int{ using value_type = int; int value; }; struct Float{ float value; }; // OK: T::value_type twice(T) が呼ばれる twice(Int{42}); // 84 // OK: int twice(int) が呼ばれる twice(42); // 84 // Error: どっちの関数にもマッチしない twice(Float{3.14f});
この時に int
型を引数に twice()
を呼び出した場合に関数テンプレートで『::value_type
がない』とエラーにならずに twice(int)
が呼び出されます。
つまり
と言うとおり『エラーにならないでオーバーロードから除外されるだけ』という挙動になります。
SFINAE はよく enable_if
などと組み合わせて利用されるためちょっとコードが複雑になってしまいますが、SFINAE 自体の仕組みはこれだけです。
そんなに難しくはないでしょ?
また、上記の例では『戻り値型に T::value_type
』を定義していましたが、他にも
template< typename T, // テンプレートのデフォルト引数として定義 typename Result = typename T::value_type > Result twice(T t){ return t.value + t.value; }
このように『テンプレートのデフォルト引数』として定義してみたり、
template<typename T> void print(T t, decltype(t.value)* = 0 /* メンバ変数 value にアクセスする */){ std::cout << t.value << std::endl; }
これならば『T がメンバ変数 value を持ってる』みたいな条件付けをすることも出来ます。
これにより『単純な型』だけではなく『より複雑な条件』で多重定義を行うことができるようになります。
このようなテクニックはいくかありますが、詳しくは"任意の式によるSFINAE"を参照してみてください。
[まとめ]
と、言う感じで簡単にコンパイル時条件分岐技法をまとめてみました。
正直 SFINAE に関してはぜんぜん紹介しきれてないですが、まあ初心者向けということで『こういうのもあるんだよー』ぐらいに思っておけばいいと思います。
SFINAE に関しては余裕があればまた別の Advent Calendar で記述したいと思います。