よくあるやり方
c++において「ある型にある名前のメンバが存在するか」調べるにはこんな書き方があります。(c++11)
template<typename T>
struct has_hoge {
private:
template<typename S, void(S::*)() = &S::hoge>
static std::true_type test(S*);
static std::false_type test(...);
public:
static constexpr bool value = decltype(test((T*)nullptr))::value;
};
これは雑な一例ですけど、大体こんな感じで調べたい名前ごとにテンプレート構造体を定義することになります。
大抵上記をマクロ関数化して以下のように使いますが、使う前に必ず定義しなくてはならないことに変わりはありません。
DEFINE_HAS_MEMBER(hoge); // ここでhogeを調べる上記の構造体を定義
...
static_assert(has_hoge<Hoge>::value, "hogeない");
今回考える実装
定義
template<typename T, typename L>
constexpr auto member_test(L l) -> decltype(l(static_cast<T*>(nullptr)), std::true_type{}) { return{}; }
template<typename T>
constexpr std::false_type member_test(...) { return{}; }
// 指定の名前の何らかのメンバが存在するか判定
#define HasAnyMember(type, member) HasTypedMember(type, member, void)
// 指定の名前と型を持つメンバが存在するか判定
#define HasTypedMember(type, member, member_type) \
(member_test<type>([](auto* t) -> decltype(static_cast<member_type>(&std::remove_pointer_t<decltype(t)>::member), static_cast<void>(0)) {}) ^ \
member_test<type>([](auto* t) -> decltype(std::declval<std::remove_pointer_t<decltype(t)>::member>(), static_cast<void>(0)) {}))
呼び出し
struct Hoge
{
using using_ = void;
union union_ {};
enum enum_ {};
struct struct_ {};
int variable;
static constexpr int constant = 0;
void function() {}
// 純粋仮想関数が入っててもOK
// オーバーロードされててもOK
virtual void function() const = 0 {}
template<typename ...Args>
static constexpr std::size_t function(Args&& ...args) { return sizeof...(Args); }
};
// マクロ引数のカンマ置換用
#define COMMA ,
// 様々なメンバをチェック
static_assert(!HasAnyMember(Hoge, noExists));
static_assert(HasAnyMember(Hoge, using_));
static_assert(HasAnyMember(Hoge, union_));
static_assert(HasAnyMember(Hoge, enum_));
static_assert(HasAnyMember(Hoge, struct_));
static_assert(HasAnyMember(Hoge, variable));
static_assert(HasAnyMember(Hoge, constant));
static_assert(HasAnyMember(Hoge, function));
static_assert(HasTypedMember(Hoge, function, void(Hoge::*)() const));
static_assert(HasTypedMember(Hoge, function<int COMMA std::string>, std::size_t(*)(int&& COMMA std::string&&)));
今回は事前定義の要らないメンバチェッカーを作りました。
そのためテンプレート関数内などで直接呼び出して使用可能です。
また、事前定義から解放された代わりにある制約が課せられています。
それはテンプレート引数やdecltype内で(直接)使えないということです。
これではメタプログラミングをする上で致命的な制約になってしまいますが、
このマクロに使い道はないのでしょうか?その点について少し掘り下げてみたいと思います。
そもそもなぜメタ領域で使えないのか
まず、上記機能の制約について読み解いてみます。
コアとなるマクロに注目しましょう。
#define HasTypedMember(type, member, member_type) \
(member_test<type>([](auto* t) -> decltype(static_cast<member_type>(&std::remove_pointer_t<decltype(t)>::member), static_cast<void>(0)) {}).value ^ \
member_test<type>([](auto* t) -> decltype(std::declval<std::remove_pointer_t<decltype(t)>::member>(), static_cast<void>(0)) {}).value)
このマクロは2つの部分からなります。
1行目が変数・定数・関数などアドレスをとれるもののチェックを行い
2行目がクラス内の型の存在を調べています。
その違い自体は重要ではなく、むしろ共通点が要です。
それはテンプレート関数にラムダを渡しているという点です。
なぜそのような形になっているかというと
- メンバの存在を調べるにはSFINAEを利用したオーバーロード解決でしかできない
- そのためにはテンプレート関数化して不適格な関数が実体化されないようにするしかない
- しかし、テンプレート関数やクラスは関数ローカルで定義できない
という三段論法的な理由によります。
つまり、SFINAEを行うテンプレート関数部分はグローバルに置き、SFINAEの条件はローカルからラムダで渡すように分離しました。
マクロから呼び出しているテンプレート関数の定義を確認しましょう。
template<typename T, typename L>
constexpr auto member_test(L l)
-> decltype(
l(static_cast<T*>(nullptr)), // ここでTに対する判定を行う
std::true_type{}) {
return{};
}
受け取ったラムダをdecltype内で呼び出し、その呼び出しが適格であればtrue_typeを返し、
不適格であればfalse_typeを返す方の関数に解決されるというわけです。
以上が今回の実装となった顛末です。
そしてここで本題となるわけですが、このマクロがテンプレート引数やdecltype内で使えないのは
ラムダは評価されない文脈では定義できないという仕様のためです。
どういうことかというと、要はメタプログラミング的コードしか書けないところでラムダを使っちゃダメ!ということで
それがまさにテンプレート引数やdecltype内ということです。
// 全部だめ
template<typename T = []{}>
void hoge();
hoge<[]{}>();
decltype([]{}) hoge;
member_test関数が宣言だけでなく実装も必要としていたりするのは大体この仕様のせいです。
あくまで関数の呼び出しになるように展開しなくてはなりません。
ニッチな使い道
もうこの時点で敗戦処理感が出てきましたが・・・なんとかこのメンバチェッカー使えないでしょうか?
ちょっと思いついたところを書いてみます。
static_assertに使ってみる
template<typename T>
void CallFoo(T& t) {
static_assert(HasTypedMember(T, foo, void(T::*)()));
t.foo();
}
テンプレート関数が実体化されたときに型Tに要求されるメンバが無かったら
static_assertでコンパイルエラーを出すようにしてみましょう!・・・・
って、どうせアクセスする場所でエラー出るんだからいらないですよね。
メンバチェック用のテンプレート定数を気軽に定義してみる
template<typename T>
static constexpr bool has_foo = HasAnyMember(T, foo);
static_assert(has_foo<Hoge>);
結局メンバ名ごとに事前定義が必要ですが、テンプレート定数だけなので構造体定義してた頃よりはすっきり書けるようになっています!
せっかくだからマクロ化してしまいましょう!
#define DEFINE_HAS_MEMBER(member) \
template<typename T> \
static constexpr bool has_ ## member = HasAnyMember(T, member)
DEFINE_HAS_MEMBER(foo);
・・・。これじゃ今までと何も変わらないじゃないか・・・
if constexprと組み合わせてみる
テンプレート引数内で使えないのでここまで一見メタプログラミングにまるで役に立たないかと思われましたが
ありました・・・!唯一何とかなりそうなのが!
template<typename T>
auto clone(T* src) {
constexpr bool cloneable = HasAnyMember(T, Clone);
if constexpr(cloneable) {
return src->Clone();
}
return new T(*src);
}
これは受け取ったオブジェクトのクローンを返す関数です。
もし引数の型にCloneというメンバがあればその呼び出しに、そうでなければコピーコンストラクタ呼び出しに
切り替えます。
これなら一応メタプログラミングですし、役に立つ場面もあるかもしれません。
心に余裕のある方、遊び心のある方にはぜひ一度ご検討願いたいと思います!
気になった方もいるかもしれませんが、わざわざcloneableにいったんマクロ結果を代入しているのはわざとです。
なんとこのマクロif constexprの条件にも直接書けません!
ラムダとメタプログラミングの相性の悪さ、何とかならないんでしょうか!
C++20版(追記)
c++20ではお待ちかねコンセプトが使えるようになったのでメンバチェックは基本requires節で行うことになりましたね。
しかし、その場合特定のメンバチェック用のコンセプトを定義しておかなくてはなりません。
c++20では完全に前準備のいらない、しかもどこにでも書けるようになった無制限のメンバチェック式が使えるようになりました。
それがこちらになります。
// Hoge型のhoge()関数をチェック
is_invocable_v<decltype([](auto&&_) ->decltype(_.hoge()) {}), Hoge>
decltypeが2回も出てきて読みにくいですがマクロ化すればその辺りもすっきりはするかと思います。
c++20から評価されない文脈でのラムダが記述できるようになったのですが、それはジェネリックラムダの実体化時に不適格なコードが関数本体に現れたらそれが評価されない文脈であっても即コンパイルエラーとなるという厳しい条件の下許可されているため、requires節のようにラムダ本体に_.hoge()と書くことはできません。また戻り値の型をコンセプトのように制約することもできますがちょっと面倒なので単純な判定をしたい時だけラムダ版でも良さそうです。
まとめ
コンセプト採用まで待とう!
※追記
先人の方がよく似たものを作ってましたC++メタ関数のまとめ
やはり同様の問題は抱えているようですね。
※追々記
コンセプトが出たおかげでかなり綺麗に書けるようにはなったと思います。
しかし、元々メンバチェックのために型特性テンプレートなどを事前定義したくないというのがモチベーションにあったのでc++20的にはラムダと標準traitsでそれが実現できたことの方が嬉しいですね。