はじめに
この記事は、C++ Advent Calendar 2018 の18日目です。
前日の記事は @_EnumHack さん
明日の記事は @Kogia_sima さんです。
本記事のテーマはC++erの大好きなmember detectorです。
Member detectorとは?
さてMember detectorとは、なんでしょうか?More C++ Idiomsのこちらの記事から引用してみます。
Compile-time reflection capabilities are the cornerstone of C++ template meta-programming. Type traits libraries such as Boost.TypeTraits and TR1 header provide powerful ways of extracting information about types and their relationships. Detecting the presence of a data member in a class is also an example of compile-time reflection.
ふむ。要するに、コンパイル時にあるクラスが特定のメンバを持っているかどうかを確認するためのイディオムということですね。
そんなもの何に使うの?と思うかもしれませんが、テンプレートを用いたGeneric programmingにおいて、テンプレートパラメータに制約を課せるということは、非常に有用です。例えば、
template <typename T> inline
void print_ok_or_ng(const T& o) {
if (o.size() == 0) {
std::cout << "NG" << std::endl;
return;
}
std::cout << "OK" << std::endl;
}
みたいな処理があったとしましょう。当然T
にはどんな型でも入ってしまうため、
do_something(1); // だめ! int は size() をもってない!
こんなコードを書いてしまうかもしれません。
まぁ、このくらいであれば、コンパイルエラーを見れば、ああなーんだdo_something
内でo.size()
してる箇所が悪いんだな〜、とすぐにわかると思います。
しかし、実際にコーディングしていくと、関数呼び出しのネストはどんどん深くなっていき、
template <typename T> inline
bool is_empty(const T& o) {
return o.size() == 0; // ここでエラーになる
}
template <typename T> inline
bool check_length(const T& o) {
return !is_empty(o);
}
template <typename T> inline
bool check(const T& o) {
return check_length(o);
}
template <typename T> inline
void print_ok_or_ng(const T& o) { // こいつに変なのをいれたのが原因だ!
if (check(o)) {
std::cout << "OK" << std::endl;
return;
}
std::cout << "NG" << std::endl;
}
という感じで、問題のある箇所を探すのがどんどん大変になるため、genericな関数を書くのであれば、なるべく入口側でエラーになってくれるのが好ましいのです。
そこで出てくるのがmember detectorです。このmember detectorというのは、ある特定のクラスが、メンバ関数をもっているかだったり、ネストされた型をもっているかを判定してくれるものでしたね。
もし、上記のようなシチュエーションでメタ関数has_size
のようなものがあった場合、
template <typename T> inline
typename std::enable_if<
has_size<T>::value,
void
>::type
print_ok_or_ng(const T& o) { // Tがsize()を持ってない時点でオーバーロードの候補から外れる!
if (check(o)) {
std::cout << "OK" << std::endl;
return;
}
std::cout << "NG" << std::endl;
}
としておくことで、そもそも引数としてsize()
を持っていないものをいれられなくなります。
では、このhas_size
ってどうやって作ればいいの?というのが本稿のテーマです。
書いてみる
さて、今回はあるクラスT
がメンバ関数size()
を持つかどうかをチェックするhas_size
、またネスト型size_type
を持つかどうかをチェックするhas_size_type
を作ります。
C++03
昔懐かしいC++03です。昔懐かしいと言っても、VisualStudio2008が現役な組織も普通にあると思いますので、亡き者とするにはまだまだ早い感じがしますね。
ただ、C++03では、どうしてもメンバ関数のdetectionコードの書き方が思いつきませんでした…(てか書けるのかな?)。
#include <iostream>
#include <vector>
template <typename T>
struct has_size_type {
private:
typedef short ok;
typedef long ng;
template <typename U>
static ok test(typename U::size_type);
template <typename U>
static ng test(...);
public:
static const bool value = sizeof(test<T>(0)) == sizeof(ok);
};
int main() {
std::cout
<< std::boolalpha
<< has_size_type<std::vector<int> >::value << std::endl // true
<< has_size_type<int>::value << std::endl; // false
return 0;
}
typedef
やテンプレートパラメータの綴じ括弧を連続させないなど、非常に味わい深いコードですね。ポイントは以下の2点です。
-
U::size_type
がなかった場合でもSFINAEによりエラーにはならず、次のオーバーロード候補が探され、引数が...
は最も優先順位が低い -
sizeof
は実際には関数を呼び出さず、結果のサイズのみを返してくれますので、OK時とNG時で別の大きさになるようなデータ型を指定している
C++11/C++14
さて次はC++11/C++14です。decltype
爆誕のおかげで、意味不明のsizeof
とかすることなく、かなりスッキリと書けるようになりました。
#include <iostream>
#include <type_traits>
#include <utility>
#include <vector>
template <typename T>
struct has_size_type {
private:
template <typename U>
static auto test(int) -> decltype(std::declval<typename U::size_type>(), std::true_type());
template <typename U>
static auto test(...) -> decltype(std::false_type());
public:
using type = decltype(test<T>(0));
static constexpr bool value = type::value;
};
template <typename T>
struct has_size {
private:
template <typename U>
static auto test(int) -> decltype(std::declval<U>().size(), std::true_type());
template <typename U>
static auto test(...) -> decltype(std::false_type());
public:
using type = decltype(test<T>(0));
static constexpr bool value = type::value;
};
int main() {
std::cout
<< std::boolalpha
<< has_size_type<std::vector<int>>::value << std::endl // true
<< has_size_type<int>::value << std::endl // false
<< has_size<std::vector<int>>::value << std::endl // true
<< has_size<int>::value << std::endl; // false
return 0;
}
オーバーロードの優先順位を使っている部分はC++03のときと同じですが、using
やstd::true_type
などが使えるのはとてもありがたいですね。ポイントは以下の2点です。
-
decltype
は式を受け取り、コンマオペレーターで繋がれたものは式として扱われる。前から評価されていき、一番末尾にある型が式の型となる。 -
declval
を使うことで参照を作れるので、コンストラクタが呼び出せなくてもOK
デフォルトテンプレートパラメータを使ったか書き方や、継承を用いるものなどもメジャーなので、もし興味があれば調べてみましょう。
C++17
最後にC++17です。void_t
という、いつだってvoidを返してくれるメタ関数が誕生し、世の中のmember detectionは様変わりしました。
#include <iostream>
#include <type_traits>
#include <vector>
#include <experimental/type_traits>
template <typename T, typename =void>
struct has_size_type : std::false_type {};
template <typename T>
struct has_size_type<T, std::void_t<typename T::size_type>> : std::true_type {};
template <typename T>
using size_func = decltype(std::declval<T>().size());
template <typename T>
using has_size = std::experimental::is_detected<size_func, T>;
int main() {
std::cout
<< std::boolalpha
<< has_size_type<std::vector<int>>::value << std::endl // true
<< has_size_type<int>::value << std::endl // false
<< has_size<std::vector<int>>::value << std::endl // true
<< has_size<int>::value << std::endl; // false
return 0;
}
どん!どうですか、このわかりやすさは。
このなんてことないメタ関数void_t
のおかげで、正直これ以外で見たことも聞いたこともない(...)
などという謎表記から開放され、おなじみのテンプレートの特殊化でmember detectionができるようになりました。ポイントは以下の2点です。
-
T::size_type
があった場合、特殊化されているほうが優先される -
is_detected
なるものがexperimentalではあるもののすでに用意されてる
ありがとうC++。もう黒魔術とは言わせない。
おわりに
さて、SFINAEといえばmember detectionといわれるほどメジャーな技法ですが、C++17の領域まで来ると、もはや技法とも言えないレベルの簡素さですね。
しかしexperimental/type_traits
は、所詮experimentalですし、なぜかVisual Studioには実装されていなかったりもするので、まだまだ暫くはC++11/14の書き方で戦うのかなぁという印象です。ただ、is_detected
自体は難しい実装でもないので自前で書いてる人も多そうです。
そーす
おまけ
ナウなキッズ「trait boundで、簡単に書けるよ。そうRustならね。」