Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

constexpr idioms

2,726 views

Published on

ドワンゴC++勉強会 #1
constexpr idioms

Published in: Software
  • Be the first to comment

constexpr idioms

  1. 1. constexpr idioms ドワンゴC++勉強会 #1 @fimbul11
  2. 2. INTRODUCTION • 計算量 (処理 / コンパイル時間の短縮) • 再帰深度 (扱える要素数に影響) constexpr /TMPにおいて以下の2点に優れた実装を考える
  3. 3. 計算量の抑制の重要性は明白 INTRODUCTION
  4. 4. INTRODUCTION 再帰深度 (扱える要素数に影響)とは たとえば単純に総和を再帰で書くとこんな感じ(計算精度は考慮しない) ! template <typename T, typename Type, typename... Types> constexpr T sum_impl(T&& result, Type&& arg, Types&&... args) { return sum_impl<T>(result + arg, forward<Types>(args)...); }
  5. 5. INTRODUCTION 再帰深度 (扱える要素数に影響)とは 線形再帰による実装だと再帰深度がO(N) 扱う要素数が増えたらどうなる? ! sum<int>(1,2,3,4,5,…,10000); // 1万までの和を求める 
 ↓ ! fatal error: recursive template instantiation exceeded maximum depth of 256
  6. 6. INTRODUCTION メタ関数やコンテナも同様の問題を抱えている ! // 典型的なtuple_elementのrecursive case実装例 線形再帰 template<size_t I, typename Head, typename... Tail> struct tuple_element<I, tuple<Head, Tail...>> : tuple_element<I-1, tuple<Tail...>> {}; ! 計算量・再帰深度は共にO(N)、Iが大きくなるとエラー
  7. 7. 再帰深度の抑制は重要 INTRODUCTION
  8. 8. 計算量・再帰深度の双方を考慮した コンパイル時処理に有用な技法を考える INTRODUCTION
  9. 9. パラメータパック展開を用いた効率的な要素の走査 INDEXTUPLE
  10. 10. 例 : N要素の配列の各要素に関数を適用した配列を作りたい make_array(f(arr[0]), f(arr[1]), ..., f(arr[N-1])); INDEXTUPLE
  11. 11. 整数列を保持する型を用意する template<size_t... Indices> struct index_tuple {}; INDEXTUPLE
  12. 12. メタプログラミングで整数列を作る 0, Nを受け取り0からN-1までの整数列を生成するindex_range // 以下の2つの型は同じ ! index_tuple<0, 1, 2,..., N-1> ! typename index_range<0, N>::type INDEXTUPLE
  13. 13. テンプレートパラメータパックの展開を利用して処理 template <typename T, size_t... Indices> constexpr auto apply_f(const T& arr, index_tuple<Indices...>&&) { return make_array(f(arr[Indices])...); } ! apply_f(arr, typename index_range<0, N>::type{}); INDEXTUPLE
  14. 14. index_rangeの実装例 計算量・再帰深度はO(N) (first >= lastとなるまで値をStepだけ増やして再帰する実装) template< size_t First, size_t Last, size_t Step, size_t... Indices > struct index_range<First, Last, Step, index_tuple<Indices...>, false> : index_range<First + Step, Last, Step, index_tuple<Indices..., First>> {}; INDEXTUPLE
  15. 15. 既に生成した数列を利用し倍々に数列を生成可能 例えばIndicesが[0,1,2,3]であれば以下の2つの型は等しくなる ! index_tuple<Indices..., (Indices + sizeof...(Indices))...> index_tuple<0,1,2,3,4,5,6,7> ! この原理で実装するとindex_rangeの計算量・再帰深度はO(logN) INDEXTUPLE 参考 : index_tuple イディオムにおける index_range の効率的な実装 http://boleros.hateblo.jp/entry/20120406/1333682532
  16. 16. まとめ INDEXTUPLE • パック展開を利用しコンテナ走査の再帰深度を抑制 • 肝となるindex_rangeは計算量・再帰深度O(logN) • 対象がインデックスアクセス可能な必要あり • C++14から同様の機能が標準入り(std::integer_sequence)
  17. 17. パラメータパック分割 テンプレートパラメータパックTYPES1をTYPES2とRESTに分割
  18. 18. パラメータパック分割 Types1の分割の前半となる部分列Types2が作れる場合に分割可能
  19. 19. パラメータパック分割 (実用性は無視した) 例 :Types1をTypes2とRestに分割 template<typename... Types2> // int,int,int struct f { // Restはtemplate argument deductionで取れる template <typename... Rest> // int,int,int,int void operator()(Types2..., Rest...) {} }; ! template <typename... Types1> // int,int,int,int,int,int,int void call_f() { f<int,int,int>()(Types1{}...); } ! call_f<int,int,int,int,int,int,int>(); Types1[int,int,int,int,int,int,int]Types2[int,int,int] Rest[int,int,int,int]
  20. 20. パラメータパック分割 実用的な例 : tuple<Types…>におけるTypes…のN番目の型を取り出したい VoidsをN-1個の[void, void, …, void]とし、任意のポインタ型がvoid*で受取可能な為 template <typename... Voids> struct tuple_element_impl { template <typename T, typename... Rest> static T eval(Voids*..., T*, Rest...); // never defined }; ! typedef typename decltype( tuple_element_impl<Voids>::eval( static_cast<identity<Types>*>(nullptr)...))::type type; identity<Types>*…をN-1番目までvoid*,void*,…,void*、N番目をT*, 残りをRest…で 処理する、typenameT::type (即ちtypename identity<N番目の型>::type)は欲しかった型 参考 : 対数オーダーでTemplate Parameter Packから要素を取り出す http://fimbul.hateblo.jp/entry/2013/08/28/203833
  21. 21. パラメータパック分割 N-1個のvoidからなるパックはindex tupleを利用して簡単に作れる template <size_t N, typename T = void> struct dummy_index { typedef T type; }; ! typename dummy_index<Indices>::type... // void, void, ..., void index_rangeの計算量・再帰深度がO(logN)なのでtuple_elementもO(logN)で実装可能
  22. 22. まとめ • 分割の前半となる部分列が作れる時、パタメータパックを 分割出来る • N個のT型からなるパラメータパックはindex_tupleを利用し て生成出来る パラメータパック分割
  23. 23. 分割統治法 再帰の形が完全二分木型になれば深度は対数オーダーになる
  24. 24. 分割統治法 1. 対象がイテレータの場合
  25. 25. 分割統治法 // sproutのcountの実装を見てみる return size == 1 ? (*first == value ? 1 : 0) // 要素数が1になった場合 : sprout::detail::count_impl_ra( // 左側を処理する first, value, size / 2 ) + sprout::detail::count_impl_ra( // 右側を処理する sprout::next(first, size / 2), value, size - size / 2 ); 対象がイテレータの場合は比較的分かりやすい Sproutのalgorithmの多くはイテレータを用いた分割統治的な実装 Copyright (C) 2014 Bolero MURAKAMI
  26. 26. 分割統治法 2. 対象がN個の引数の場合
  27. 27. 分割統治法 N個の数の総和関数sumを再び考える
  28. 28. 分割統治法 template <typename T, typename... Types> constexpr T sum(const Types&... args) { return detail::sum_impl< std::make_index_sequence<sizeof...(Types) / 2> >::template eval<T>(args...); } ! sum<int>(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); // 55 パック分割を利用してN個の引数を半々にすればよい(計算精度は考慮しない) sizeof...(Types) / 2 要素の数列を作ってsum_implに渡す
  29. 29. 分割統治法 template <std::size_t... Indices> struct sum_impl<std::index_sequence<Indices...>> { template <typename T, typename... Types> static constexpr T eval( // パック分割による引数の受取 const typename dummy_index<Indices, T>::type&... args1, // T, T, ..., T const Types&... args2) { return sum_impl< // 引数列の左半分を更に半分ずつに分けるように再帰 std::make_index_sequence<sizeof...(Indices) / 2> >::template eval<T>(args1...) + // 結果を足し合わせる(統治) sum_impl< // 引数列の右半分を更に半分ずつに分けるように再帰 std::make_index_sequence<sizeof...(Types) / 2> >::template eval<T>(args2...); } }; パラメータパック分割を用いて引数列を分割しながら再帰
  30. 30. 分割統治法 template <> struct sum_impl<std::index_sequence<>> { template <typename T> static constexpr T eval(const T& arg) { return arg; } }; 分割を繰り返し要素数が1になった段階(最も再帰が深い段階) ここから統治段階に移り、和を計算する 再帰深度はO(logN)
  31. 31. 分割統治法 template <typename... Types> struct overloaded_function : detail::overloaded_function_impl< typename index_range<0, sizeof...(Types) / 2>::type, typename index_range<sizeof...(Types) / 2, sizeof...(Types)>::type, type_tuple<Types...>> { // 略 同様の技法をクラスに適用し完全二分木型に多重継承したり可能 参考 : overloaded_functionの再帰深度を抑えた(このような継承の仕方が役に立つケース) http://fimbul.hateblo.jp/entry/2014/06/07/033509
  32. 32. まとめ • 再帰深度の改善が見込める • 対象がイテレータなら比較的簡単、パックの分割を利用し ても実装可(最初に引数をコンテナに入れてしまうのも手) • 継承の形のコントロールも可能 分割統治法
  33. 33. 型のMAP 型(非型テンプレートパラメータ)をキーとして 型(非型テンプレートパラメータ)を得る
  34. 34. 型のMAP 例 : mpl::vectorのようなものを作ってみる
  35. 35. 型のMAP Typesと同じ長さの数列をimplに渡す(0~N-1までの一意なインデックス生成) template <typename... Types> struct type_vector : type_vector_impl<typename index_range<0, sizeof...(Types)>::type, identity<Types>...> {};
  36. 36. 型のMAP パラメータパックを展開しながら継承、高さ1のn分木形に多重継承が実現 template <size_t... Indices, typename... Types> struct type_vector_impl<index_tuple<Indices...>, Types...> : indexed_type<Types, Indices>... 型とインデックスをindexed_type<Types, Indices>のペアとして継承
  37. 37. 型のMAP あるDerivedという型が一意なインデックス0,1,…,N-1を持つ ! Base<T1, 0>, Base<T2, 1>, ..., Base<Tn, N-1> ! を多重継承しているときDerived型オブジェクトの暗黙のアップキャストを試みる 要素アクセスの実装方針
  38. 38. 型のMAP インデックスkさえ与えればインデックスの一意性から継承元Base<T, k>を一意に 特定出来るので型Tの部分は推論させることが出来る 要素アクセスの実装方針
  39. 39. 型のMAP template <size_t... Indices, typename... Types> struct type_vector_impl<index_tuple<Indices...>, Types...> : indexed_type<Types, Indices>... { // N個のindexed_typeを多重継承 template <size_t N, typename T> static T get_impl(indexed_type<T, N>); // Nが与えられれば、推論でTは取れる template <size_t N> static auto get() -> decltype(get_impl<N>( declval<type_vector_impl<index_tuple<Indices...>, Types...>>())); ! template <size_t N> using at = typename decltype(get<N>())::type; }; type_vector<int, char, double>::at<2>; // double 要素アクセスの実装
  40. 40. パフォーマンス 型のMAP この実装ではコンテナの構築時にindex_rangeをただ1度だけ呼ぶ、それ 以降は、template argument deductionで任意のインデックスの型が取れ る、要素アクセスは高速で再帰深度も抑えられている
  41. 41. 同様にしてtupleも実装出来る (*thisとインデックスNのペアから値を保持している継承元へアップキャスト) 型のMAP template <size_t N, typename T> constexpr const typename T::type& get_impl(const value_holder<T, N>& value) const noexcept { return value(); } ! template <size_t N> constexpr const value_type<N>& get() const noexcept { return get_impl<N>(*this); } 参考 : 再帰深度を抑えたtuple的コンテナの構築 http://fimbul.hateblo.jp/entry/2014/05/25/014112
  42. 42. まとめ • 多重継承とアップキャストを利用して型(或いは非型テンプ レートパラメータ)でmapのようなものが実現出来る • Key側は一意な必要がある(例は一意なインデックスを用いた) • 要素アクセスが速く、再帰深度も抑えられるので非常に有用 型のMAP
  43. 43. その他の技法 • 二分探索法 再帰で簡単に実装可能、計算量・再帰深度改善が見込める • 倍分再帰 対象範囲を倍々にしながら再帰的に処理を適用する技法 Sproutで再帰深度を抑えたC++11 constexpr対応distanceの実 装等に使用、解説はあるので以下を参考にすると良い 参考: constexpr アルゴリズムの実装イディオム その1 http://boleros.hateblo.jp/entry/20130221/1361453624

×