C++でメタプログラミングの基礎
最近C++で学んだことのメモです。主にメタプログラミング周り。
twitter上ではRust人気に押されている感はありますがC++も進化を続けていて、特にconstexprに代表されるコンパイル時計算まわりは非常に強力だなと思えます。
C++98付近で知識が止まっているなら「へぇ、こんなことができるんだ」と思えるかもしれません。私は思いました。
内容はSFINAEメインです。本来はテンプレートの部分特殊化なども触れるべきだと思うのですが、使いこなしの経験が浅いので触れていません。
前提知識:型推論autoと戻り値型の後置
C#のvarに相当するものとしてautoが入りました。C#と違ってこれは戻り値にも使えます。
auto Hoge() { return 1; //整数値リテラルの1から推論して戻り値はint } int main() { auto x = Hoge(); //勿論変数にも使える std::cout << typeid(x).name() << '\n'; // int を出力。余談ですが最近 std::endl あまり見ないですね… return 0; }
また、戻り値にautoを使いつつ後置で型を指定することができます。もちろん指定した型に変換できない値をreturnするとコンパイルエラーになります。
auto Hoge() -> int { return 1; }
前提知識:decltype
decltype()は式を受け取って、式の評価した結果の型を返します。
auto main() -> int { int x { 10 }; decltype(x) y = x + 10; //変数xを評価した結果はintなのでdectype(x)はint decltype(1 + 1) z = y; //1 + 1 を評価した結果はintなので decltype(1 + 1)はint std::cout << y << ' ' << typeid(decltype(y)).name() << '\n'; // 20 int return 0; }
SFINAE
さて、templateと組み合わせてdecltypeを戻り値に使ってみます。
template<class T> auto Hoge(T& t) -> decltype(t.Fuga(), true) { std::cout << "TはFuga()を持つ" << '\n'; return true; } auto Hoge(...) -> bool { std::cout << "TはFuga()を持たない" << '\n'; return false; }
decltype(t.Fuga(), true)
についてですが、C++のカンマ演算子は、カンマの前の式を評価しつつ結果は捨て、カンマの後ろの式を評価した結果を返します。よって、decltype(t.Fuga(), true)
は、t.Fuga()を評価しつつ結果はtrueを評価したboolになりますのでHogeの戻り値はboolです。
しかし型引数T型のインスタンスtがFuga()を持たなかった場合、decltypeは失敗します。
ここでC++のSFINAEというルールがでてきます。「Substitution Failure Is Not An Error (置換の失敗はエラーではない)」の略で、テンプレート解決に失敗した場合は、即座にコンパイルエラーにせずオーバーロードの解決から外れるだけという規則です。※もちろんオーバーロードが一つも見つからなかったらコンパイルエラーですが。
上の例では、Fuga()をもたない引数を渡したら Hoge(...) のほうに解決されるので、メソッドの有無で処理を静的に分岐できます。
struct X { void Fuga() { } }; struct Y { }; auto main() -> int { X x{}; Y y{}; std::cout << Hoge(x) << '\n'; std::cout << Hoge(y) << '\n'; }
TはFuga()を持つ 1 TはFuga()を持たない 0
補足:std::declval()
先の例のdecltype(t.Fuga(), true)
についてですが、戻り値の型やtemplateの型引数にdecltypeを使う場合、式に用いる変数はナントカ型の変数であれば実体は何だって良いです。可能ならばdecltype(true)の代わりにdecltype(bool)とか書きたいくらいです。(書けませんが)
さて、「ナントカ型の変数であれば実体は何だって良い」モノを作るために、<utility>
にstd::declval<T>()
があります。
これを使うと、decltype(t.Fuga(), std::declval<bool>())
と書くことができ、trueやfalseはどうだってよくただboolを返したいということが伝わりやすくなりますし、自作のstructを渡すときもコンストラクタがどうだっけ、などは気にしなくて良くなります。
detection イディオムとconcept
先の例ではオーバーロードで呼び出すメソッドを分岐しましたが、型が特定の条件を満たすかをboolの定数式でもらえるとif constexpr
でコンパイル時分岐に使えたりほかにも面白い用途に使えたりします。型が特定の条件を満たすかを定数式で返すための最も簡単な方法はC++20で追加されたconceptだと思います。
template<class T> concept HasFuga = requires(T & t) { t.Fuga(); };
conceptは、templateの型引数につけて型制約に使うことが多いと思いますが、それ自体がboolを返す定数式でもあります。
// // 型制約としての用法 // template<HasFuga T> //型制約 TはHasFugaでなければならない struct C { }; struct X { void Fuga() { } }; struct Y { }; auto main() -> int { C<X> x; C<Y> y; //コンパイルエラー! E3244 テンプレート制約がみたされていません }
// // bool型の定数式としての用法 // auto main() -> int { //constexpr なので、XがHasFugaを満たさない場合コンパイル時にこの分岐は消滅して実行時コストゼロ if constexpr (HasFuga<X>) { std::cout << "XはFugaを満たす" << '\n'; } return 0; }
std::enable_if:特定の条件を満たすときだけ関数を定義する
bool型の定数式をもらえるとできる面白いことの一つに、std::enable_ifがあります。これは<type_traits>
ヘッダーに存在し、特定の条件を満たすときだけ関数を定義することができます。上手く使えばバイナリサイズを削減できそうです。
// std::enable_if<bool Condition, T> // std::enable_if<bool Condition, T>::type は Tを返すので、 // std::enable_if<bool Condition, T>::type を戻り値としておけば // ConditionがtrueのときはTが戻り値となるが、 // ConditionがfalseのときはSFINAEでオーバーロード解決から外される=関数が定義されない template<class T> auto DoFuga(T& t) -> std::enable_if<HasFuga<T>, void>::type { t.Fuga(); }
detection イディオム
concept以前のC++で型が特定の条件を満たすかを検出してboolの定数式で返す手法はdetectionイディオムと呼ばれ以下の記事に詳しいです。 onihusube.hatenablog.com
conceptが使えるようになったのでdetectionイディオムはもう不要かというとそういうこともなく、conceptはfriendと組み合わせることができないようで非publicな関数などを検出できないのでまだdetectionイディオムは使いでがあります。