はじめに
Boost.Hana は、Boost の新しくて速いメタプログラミングライブラリです。また、公式リファレンスでの説明は以下の通りです。
Hanaは、型と値の両方の計算に適したC++メタプログラミング用のヘッダーオンリーライブラリです。 それが提供する機能は、十分に確立されたBoost.MPLおよびBoost.Fusionライブラリによって提供されるもののスーパーセットです。 C ++ 11/14の実装手法とイディオムを活用することで、Hanaは以前のメタプログラミングライブラリと同等以上のコンパイル時間とランタイムパフォーマンスを誇り、プロセスの表現力を大幅に向上させます。 Hanaはアドホックな方法で簡単に拡張でき、Boost.Fusion、Boost.MPL、および標準ライブラリとすぐに使用できる相互運用を提供します。(主に google 翻訳です)
この記事では、この Boost.Hana の一部の機能をC++Now 2017: Louis Dionne "Fun with Boost.Hana" - YouTubeや公式リファレンスから紹介したいと思っています。ですが、Hana にはここで紹介する他にもたくさんの機能があります! また、Hana の分かりやすいチュートリアルは、公式リファレンス(google 翻訳)にあります。
(Boost の導入方法については、Boostライブラリのビルド方法 - boostjpに詳しく書かれています。
また、サンプルプログラムのコンパイルの際は、C++14 の機能を有効にしてください。(参考ページ))
間違い等ありましたら、コメントや編集リクエストをくださるとありがたいです...!
Hana と 他のライブラリの比較
コンパイル時に型の並びを操作する
まず、Boost.MPL での処理は次のようになるようです。
#include <type_traits>
#include <boost/mpl/vector.hpp>
#include <boost/mpl/remove_if.hpp>
#include <boost/mpl/transform.hpp>
#include <boost/mpl/equal.hpp>
namespace mpl = boost::mpl;
// 型のシーケンス
using Types = mpl::vector<int, void, char, void, double>;
// void 型を取り除く
using NoVoid = mpl::remove_if<Types, std::is_void<mpl::_1>>::type;
// -> mpl::vector<int, char, double>
static_assert(boost::mpl::equal<NoVoid, mpl::vector<int, char, double>>::value);
// ポインタに変換
using Ptrs = mpl::transform<Types, std::add_pointer<mpl::_1>>::type;
// -> mpl::vector<int*, void*, char*, void*, double*>
static_assert(boost::mpl::equal<Ptrs, mpl::vector<int*, void*, char*, void*, double*>>::value);
int main() {}
型の並びがmpl::vector
型で表現され、それと述語のメタ関数を引数にして、型の並びを操作しています。すべてが型で表現されており、メタプログラミング! という印象です。(このスタイルは、Hana のリファレンスのどこかで "Classic MPL-style" と呼んでいる箇所がありました)
続いて、Boost.Hana
で同様の処理をすると、次のようになるようです。
#include <boost/hana/tuple.hpp>
#include <boost/hana/traits.hpp>
#include <boost/hana/remove_if.hpp>
#include <boost/hana/equal.hpp>
namespace hana = boost::hana;
// 型のシーケンス
auto Types = hana::tuple_t<int, void, char, void, double>;
// void 型を取り除く
auto NoVoid = hana::remove_if(Types, [](auto t) {
return hana::traits::is_void(t);
}); // -> hana::tuple_t<int, char, double>
static_assert(NoVoid == hana::tuple_t<int, char, double>);
// ポインタに変換
auto Ptrs = hana::transform(Types, [](auto t) {
return hana::traits::add_pointer(t);
}); // -> hana::tuple_t<int*, void*, char*, void*, double*>
static_assert(Ptrs == hana::tuple_t<int*, void*, char*, void*, double*>);
int main() {}
hana::tuple_t<T...>
は型のように見えますが、実際はタプルのオブジェクトのようです。 このように、Boost.Hana
では、すべてがオブジェクトとして表現されているようです。(おそらく、このあたりがクラシックではないのだと思われます...)また、基本的に型の並びはhana::tuple
で表現されているらしいです。そして、ラムダ式を使用できるのがとても便利らしいです。(キャプチャなどで副作用をもたらしたりできるらしいです...)
要素の型によって並びを操作する(実行時)
初めに、Boost.Fusion での処理は次のようになるようです。
#include <string>
#include <type_traits>
#include <cassert>
#include <boost/fusion/algorithm/transformation/remove_if.hpp>
#include <boost/fusion/include/make_vector.hpp>
#include <boost/fusion/include/equal_to.hpp>
#include <boost/mpl/placeholders.hpp>
using namespace std::string_literals;
namespace fusion = boost::fusion;
namespace mpl = boost::mpl;
// 実行時です
int main()
{
// 様々な型の要素
auto vector = fusion::make_vector(1, 2.3f, "Hello"s, 3.4, 'x');
// float 型の要素を取り除く
auto no_floats = fusion::remove_if<
std::is_floating_point<mpl::_>>(vector);
assert(no_floats == fusion::make_vector(1, "Hello"s, 'x'));
}
これを、Boost.Hana を使って書くと、次のようになるようです。
#include <string>
#include <cassert>
#include <boost/hana/tuple.hpp>
#include <boost/hana/remove_if.hpp>
#include <boost/hana/traits.hpp>
#include <boost/hana/equal.hpp>
namespace hana = boost::hana;
using namespace std::string_literals;
// 実行時です
int main()
{
// 様々な型の要素
auto tuple = hana::make_tuple(1, 2.3f, "Hello"s, 3.4, 'x');
// float 型の要素を取り除く
auto no_floats = hana::remove_if(tuple, [](auto const& t) {
return hana::traits::is_floating_point(hana::typeid_(t));
});
assert(no_floats == hana::make_tuple(1, "Hello"s, 'x'));
}
アルゴリズム関数の述語のラムダ式の中で、ラムダ式の引数を、hana::typeid_()
関数を適用してからhana::traits
の関数へ渡しているのがポイントでしょうか。この例は、コンパイル時のものではないですが、型で処理をしているという点では、メタプログラミングと言えそうです。(hana::tuple
の要素がすべてコンパイル時に構築できる型のものであれば、コンパイル時に処理できます)また、boost/hana/traits.hpp
では、標準の<type_traits>
と同等の機能が用意されているらしいです。
Hana で 自作の traits を使う
上記のコードに出てきた、hana::is_floating_point()
のようなものを自作するためには、以下のようにすれば良さそうです。
#include <string>
#include <vector>
#include <type_traits>
#include <boost/hana/tuple.hpp>
#include <boost/hana/filter.hpp>
#include <boost/hana/traits.hpp>
#include <boost/hana/equal.hpp>
namespace hana = boost::hana;
using namespace std::string_literals;
// 自作メタ関数 イテレータを持つ型かどうかを調べる
template <class T, class = void>
struct has_iterator : std::false_type {};
template <class T>
struct has_iterator<T, std::void_t<typename T::iterator>> : std::true_type {};
// hana で 使える自作の traits
constexpr auto has_iterator_for_hana = hana::trait<has_iterator>;
// 型のシーケンス
auto Types = hana::tuple_t<int, std::string, char, std::vector<int>, double>;
// イテレータを持つ型のみを残す
auto HasItr = hana::filter(Types, [](auto t) {
// 使用する例
return has_iterator_for_hana(t);
}); // -> hana::tuple_t<std::string, std::vector<int>>
static_assert(HasItr == hana::tuple_t<std::string, std::vector<int>>);
int main() {}
型の性質を調べて、std::false
またはstd::true
を継承するようなメタ関数は、hana::trait<T>
を使うことで、Hana で使用できるようです。また、型に修飾を施すようなメタ関数は、代わりにboost/hana/type.hpp
にあるhana::metafunction<T>
を使うと良いようです。
constexpr auto remove_cv = hana::metafunction<std::remove_cv>;
hana::filter()
, hana::remove_if()
などの関数は、Hana
に用意されているその他の関数を組み合わせることで、様々なアルゴリズムが実現可能だと思われます。詳しくは公式リファレンスを参照してください...!
hana::tuple
から型の情報を取り出す
hana::tuple
から、型を取り出そうと次のようなコードを書いたところ、以下のようになりました。
#include <boost/hana/tuple.hpp>
#include <iostream>
#include <utility>
#include <boost/type_index.hpp>
namespace hana = boost::hana;
// T... に型の情報が入っています
template <typename ... T>
void tuple_to_type(hana::tuple<T...>)
{
// 型を表示する例
std::initializer_list<int> swallow =
{ (void( std::cout << boost::typeindex::type_id_with_cvr<T>().pretty_name() << "\n"), 0)... };
}
int main()
{
// 型のシーケンス
auto Types = hana::tuple_t<int, double, char, void>;
// 型の情報を取り出す
tuple_to_type(Types);
}
出力
boost::hana::type_impl<int>::_
boost::hana::type_impl<double>::_
boost::hana::type_impl<char>::_
boost::hana::type_impl<void>::_
テンプレート関数の引数にtuple
を渡すことで、tuple
のテンプレート引数の型を取得しようとしています。boost::typeindex::type_id_with_cvr<T>().pretty_name()
で、それぞれの型名を取得しています。パラメーターパックT...
を展開する際は、Swallow
という手法を用いています。(参考リンク C++のパラメータパック基礎&パック展開テクニック - Qiita)実際に実行すると、boost::hana::type_impl<T>::_
が得られました。struct hana::type_impl<T>::_
の定義を見てみたところ、hana::type_impl<T>::_
のテンプレート引数T
は、型エイリアスtype
で取得できるようでした。そこで、次のように書き替えたところ、うまく取得できるようでした。
#include <boost/hana/tuple.hpp>
#include <iostream>
#include <utility>
#include <boost/type_index.hpp>
namespace hana = boost::hana;
// T... に型情報が入っています。 T::type で、型を取得できるようです
template <typename ... T>
void print_tuple_elem_type(hana::tuple<T...>)
{
// 型を表示する例
std::initializer_list<int> swallow ={ (void( // ↓ここを、T から typename T::type に変更しました
std::cout << boost::typeindex::type_id_with_cvr<typename T::type>().pretty_name() << "\n"
), 0)...
};
}
int main()
{
// 型のシーケンス
auto Types = hana::tuple_t<int, double, char, int, void>;
// 型の情報を取り出す
print_tuple_elem_type(Types);
}
出力
int
double
char
int
void
パラメーターパックの型と要素を取得する
パラメーターパックのそれぞれの型と要素を取得するためには、hana::detail::type_at<n, T>::type
、hana::arg<n>()
を用いれば良さそうです。
#include <iostream>
#include <utility>
#include <string>
#include <boost/type_index.hpp>
#include <boost/hana/tuple.hpp>
#include <boost/hana/detail/type_at.hpp>
#include <boost/hana/functional/arg.hpp>
namespace hana = boost::hana;
using namespace std::string_literals;
// パラメーターパック n 番目の要素の型、値を取得します
template <std::size_t n, typename ... T>
void print_param_pac_elem(T ... args)
{
std::cout
<< "型 : "
<< boost::typeindex::type_id_with_cvr<
typename hana::detail::type_at<n - 1, T ...>::type>().pretty_name() << "\n"
<< "値 : "
<< hana::arg<n>(args...);
}
int main()
{
print_param_pac_elem<1>(1, "std::string"s, 'x', 3.14);
// ↑1番目 ↑3番目
// ↑2番目 ↑4番目
}
出力
型 : int
値 : 1
hana::detail::type_at<n, T>::type
、の n は 0 から始まっていますが、hana::arg<n>()
の n は 1 から始まっていることに注意が必要そうです。また、このようにパラメーターパックを展開しなくても、hana::tuple
に要素を固めてhana
に用意されている関数を使えば、様々な操作はできそうです。
型の並びから、条件を満たす型の数を調べる
最後に、方の並びから条件を満たす型の数を調べる例を次に示します。
#include <boost/hana/assert.hpp>
#include <boost/hana/equal.hpp>
#include <boost/hana/ext/std/integral_constant.hpp> // なぜか必要のようです
#include <boost/hana/fold_left.hpp>
#include <boost/hana/if.hpp>
#include <boost/hana/integral_constant.hpp>
#include <boost/hana/plus.hpp>
#include <boost/hana/type.hpp>
#include <boost/hana/tuple.hpp>
#include <type_traits>
namespace hana = boost::hana;
auto types = hana::tuple_t<long, float, short, float, long, long double>;
auto number_of_floats = hana::fold_left(types, hana::int_c<0>, [](auto count, auto t) {
// もし、引数 t が浮動少数点数なら、引数 count に 1 を足したものを返します
return hana::if_(hana::trait<std::is_floating_point>(t),
count + hana::int_c<1>,
count
);
});
BOOST_HANA_CONSTANT_CHECK(number_of_floats == hana::int_c<3>);
int main() { }
ここで、hana::int_c<n>
は、型の世界で整数を表すようなものだと思われます。integral_constant - cpprefjp C++日本語リファレンス と同じ機能を持ち、さらに機能が拡張されたものらしいです。また、hana::fold_left
は、二項演算を使用した左結合の畳み込み(?)のようです。x1,...,xn, 関数 f およびオプションの初期状態を含む構造体がある場合、fold_left は次のように f を適用するとのことです。
f(... f(f(f(x1, x2), x3), x4) ..., xn) // 初期状態なし
f(... f(f(f(f(state, x1), x2), x3), x4) ..., xn) // 初期状態(state)あり
(C++17 から追加された畳み込み式 - cpprefjp C++日本語リファレンスの機能と同等のもののようで、こちらの説明のほうが分かりやすいかも知れません。)
この場合、上の説明の 関数 f は、述語のラムダ式
[](auto count, auto t) {
return hana::if_(hana::trait<std::is_floating_point>(t),
count + hana::int_c<1>,
count
);
}
で、初期状態が hana::int_c<0>
、x1,...,xn はそれぞれlong, float, short, float, long, long double
といった雰囲気です。
実際の動きとしては、まず0, long
を引数にとり、long
は浮動少数点数ではないので、0
を返します。
次に、さきほど返された0
とfloat
を引数にとり、float
は浮動少数点数なので、0 + 1
を返します。
そして、今返された1
とshort
を引数にとり、short
は浮動少数点数ではないので、1
を返す...というように繰り返されて、最終的に浮動少数の数を返す、というようになっているようです。
まとめ
Boost.Hana は型がtuple
などのオブジェクトとして表されるため、ラムダ式を用いたり、実行時のプログラムと同じようなスタイルでメタプログラミングができたりするようです。Boost.MPL のような、クラシック(?)な普通のメタプログラミングのスタイルも良いですが、この Boost.Hana の記述方法も面白そうですね...! また、ここで紹介したようなものは Boost.Hana の機能のごく一部なので、実際に使ってみる際は公式リファレンスを参照してみてください...!