こんにちは。田原です。
前回までで テンプレートの明示的特殊化と部分的特殊化とオーバーロード(まとめて表現できる言葉が欲しいですね)を使って必要に応じて異なるテンプレートを定義する方法を解説しました。今回はその応用を1つ解説します。SFINAEとテンプレートの合せ技でよく出てくる「メンバの有無判定」を取り上げてみました。
1.まずは期待仕様について
「メンバの有無を判定するテンプレート」と言われると、hasMembaer<クラス, メンバ名>::value
のようなイメージでクラスとメンバ名を与えたら、クラスがそのメンバを持っていると true になるテンプレートを期待すると思います。ちょっと頭だけ書いてみました。
template<class tClass, typename tMember> bool hasMember();
しかし、これを作ることはできません。鋭い方は既にお分かりと思いますが、メンバ変数とメンバ関数のどちらとも型ではないのでテンプレートの型パラメータに与えることができません。
では、非型パラメータならどうか?ですね。
template<class tClass, typename tMemberType, tMemberType nMember> bool hasMember();
これを書くこと自体は可能です。しかし、意味があるものは作れません。
nMemberパラメータには有無判定したいメンバを指定する必要があります。そのメンバがある時は指定できますが、無い時は当然指定できません。エラーになります。
でも、エラーはSFINAEで無視されるのでは?と思いますよね。(私は思いました。)残念ながらこのケースではSFINAEは働きません。次のhasMemberの呼び出しを実体化する際に、Foo::barがないのでエラーになるのですが、それがSFINAEで無視されると仮定します。さて、このif文はどのようにコンパイルされるのでしょうか?
SFINAEにより無視した後、何か他のエラーにならないものを選択できないとコンパイルできませんが、実引数の時点でエラーですのでどのテンプレート定義を選択してもエラーのままです。つまりコンパイルする術がないのです。(解ってしまえば当たり前なのですが、長い間ここが良く解っていませんでした。)
class Foo { }; int main() { if (hasMember<Foo, int, Foo::bar>()) std::cout << "has member\n"; }
つまり、実引数はコンパイルに通るものを与えないといけません。ということは、指定のクラスが必要なメンバを持っていないとhasMemberテンプレートに与えることができないわけです。そのため、hasMemberを呼び出す時点でそのメンバ変数があることが判っているクラスしか指定できないのです。全く役に立たないhasMemberですね。
テンプレートが超拡張されて、型パラメータ、非型パラメータに続いて、「名前」を指定できる名前パラメータ的なものが用意されない限り、hasMembaer<クラス, メンバ名>::value
のようなテンプレートを実現するのは無理ではないかと思います。
2.ではどうするのか?
名前パラメータ的なものって、実は#defineで定義するマクロなら可能です。マクロの引数は C++ の構文とは全く無関係です。単にソース・コードの文字を置き換える機能ですから。それが型名だろうが変数名だろうが気にしません。
つまり、マクロを使ってメンバーの有無を判定するテンプレートを作るわけです。
メタ・メタ・プログラミング
テンプレートをプログラムすることははクラスや関数を作り出すプログラムを書くことですから「メタ・プログラミング」と呼ばれます。そのテンプレートを作り出すマクロを書くことは「メタ・メタ・プログラミング」かも。
この1~2年ほどの夢だったのですが、このブログを書くために色々考えてやっと成功しました。
#include <type_traits> #define HAS_MEMBER(dMemberName) \ template<class tClass> \ class has_##dMemberName \ { \ template<class tInnerClass, int dummy=(&tInnerClass::dMemberName, 0)>\ static std::true_type check(tInnerClass*); \ static std::false_type check(...) ; \ static tClass* mClass; \ public : \ static const bool value = decltype(check(mClass))::value;\ }
使うためには、事前準備が必要です。テンプレートは使う前に定義が必要ですから使う前に準備が必要なのです。判定したいメンバ名を指定してテンプレート定義を生成します。
HAS_MEMBER(var); HAS_MEMBER(func);
これにより、has_var<クラス>テンプレート
とhas_func<クラス>テンプレート
が定義されます。
そして、これらのテンプレートは指定したクラスがvarやfuncを持っていたらvalueがtrueになります。
次のような具合です。
#include <iostream> struct Foo { }; struct Bar { int var; void func(int iParam) { std::cout << "func(" << iParam << ")\n"; } }; int main() { std::cout << "has_var<int>::value=" << has_var<int>::value << "\n"; std::cout << "has_var<Foo>::value=" << has_var<Foo>::value << "\n"; std::cout << "has_var<Bar>::value=" << has_var<Bar>::value << "\n"; std::cout << "has_func<int>::value=" << has_func<int>::value << "\n"; std::cout << "has_func<Foo>::value=" << has_func<Foo>::value << "\n"; std::cout << "has_func<Bar>::value=" << has_func<Bar>::value << "\n"; }
has_var<int>::value=0 has_var<Foo>::value=0 has_var<Bar>::value=1 has_func<int>::value=0 has_func<Foo>::value=0 has_func<Bar>::value=1
3.has_varテンプレートの解説
まず、マクロは展開される時、単純に仮引数が与えられた実引数に置き換えられます。また、##はマクロの中だけで使えるプリプロセッサ演算子で前後の文字列を単純に繋げます。
従って、HAS_MEMBER(var);
は、次のようなテンプレート定義に展開されます。
template<class tClass> class has_var { template<class tInnerClass, int dummy=(&tInnerClass::var, 0)> static std::true_type check(tInnerClass*); static std::false_type check(...) ; static tClass* mClass; public : static constexpr bool value = decltype(check(mClass))::value; }
ポイントは「関数テンプレートcheck()とその通常関数check(…)によるオーバーロード(4-6行)」、および、「その呼び出し(9行)」です。
厳密にはメンバ関数テンプレートですが、この使い方の場合は今まで解説してきた関数テンプレートと同じように振る舞いますので気にしないでください。
(あああ、そういえばメンバ・テンプレートの解説を忘れてます。後日解説します。)
3-1.オーバーロード関数の定義
まず、関数テンプレートcheck(tInnerClass*)は、通常関数check(…)とオーバーロードされています。
通常関数でオーバーロードされた場合、一般には通常関数の方が優先的に選択されるのですが、可変長引数(...)
の場合は逆に優先順位が落ちるようです。可変長引数はなんでも受け入れますから、優先順位が高いと他の全てが選択されませんのでそのように決めたのだと思います。
ですので、この2つのcheck()は、関数テンプレートに適合する場合は関数テンプレートが選択されます。
3-2.SFINAEによるオーバーロードの解決
9行目にcheck(mClass)の呼び出しが記述されています。この呼び出しでどちらかのcheck()が実体化されます。mClassは8行目で指定クラスへのポインタとして定義されていますので渡しているパラメータはtClass*
型です。
さて、前回解説したように関数テンプレートはプライマリー・テンプレートをオーバーロードできます。その際、非型パラメータを指定し、その非型パラメータの記述部分でstd::enable_ifを用いて、エラーが発生したら選に漏れるように設計していました。
今回は、プライマリー・テンプレート自体はオーバーロードしていないため、ちょっと手抜きして非型パラメータのデフォルト値でエラーを発生させています。int dummy=(&tInnerClass::var, 0)
の部分です。(&tInnerClass::var, 0)
はカンマで区切って2つの値を記述しています。この場合、カンマで区切った最後の値が有効になります。途中のものは評価されますが、結果は無視されます。ですので、dummyは0がデフォルト値となります。
そして、途中の&tInnerClass::var
はメンバvarへのポインタです。(varは関数・変数 × static・非staticどれでもOKです。)つまり、tInnerClassがメンバvarを持っていればエラーになりません。逆にvarを持っていなければエラーになります。
- エラーになった場合、代替案のcheck(…)が控えていますから、SFINAEによりcheck(…)が選択されます。
- エラーにならなければ関数テンプレートの方のcheck()が選択されます。
これにより、9行目のcheck(mClass)は、ポインタmClassが指すクラスがメンバvarを持ってなければ通常関数のcheck(…)、持っていれば関数テンプレートのcheck()になります。
3-3.ちょっと不思議な戻り値の型の取り出し
そのcheck(mClass)は、更にdecltype()に渡されています。前回軽く説明したように「decltyeはその引数の「型」を返します。」そして、通常関数のcheck(…)の戻り値の型は std::false_type です。関数テンプレートcheck()の戻り値の型は std::true_type です。
従って、9行目のdecltype(check(mClass))
は、前者ならstd::false_type、後者ならstd::true_typeです。
そして、std::false_type::value、std::true_type::valueのどちらかをhas_var<クラス>::value
へ設定しています。
check()の中身はなくてよい!?
9行目でcheck()関数を呼び出しているように見えますね。本当に呼び出した場合、中身が無いとエラーになります。しかし、実際に呼び出さなければ中身がなくてもエラーになりません。
この9行目のcheck()関数呼び出しはdecltype()で囲われています。decltype()は戻り値の型を返しますから、check()の実際の戻り値はなんでもよくて、戻り値の型さえ分かればコンパイルできます。
そして、上記のようにどちらのcheck()が呼ばれるのかが判定できれば、check()の戻り値の型も分かります。従って、check()関数自体は呼び出されることはないので中身を定義する必要はありません。テンプレート・メタ・プログラミングでは時々見かけるテクニックです。
3-4.流れのまとめ
長いですね。纏めると次のようになります。
has_var<Foo>::value
の場合
- 9行目のcheck(mClass)で、check(Foo*)が実体化される
- Foo::varは存在しないため、これは通常関数のcheck(…)が選択される
- check(…)の戻り値はstd::false_typeなので、decltype(check(mClass))はstd::false_typeである
static constexpr bool value = std::false_type::value;
となるので、has_var<Foo>::value
はfalseとなる
has_var<Bar>::value
の場合
- 9行目のcheck(mClass)で、check(Bar*)が実体化される
- Bar::varは存在するため、これは関数テンプレートのcheck()が選択される
- check()の戻り値はstd::true_typeなので、decltype(check(mClass))はstd::true_typeである
static constexpr bool value = std::true_type::value;
となるので、has_var<Foo>::value
はtrueとなる
3-5.ソースとCMakeLists.txt
Wandboxで確認する。4.まとめ
本格的なテンプレート・メタ・プログラミングっぽくなってきました。まだ私が解説できるレベルですので難易度は低い方ですが、なかなかハードと思います。
テンプレート・メタ・プログラミングで一番困るのはデバッグです。普通のプログラミングの場合、ともかくコンパイルに通して実行しつつバグを探します。実行しながらですので、シーケンシャルな実行をデバッガを使ったり、ログを取ったりしながら追いかけることができます。
テンプレート・メタ・プログラミングの場合、コンパイルに通ればtypeid().name()を使ってどんな型へ展開されているのか見ることができますが、なかなかコンパイルに通らないです。なので、#if 0~#endifで括りつつ、端から順番にコンパイルに通しつつ、typeid().name()を使ってデバッグを進めることも多いです。ですのでTYPENAMEマクロが手放せません。
そして、頭が痛いのがエラー・メッセージの長さです。今回はクラスが1つしかないし、STLをほぼ使っていないのでエラーメッセージは短いのですが、ちょっと複雑なテンプレートを作ると長いです。
良く使うstd::stringは実はテンプレートのtypedefです。その名前はstd::basic_string<char, char_traits<char>, allocator<char> >
なので、これのコンストラクタは、std::basic_string<char, char_traits<char>, allocator<char> >:: (const char* s, const allocator<char>&)
です。これにマッチする物がないみたいなエラーが出ると本当長いです。
エラー・メッセージをコピペしてエディタに貼り付け、区切り毎に改行とインデントを付けて読んでます。
本当に苦労しますが、うまく行った時の快感は格別です。
さて、次回はdecltypeとstd::declvalについて解説したいと思います。decltypeは既にちょこまか出てきてますが、これはC++11の結構大きな立役者の1つと感じます。その辺りを絡めて解説したいと思います。お楽しみに。
クラステンプレートの部分的特殊化が
プライマリーテンプレートより実体化の優先順位が高い
という仕様を利用した
Detection Idiom
というメタ関数の実装テクニックがあります。
メンバ関数を書くのはもはや時代遅れのようです。[要出典]
コード例:https://wandbox.org/permlink/aFtZSOQWRoiR3eKu
また、これはC++14のジェネリックラムダによるオーバーロードと、
条件演算子の暗黙の型変換を悪用した方法ですが、
汎用的なHAS_XXXマクロを書くことができます。
一々マクロによるクラス定義の生成をせずに、
直接マクロがコンパイル時計算可能な式に置き換わります。
メンバ型名を調べるマクロを書いてみました。
コード例:https://wandbox.org/permlink/F9vJuSwGVpgl4pUw
いなむのみたまさん、コメントありがとうございます。
Detection Idiom なるほど!! TMPは先人が作ったパターンを利用しないとなかなか自力で1から作るのは難しいので、これは良いものですね。
void_tはやっと標準規格に採用されたのですね。
SFINAEの学習は主に↓にお世話になったのですが、この ignore ですもんね。
https://qiita.com/hidachinoiro/items/b866966eef77ac84f8f2
std::enable_ifで十分だから入らなかったのかなと思ってました。やっぱり有った方がよいのですよね。
ところで、マクロと組み合わせたらHAS_MEMBER(クラス、メンバ名)が書けるのですね!!
考えてみるとあり得る話です。気が付きませんでした。
でも、これは私ではとてもとても作れないです。下記がポイントですね。
①ラムダ式を基底クラスとし、オーバーロード関数を2つ 「インライン」で定義している。
②しかし、①は呼び出さず、?:演算子の戻り値の型を決定するために使って、コンパイル時定数としている。
この2つのテクニックを初めてみました。
あああ、少し判定対象を変えてみたらダメでした。現状では、「メンバ型」の有無判定しかできていないようです。
https://wandbox.org/permlink/XjHDQXcyhRTMneU5
あ、でも、下記でできました。
https://wandbox.org/permlink/ify3yRafwfWLOBDV
更に合体したら、型、関数、変数の全てに対応できました。なるほど、これは凄い!!
https://wandbox.org/permlink/hzU6wIABlyIqPcKa