【初心者C++er Advent Calendar 2016 17日目】C++ で型と値を理解する
初心者C++er Advent Calendar 2016 17日目の記事です。
初心者ネタに型と値の話でも。
[値とは]
C++ における値とは数値や文字列、クラスのインスタンスなどを指します。
// 数値や小数 42 3.14f // 文字や文字列 'c' "homu" // クラスのインスタンス std::string{} std::vector<int>{}
まあ要はデータそのものですね。
[型とは]
一方、C++ における型とは int
や char
、std::string
などを指します。
型は一般的に『何かの値を受け取るものを定義する時』つまり『変数を定義する時』や『関数の引数を定義する時』などに使用します。
// 42 という『値』を受け取る変数を『int 型』として定義する int n = 42; // 『int 型』と『float 型』を受け取って『bool 型』を返す関数を宣言する bool func(int, float); // 関数に『値』を渡して『値』を受け取る bool b = func(42, 3.14f);
このように型とは『値と対になるような存在』になりどんな値にも『型』は存在します。
[型による制約]
基本的には『同じ型』の変数に対してのみ値を代入することが出来ます。
int a; // int 型の変数を定義 a = -10; // int 型の値を代入する a = "homu"; // Error: 他の型を代入することは出来ない
このようにすることで任意の変数は『定義した時の型』である事が保証されます。
ただし、値によっては『別の型』を代入する事ができる場合もあります。
float b; // float 型の変数を定義 b = 3.14f; // float 型の値を代入する b = 42; // OK: int 型の値を代入してもエラーにならない
このように他の型へ変換されることを暗黙の型変換と呼びます。
C++ ではしばしばこのように暗黙的に別の型へと変換される事があるので注意してください。
このように C++ では『値』がどんな『型』になるのか、また『値を演算した結果(関数を呼び出した結果)』どんな『型に変換されるのか』を意識して書くことが重要になってきます。
これに慣れてくると
result = 42 + 3.14f;
というような処理が
float = int + float;
のように型に脳内変換されます。
[値から型を定義する]
さて、ちょっと型の話をしていきましょう。
コードを書いてると値や式から型を取得したいと思うことが度々あります。
int a = 10; float b = 3.14f; // どんな型を返すんだっけ??? ??? c = a + b;
そういう時は C++11 から追加された decltype()
という機能を使用します。
decltype()
は式(値)を渡すことで『その式(値)の型』を返します。
int a = 42; decltype(a); // int 型を返す decltype(a) b = 10; // 型の変わりに使用することができる // typedef などで使用することもできる typedef decltype(a) a_type; float f = 3.14f; decltype(a + f) result = a + f; // decltype() には式も渡せる decltype(func(a, b)) result2 = func(a, b); // 関数も渡すことができる int const n = 42; // const 修飾子も加味した型を返す decltype(n); // const int
こんな感じです。
基本的に『decltype()
を定義したスコープから見える変数』はすべて参照する事が出来ます。
ですので以下のようにC++11 で追加された『後置戻り値型』でも decltype()
が使用できます。
template<typename T, typename U> auto plus(T a, U b) // 型情報だけでは演算結果の型を取得するのは難しいが // decltype() を利用すれば式から型を推論することができる ->decltype(a + b){ return a + b; }
decltype()
が具体的にどのような型を返すのかは以下のサイトがわかりやすく解説しています。
[まとめ]
そんな感じで値と型の話について書いてみました。
C++(に限らないけど)において『型情報』というのは重要なファクターになります。
特に昨今のテンプレートや auto
などを使用したコードでは『表面の型』が見えないことが多いです。
そういう時こそ『型』を意識してコードを書いていくことが C++ を学ぶ上で大切になっていきます。
動的型付け言語を普段書いている人にとって『型情報』というのはめんどくさい要因の一つだと思います。
しかし、『型による制約』によるリターンもとても大きいものではあります。
C++ 初心者の人はコードを書いてる時に少しだけ型について意識して書いてみると C++ への理解がより深まると思います。
【初心者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 で記述したいと思います。
【C++ Advent Calendar 2016 13日目】C++ で楽しく Expression Templates しよう
C++ Advent Calendar 2016 13日目の記事です。
13日目が空いてるようなのでちょっと Expression Templates というマニアックなネタでも。
まあ C++ プロの方なら Expression Templates なんて余裕ですよね。
[注意]
本記事のコードは基本的には C++14 に準じているので、自環境で試したい場合は注意してください。
[Expression Templates とは]
Expression Templates とは wikibooks から引用すると
C/C++ において数学的な式を評価するありふれたやり方として、関数中に式をラップし、その関数ポインタを渡して、入力された数値群に対して適用するというものがある。この方法では、関数呼び出しと一時オブジェクト生成のオーバーヘッドが生じる。また、ソース中の式の位置が呼び出し元から非常に離れていることも頻繁であり、可読性と保守性に悪影響を与える。式テンプレート(Expression template)は、式をその場に展開することによって、関数ポインタを不要にし、式と呼び出し元をまとめることで問題を解決する。
式テンプレートには再帰的な型の合成(Recursive Type Composition)が用いられている。再帰的な型の合成では、少量のコードから複雑な型の階層を生じさせることができる。一方向の再帰的な型の合成では線形な型リスト(Type List)が生成される。二方向の再帰的な型の合成は二項式に対して有用であり、以下の例で用いている。メタプログラミングテクニックはデータのように型を合成することを可能にする。違いは、メタプログラムはコンパイル時に動作すること、またそれゆえデータではなく型に対してはたらくことだけである。
参照:More C++ Idioms/式テンプレート(Expression-template) - Wikibooks)
なるほどよくわからん。
と、言うことでこういう概念はすっ飛ばして、実際にコードを書いていってみます。
ちなみにこの記事は『Expression Templates とはなんぞやを解説する記事』ではなくて『C++14 でオレオレ Boost.Lambda を実装する』という記事になります。
[Boost.Lambda/Phoenix]
実際にコードを書いていく前に少し Boost.Lambda と Boost.Phoenix を解説します。
Boost.Lambda と Boost.Phoenix はまさに Expression Templates を利用して『ラムダ式(のようなもの)を定義するため』につくられたライブラリです。
ちなみに Boost.Lambda と Boost.Phoenix の2種類のライブラリが存在するがここでは Boost.Phoenix について説明します。
Boost.Phoenix は『プレースホルダ』と呼ばれるものを利用して式を定義していきます。
// この argN というのがプレースホルダになる // このプレースホルダはあとから評価した時の引数に着替えられる auto plus = arg1 + arg2 * arg3; // この時に arg1 は第一引数、arg2 は第二引数…という感じに置き換えられ // 最終的には 1 + 2 * 3 という式が評価される plus(1, 2, 3); // 7
このプレースホルダは何かというと単なる関数オブジェクトになり
arg1(1, 2, 3); // 1 arg2(1, 2, 3); // 2 arg3(1, 2, 3); // 3
という風に『第n番目の引数を返す』という挙動になります。
また、この他にも
// 値を返すだけの関数オブジェクトを返す auto _42 = val(42); // 引数の値を返すだけ _42(); // 42
という関数オブジェクトを返す val()
という関数も存在します。
この関数オブジェクトも先ほどのプレースホルダと組み合わせて使用できます。
// 関数オブジェクトから演算子を使って式を組み立てていく auto expr = arg1 + arg2 * val(42) expr(1, 2); // 85
こんな感じで Boost.Phoenix にはいろいろな関数オブジェクトが用意されており、それを組み合わせて式を定義していく感じになります。
ここで注意するのはすべて『関数オブジェクト』として扱うということです。
この点はかなり重要なので覚えておくとよいです。
それでは実際にこのような関数オブジェクトを実装してみます。
[val()
定義する]
まずは比較的な簡単な val()
の定義から。
まあこれは単に値を保持して返すだけのオブジェクトになります。
template<typename T> struct holder{ auto operator ()(...) const{ return value; } T value; }; template<typename T> holder<T> val(T t){ return {t}; } auto result = val(42)();
まあ値を保持する holder
クラスを用意して、それを生成するための val()
関数をヘルパ関数として定義してるだけですね。
この実装のキモとしては『実際の関数オブジェクトとして定義するクラス』と『その関数オブジェクトを返す関数』をわけて考えてることです。
これにより使用する側はクラステンプレートを意識する必要がありません。
あ、あと operator ()
が『可変長引数』を受け取っているのもポイントです。
これはあとから説明します。
ちなみに本筋とは関係ないところでコード量が増えてしまうので rvalue reference は省略して書いていきます。
まあ本物の C++ なら rvalue reference に対応するなど容易いはず
[プレースホルダオブジェクトを定義する]
次はプレースホルダです。
val()
と比較してちょっと複雑ですが、そこまで難しくはないです。
template<std::size_t Index> struct placeholder{ template<typename... Args> auto operator ()(Args... args) const{ // n番目の引数を受け取る実装に tuple を利用する return std::get<Index>(std::make_tuple(args...)); } }; const auto arg1 = placeholder<0>{}; const auto arg2 = placeholder<1>{}; const auto arg3 = placeholder<2>{}; arg1(1, 2, 3); // 1 arg2(1, 2, 3); // 2 arg3(1, 2, 3); // 3
こちらは operator ()
の引数を可変長テンプレート引数で受け取って std::tuple
と std::get
を利用して『任意の引数値を返す』という関数オブジェクトになっています。
また、これも『placeholder
クラス』と『arg1
, arg2
... オブジェクト』と切り分けて実装しています。
テンプレートを使用することでこういう『連番』のオブジェクトも比較的簡単に定義することが出来ますね。
[operator+
を定義]
さて、最後に関数オブジェクトを演算するための operator +
を定義します。
template<typename L, typename R> struct plus{ template<typename... Args> auto operator ()(Args... args) const{ return left(args...) + right(args...); } L left; R right; }; template<typename L, typename R> plus<L, R> operator +(L left, R right){ return {left, right}; }
まず、operator +
を定義し、その左辺値と右辺値を保持するクラスを定義します。
そして operator ()
ではその『左辺値と右辺値を評価』します。
ここがポイントですね。
つまり
関数オブジェクト + 関数オブジェクト
という式は
関数オブジェクト(引数) + 関数オブジェクト(引数)
という風に評価されます。
このように関数オブジェクトが再帰的に評価されることで arg1
や val
などの結果が最終的には『+
演算子として』処理されます。
はい、ここがポイントですね。
これにより
arg1 + arg2 + val(42) + arg1;
みたいな連続した式も問題なく展開されます。
この時にクラスが保持してる値が『関数オブジェクト』か『そうでない』かがキモになってきます。
今回定義した operator +
は左辺値と右辺値が『関数オブジェクトを受け取ること』を想定しているので『operator ()
』で評価します。
しかし、先ほど定義した val()
の holder
クラスは関数オブジェクトではなくて『値』を保持しているため『評価しないで』そのままの値を返します。
基本的にはこれの応用で他の演算子も定義すればおっけーです。
[完成]
以下が完成形になります。
#include <tuple> #include <iostream> namespace lambda{ template<typename T> struct holder{ auto operator ()(...) const{ return value; } T value; }; template<typename T> holder<T> val(T t){ return {t}; } template<std::size_t Index> struct placeholder{ template<typename... Args> auto operator ()(Args... args) const{ return std::get<Index>(std::make_tuple(args...)); } }; const auto arg1 = placeholder<0>{}; const auto arg2 = placeholder<1>{}; const auto arg3 = placeholder<2>{}; template<typename L, typename R> struct plus{ template<typename... Args> auto operator ()(Args... args) const{ return left(args...) + right(args...); } L left; R right; }; template<typename L, typename R> plus<L, R> operator +(L left, R right){ return {left, right}; } } int main(){ auto expr = lambda::arg1 + lambda::arg2; std::cout << expr(4, 2) << std::endl; auto plus3 = lambda::arg1 + lambda::val(3); std::cout << plus3(4) << std::endl; return 0; }
演算子は ADL で呼び出してるので namespace を追加してます。
とりあえず、これで完成ですね!
Expression Templates という概念はわかりづらいと思いますが、実際はこんな感じにパーツを組み合わせて式をつくっていくと考えればわかりやすいと思います。
C++11/14 でかなり実装も簡単になりましたしね。
また C++11 で言語レベルでラムダ式は追加されましたが、
filter(list, [](auto n){ return n < 0; });
よりも
filter(list, arg1 < val(0));
みたいに今回つくったラムダ式を使ったほうがコードがすっきりすると思います。
これは便利ですね。
ではみなさん、良いお年をー。
[これで終わりと思った?]
残念!まだ終わりではありません!!!
ここからが本番になります。
[C++14 で実装されたと多相ラムダと戻り値型の型推論]
さて、C++14 では新たに引数型を推論する多相ラムダと戻り値型を推論する強力な機能が追加されました。
[多相ラムダ]
ラムダ式の引数型を auto
にすることで関数テンプレートのように型推論を行ないます。
// 引数型に依存することなくラムダ式を定義することができる auto plus = [](auto a, auto b){ return a + b; }; plus(1, 2); // 3 plus(3.14f, 53.24f); // 56.38 plus(3.14f, 42); // 45.14 plus(3.14f, 42); // "homumami"
このように引数型に依存することなくラムダ式を定義することが出来ます。
多相ラムダを受け取ることができる型がほしいよね
[戻り値型の型推論]
C++14 では戻り値型を auto
とすることでコンパイラが式から型推論を行ってくれます。
template<typename T, typename U> auto plus(T a, U b){ // a + b の結果の型を推論してくれる return a + b; } plus(1, 2); // 3 plus(3.14f, 53.24f); // 56.38 plus(3.14f, 42); // 45.14 plus(3.14f, 42); // "homumami"
C++14 以前では戻り値型 decltype()
などを使用する必要がありましたが、C++14 ではそれすら必要なくなりました。
// C++11 時代は decltype() で戻り値型を定義する必要があった template<typename T, typename U> auto plus(T a, U b) ->decltype(a + b){ // a + b の結果の型を推論してくれる return a + b; }
decltype()
を使用した場合は『関数を定義するときに型が決定されてる必要』があったんですが、これにより『関数内で定義された型』も返すことができるようになりました。
// 関数内で定義したローカルクラスを返すこともできる auto make_homu(){ struct person{ std::string name; int age; }; return person{ "homu", 14 }; } auto data = make_homu(); data.name; // "homu" data.age; // 14
ただし、次のように『実行時によって異なる型を返す』ことは出来ないので注意してください。
auto func(int flag){ // Error: 同じ型を返さなければならない if( flag ){ return 0; } else { return "Error"; } }
[つまり]
もうお気づきの方もいると思いますが、これにより『ラムダ式』をそのまま返すことができるようになりました。
// C++ でクロージャっぽいものを定義 auto counter(){ int value = 0; // キャプチャした変数を書き換える場合は mutable を付属させる return [=]() mutable{ value += 1; return value; }; } count(); // 1 count(); // 2 count(); // 3
と、いうことで先ほどつくったプレースホルダなどをラムダ式を使って書き換えていきましょう
[val()
定義する 2]
まずは val
から。
これは引数をそのまま返すラムダ式を定義するだけなので簡単ですね。
template<typename T> auto val(T t){ return [=](...){ return t; }; }
はい、先ほど定義した holder
クラスは綺麗サッパリなくなって関数1つだけになりました。
すごいすっきりしましたね。
[プレースホルダオブジェクトを定義する 2]
続けてプレースホルダも定義してみます。
template<std::size_t Index> auto placeholder(){ return [](auto... args){ return std::get<Index>(std::make_tuple(args...)); }; } const auto arg1 = placeholder<0>(); const auto arg2 = placeholder<1>(); const auto arg3 = placeholder<2>();
はい、この通り placeholder
クラスではなくて placeholder
関数になりました。
また、多相ラムダを使用することで template
も省略出来ます。
こちらも実装がすっきりしましたね。
[operator+
を定義 2]
最後に operator +
ですね。
これも簡単に定義できます。
template<typename Left, typename Right> auto operator +(Left left, Right right){ return [=](auto... args){ return left(args...) + right(args...); }; }
完璧ですね…。
[まとめ]
と、言うことでまとめるとこんなコードになります。
#include <tuple> namespace lambda{ template<typename T> auto val(T t){ return [=](...){ return t; }; } template<std::size_t Index> auto placeholder(){ return [](auto... args){ return std::get<Index>(std::make_tuple(args...)); }; } const auto arg1 = placeholder<0>(); const auto arg2 = placeholder<1>(); const auto arg3 = placeholder<2>(); template<typename Left, typename Right> auto operator +(Left left, Right right){ return [=](auto... args){ return left(args...) + right(args...); }; } } // namespace lambda #include <iostream> int main(){ auto expr = lambda::arg1 + lambda::arg2; std::cout << expr(4, 2) << std::endl; auto plus3 = lambda::arg1 + lambda::val(3); std::cout << plus3(4) << std::endl; return 0; }
どうでしょう。
最初にクラスを使って実装したコードに比べてだいぶすっきりしたと思いませんか?
Expression Templates の話どこに行ったんだって感じですよね。
まあでも結果的に C++14 だとこんなコードが書けるようになるんですよ。
面白いですよね。
ちなみに C++14 + ラムダ式で実装した場合の欠点としては『constexpr
』として定義できないことです。
最初にクラスを使って実装した場合は戻り値型に constexpr
をつければコンパイル時に評価することが出来ます。
これは大変強力なのでどっちで実装するべきなのかは悩みどころですね…。
ただ、C++17 ではラムダ式が constexpr
が定義できるらしいのでそこもカバーできるようになると思います。
と、言うことで本記事は『Expression Templates を解説する』記事でもなく『オレオレ Boost.Lambda/Phoenix を実装する』記事でもなく『C++14 の多相ラムダと戻り値型の auto
が素晴らしい』という記事になります。
それではメリークリスマス。
【C++ Advent Calendar 2016 22日目】C++ で enable_if を使うコードのベストプラクティス
[追記]
@pink_bangbi typename std::enable_if<..., std::nullptr_t>::type* = nullptr の * は余計ではありませんか。これだと nullptr_t にする意味が
— akinomyoga (@akinomyoga) 2016年12月22日
すみませんすみません、修正しました。
[本編]
C++ Advent Calendar 201622日目の記事です。
『ベストプラクティス』と書いていますが、釣りタイトルですです。誰かもっといい書き方教えてください。
最近さっぱり C++ を書いてないのでいろいろと厳しい。
あ、あと最初に書いておくと本記事はあくまでも『enable_if
の使い方』に関する記事なので SFINAE 自体にはあまり言及しません。
[enable_if とは]
enable_if
は一般的にSFINAE を利用したコンパイル条件分岐で使用するためのメタ関数になります。
SFINAE とは cpprefjp から引用すると
「SFINAE (Substitution Failure Is Not An Errorの略称、スフィネェと読む)」は、テンプレートの置き換えに失敗した際に、即時にコンパイルエラーとはせず、置き換えに失敗した関数をオーバーロード解決の候補から除外するという言語機能である。
たとえば、関数のシグニチャの一部として「typename T::value_type」が書いてあり、型Tがvalue_typeという型を持っていない場合、その関数がオーバーロード解決から除外される。これによって型が任意の機能を持っているかを、コンパイル時に判定できた。
参照:任意の式によるSFINAE - cpprefjp C++日本語リファレンス
とのこと。
例えば、関数テンプレートの呼び出しの際にテンプレート引数の型が『整数型かそうでないか』で呼び出す関数を切り分けたい場合などで SFINAE と enable_if
を組み合わせて使用します。
まあ C++ を書いてる人であれば enable_if
や SFINAE などは知っていて当然ですよね。
それでは実際に enable_if
を使ったコードを書いていきます。
[C++03 時代の enable_if
]
まず、C++03 時代の enable_if
の利用方法を書いてみましょう。
C++03 ではまだ enable_if
は標準ライブラリに入っておらず、Boost を利用します。
#include <boost/core/enable_if.hpp> #include <boost/type_traits/is_integral.hpp> template<typename T> void func(T t, typename boost::enable_if<boost::is_integral<T> >::type* = 0){ std::cout << t << "は整数だよ" << std::endl; } void func(...){ std::cout << "引数は整数じゃないよ" << std::endl; } func(10); // 10は整数だよ func('c'); // cは整数だよ func(3.14f); // 引数は整数じゃないよ
http://melpon.org/wandbox/permlink/w3oZ2lSl1aS8bbTa
C++03 時代では割とオーソドックな書き方になるかと思います。
C++03 では『関数のデフォルト引数として enable_if::type
を定義してる』のが肝になります。
また『テンプレート型の条件』には is_integral
というメタ関数を使用しています。
このメタ関数は『整数型であれば is_integral::value
が true
を返し、そうでないなら false
』という関数になります。
このメタ関数と enable_if
を組み合わせることで関数呼び出しの条件分岐を行っています。
ちなみに boost::enable_if
の場合は内部で ::value
が呼び出されるので、呼び出し側では ::value
をつける必要はないので『生の型』をそのまま enable_if
へと渡します。
[C++11 時代の enable_if
]
それでは C++03 のコードをベースに C++11 に対応していきましょう。
[std::enable_if
を使う]
C++11 では標準ライブラリに enable_if
が入ったため、Boost に依存することなく enable_if
が使用できるようになりました。
ついでに boost::is_integral
も標準ライブラリ入りしたのでそれも使用するように変更します。
typename boost::enable_if<boost::is_integral<T> >::type* = 0
↓
typename std::enable_if<std::is_integral<T>::value>::type* = 0
ここで注意なのは boost::enable_if
の場合は内部で ::value
が展開されていたんですが、std::enable_if
の場合は型ではなくて値(::value
)を渡す必要があります。
boost::enable_if
から std::enable_if
にコードを変更する場合にはこのことに注意してください。
[関数テンプレートのデフォルト引数で enable_if
を定義]
C++11 では関数テンプレートでも『テンプレート型のデフォルト引数』を定義できるようになりました。
ですので、関数の引数ではなくて『関数テンプレートのデフォルト引数』で enable_if
を定義する事が出来ます。
template< typename T, // nullptr を受け取るのがポイント typename std::enable_if<std::is_integral<T>::value, std::nullptr_t>::type = nullptr > void func(T t){ std::cout << t << "は整数だよ" << std::endl; }
基本的に関数の引数で定義してた時と同じような感じですが、0
ではなくて nullptr
をデフォルト値として渡しています。
また、enable_if
の第二引数は ::type
で返す方を指定できるため std::nullptr_t
を渡してします。
これがポイントなんですが nullptr
で受け取る理由は以下の記事を参照してください。
この時に enable_if
の第二引数に std::nullptr_t
型を渡しているのもポイントなんですが、個人的には
typename std::enable_if<std::is_integral<T>::value>::type* = nullptr
みたいに省略しても基本的には問題ないと思います。
また、他には enabler
というイディオムを利用することもあります。
// enable_if で使用するためのダミーの変数を『宣言』しておく extern void* enabler; template< typename T, // nullptr の変わりに enabler を渡す typename std::enable_if<std::is_integral<T>::value>::type*& = enabler > void func(T t){ std::cout << t << "は整数だよ" << std::endl; }
[C++11 のまとめ]
#include <utility> #include <iostream> template< typename T, typename std::enable_if<std::is_integral<T>::value, std::nullptr_t>::type = nullptr > void func(T t){ std::cout << t << "は整数だよ" << std::endl; } void func(...){ std::cout << "引数は整数じゃないよ" << std::endl; } int main(){ func(10); // 10は整数だよ func('c'); // cは整数だよ func(3.14f); // 引数は整数じゃないよ return 0; }
http://melpon.org/wandbox/permlink/gTyeZk1qGdyYogV5
[C++14 時代の enable_if
]
C++14 ではさらに enable_if
の alias templates である enable_if_t
というメタ関数が標準ライブラリに追加されました。
この enable_if_t
を使用することで typename
と ::type
を省略する事ができるようになります。
typename std::enable_if<std::is_integral<T>::value, std::nullptr_t>::type = nullptr
↓
std::enable_if_t<std::is_integral<T>::value, std::nullptr_t> = nullptr
また、alias templates 自体は C++11 で実装された機能なので、以下のように定義しておけば C++11 でも使用することが出来ます。
template< bool B, class T = void > using enable_if_t = typename enable_if<B,T>::type;
参照:std::enable_if - cppreference.com
[C++17 時代の enable_if
]
よくわからんので誰か書いて。
[まとめ]
まとめると最新版の enable_if
を使ったコードはこうじゃ。
#include <type_traits> #include <iostream> template< typename T, std::enable_if_t<std::is_integral<T>::value, std::nullptr_t> = nullptr > void func(T t){ std::cout << t << "は整数だよ" << std::endl; } void func(...){ std::cout << "引数は整数じゃないよ" << std::endl; } int main(){ func(10); // 10は整数だよ func('c'); // cは整数だよ func(3.14f); // 引数は整数じゃないよ return 0; }
http://melpon.org/wandbox/permlink/7WmFlkGJQ94yVRAU
と、言う感じで各時代の enable_if
の使い方をまとめてみました。
enable_if
を使った SFINAE はメタプログラミングをする C++er に取っては一般的なテクニックだと思いますが、結構時代によって書き方が変わってきていますね。
今回は enable_if
の記事だったのであまり SFINAE には突っ込みませんでしたが、SFINAE を使ったテクニックも時代によって変わってきているので、気になる方は調べてみるとよいと思います。
また、他に『enable_if
はもっとこうしたほうが簡単に書けるよ!』みたいなツッコミがあればぜひ教えてもらえると助かります。
あと C++17 はよくわからないので誰か書いてください。 C++17 だと constexpr if があるのでそもそも enable_if
が要らなくなるという話もある
[おまけ]
ちなみに enable_if
の第二引数には ::type
が返す型を指定することが出来ます(デフォルトでは void
型を返す。
ですので関数定義時に戻り値型が決まっているなら次のように『戻り値型』に enable_if
を定義することもできます。
template< typename T > // 戻り値型が void ならそのまま enable_if::type を戻り値型に typename std::enable_if<std::is_integral<T>::value>::type func(T t){ std::cout << t << "は整数だよ" << std::endl; } template< typename T > // 任意の型を返す場合は enable_if の第二引数に渡す typename std::enable_if<std::is_integral<T>::value, int>::type plus10(T t){ return t + 10; } template< typename T > // もちろん enable_if_t を使用して typename と ::type を省略することも std::enable_if_t<std::is_integral<T>::value, T> twice(T t){ return t + t; }
これは C++03 でも有効なのでデフォルト引数で定義したくない方は使ってみるとよいと思います。
unite-oldfiles つくった
【初心者C++er Advent Calendar 2016 13日目】予約済み識別子に注意しよう
初心者C++er Advent Calendar 2016の13日目が空いてるようなの書きました。
本記事は初心者C++er Advent Calendar 201613日目の記事になります。
初心者C++er Advent Calendar 2016では 3回目の記事になりますが、今回は予約済み識別子の話です。
[予約済み識別子とは]
『予約済み識別子』とは『言語仕様』や『処理系』などで使用するために予約されている識別子のことです。
C++ では以下の3つのいずれかを満たす名前が『予約済み識別子』として予約されています。
- グローバルスコープを持ち、
_
で始まる名前 _
で始まり、その次が大文字の名前__
を含む名前
例えば、__LINE__
みたいな定義済みマクロの名前であったり、処理系依存であれば msvc の _MSC_VER
定義済みマクロがそれにあたります。
要は『__
を含む名前とかはコンパイラやライブラリの実装で使う可能性があるからユーザは使わないでねー』というためのものですね。
ですので、以下のようなコードは未定義の動作になります。
// インクルードガードなどで名前の先頭に _ をつけてるのでダメ #ifndef _INCLUDED_PATH_TO_ #define _INCLUDED_PATH_TO_ #endif // _ から始まってる名前なのでダメ typedef struct _X { // __ が含まれてる名前なのでダメ int value__; } X;
インクルードガードのマクロ名に『_
から始まってる名前』を使ってるコードをよく見かけますが、これも厳密にいえば未定義の動作になります。
[まとめ]
実際のところ予約済み識別子を使ったところでそこまで問題になる可能性は低いと思います。
しかし、わざわざ未定義の動作になる可能性があるコードを書く必要もないので、C++ を書くときは注意するとよいと思います。
言語によっては _value
みたいに『先頭に _
をつける事』を推奨してることもありますが、C++ では基本的に避けるべきです。
ですので、C++ では以下のように『名前の末尾に _
をつけること』が多いです。
// だめ auto _value = 42; // おっけー auto value_ = 42;
こんな感じで基本的には名前の先頭に _
をつけるのを避けるように気をつければ問題ないと思います。
以上で、予約済み識別子の説明は終了です。
予約済み識別子警察には気をつけましょう。
[余談]
Vim を使ってる人は以下のプラグインをいれてみるといいと思います。
[参照]
RSpec のマッチャで === を使う
通常 eq
を使用すると ==
が使われるんですが、これを ===
で比較したかった。
このように任意の演算子でテストしたい場合は次のように書ける。
expect(9).to be > 6 expect(3).to be <= 3 expect(1).to be < 6 expect('a').to be < 'b' expect(String).to be === "hoge"
これは知らなかったので便利。