constexpr を使うべき5の理由 - その4
「constexpr を使うべき5の理由」ようやく4つめです。
今回は、いわゆるメタプログラミングにおける constexpr について取り上げます。
4.あのうんざりするテンプレートメタプログラミングによる数値計算からはもはや解放された
以下のコードは、[1 .. 5] のリストの各要素を加算するアキュムレートを、古式ゆかしい TMP で書いたものである。
#include <type_traits> #include <boost/mpl/apply.hpp> #include <boost/mpl/next.hpp> #include <boost/mpl/deref.hpp> #include <boost/mpl/vector_c.hpp> #include <boost/mpl/begin_end.hpp> #include <boost/mpl/int.hpp> #include <boost/mpl/plus.hpp> #include <boost/mpl/lambda.hpp> template<typename First, typename Last, typename Init, typename BinaryOp, typename = void> struct meta_accumulate_impl { public: typedef typename meta_accumulate_impl< typename boost::mpl::next<First>::type, Last, typename boost::mpl::apply<BinaryOp, Init, typename boost::mpl::deref<First>::type>::type, BinaryOp >::type type; }; template<typename First, typename Last, typename Init, typename BinaryOp> struct meta_accumulate_impl< First, Last, Init, BinaryOp, typename std::enable_if<std::is_same<First, Last>::value>::type > { public: typedef Init type; }; template<typename First, typename Last, typename Init, typename BinaryOp> struct meta_accumulate : public meta_accumulate_impl<First, Last, Init, BinaryOp> {}; typedef boost::mpl::vector_c<int, 1, 2, 3, 4, 5> src; typedef typename meta_accumulate< typename boost::mpl::begin<src>::type, typename boost::mpl::end<src>::type, boost::mpl::int_<0>, boost::mpl::plus<boost::mpl::_1, boost::mpl::_2> >::type accumulated; static_assert(accumulated::value == 15, "");
テンプレートメタプログラミングに慣れたあなたであれば、この程度のコードを読むのはけして苦ではないだろう。
しかしながら、単に数値計算として見た場合、上記のコードはロジックを素直に表現していると言えるだろうか?
そして何より記述量の多さにうんざりせざるをえない。
では、同じ目的のコードを constexpr で書いてみる。
#include <sprout/functional.hpp> template<typename InputIterator, typename T, typename BinaryOperation> inline constexpr T accumulate(InputIterator first, InputIterator last, T init, BinaryOperation binary_op) { return first == last ? init : accumulate(first + 1, last, binary_op(init, *first), binary_op) ; } constexpr int src[] = {1, 2, 3, 4, 5}; constexpr int accumulated = accumulate( src, src + 5, 0, sprout::plus<int>() ); static_assert(accumulated == 15, "");
どうだろうか。
非メタプログラマでも普段見慣れているコードとあまり変わらないのではないかと思う。
これは、条件分岐に if でなく条件演算子を、ループの代わりに再帰を用いている点を除けば、std::accumulate の典型的な実装のロジックと同様である。
数値計算のためのコードとしては、TMP と constexpr では後者のほうが読みやすく書きやすいのが見てとれるだろう。
さらに、constexpr であれば浮動小数点型やユーザ定義型(リテラル型)も扱うことが可能だ。
constexpr double src[] = {0.1, 0.2, 0.3, 0.4, 0.5}; constexpr double accumulated = accumulate( src, src + 5, 0, sprout::plus<double>() ); static_assert(accumulated == 1.5, "");
(>> id:xxxxxeeeee)さん ×plus
これは TMP では書けないコードである。
そして、この constexpr accumulate は、コンパイル時でも実行時でも同じように呼び出すことができる。
だから、同じ目的の計算のためのコードを、実行時のための関数とコンパイル時のためのクラステンプレートとで二重に実装する必要はもはやない。
ただ一つの constexpr 関数で事足りるのだから。
例えば Boost.Math では公約数/公倍数を求めるものとして、ランタイム用の関数とコンパイルタイム用のクラステンプレートの二つを用意している。
boost::math::gcd/lcm (関数)
boost::math::static_gcd/static_lcm (クラステンプレート)
Sprout.Math では、たった一つの constexpr 関数を用意している。
sprout::math::gcd sprout::math::lcm
sprout::math::gcd/lcm のように constexpr である程度複雑な処理を行うコードは、もし初見ならば少しばかり奇妙なコードに思えるかもしれない。
しかしこれはライブラリ化されたものであるから、実装を知らなくても、ユーザコードに使用するぶんには問題ない。
#include <sprout/math/common_factor.hpp> constexpr int result = sprout::math::gcd(12707, 12319); static_assert(result == 97, "");
通常の関数と同じように呼び出せばよいだけだ。
Sprout C++ Library では、これだけでなく三角関数をはじめとする各種数学関数、疑似乱数、レイトレーシングなどを constexpr で実装しており、
実装もわりと簡単なので、constexpr のポテンシャルを表しているといえるでしょう。
もちろん、これによって TMP そのものが不要になるわけではない。
型に対する操作については TMP でしかできないのだから。
つまり、型に対するメタプログラミングは TMP、値(オブジェクト)に対するメタプログラミングは constexpr と、分担を明確にできるのが意義である。
そしてそれらはコンパイル時に処理されるもの同士相互に連携することができる。
constexpr によって C++11 におけるメタプログラミング環境はより豊かになったといえるだろう。