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

constexpr idioms

  • Upload
    fimbul

  • View
    1.705

  • Download
    2

Embed Size (px)

DESCRIPTION

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

Citation preview

Page 1: constexpr idioms

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

@fimbul11

Page 2: constexpr idioms

INTRODUCTION

• 計算量 (処理 / コンパイル時間の短縮)

• 再帰深度 (扱える要素数に影響)

constexpr / TMPにおいて以下の2点に優れた実装を考える

Page 3: constexpr idioms

計算量の抑制の重要性は明白

INTRODUCTION

Page 4: constexpr idioms

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)...); }

Page 5: constexpr idioms

INTRODUCTION再帰深度 (扱える要素数に影響)とは

線形再帰による実装だと再帰深度がO(N)

扱う要素数が増えたらどうなる? !sum<int>(1,2,3,4,5,…,10000); // 1万までの和を求める ↓ !fatal error: recursive template instantiation exceeded maximum depth of 256

Page 6: constexpr idioms

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が大きくなるとエラー

Page 7: constexpr idioms

再帰深度の抑制は重要

INTRODUCTION

Page 8: constexpr idioms

計算量・再帰深度の双方を考慮した コンパイル時処理に有用な技法を考える

INTRODUCTION

Page 9: constexpr idioms

パラメータパック展開を用いた効率的な要素の走査

INDEX TUPLE

Page 10: constexpr idioms

例 : N要素の配列の各要素に関数を適用した配列を作りたい

make_array(f(arr[0]), f(arr[1]), ..., f(arr[N-1]));

INDEX TUPLE

Page 11: constexpr idioms

整数列を保持する型を用意する

template<size_t... Indices> struct index_tuple {};

INDEX TUPLE

Page 12: constexpr idioms

メタプログラミングで整数列を作る 0, Nを受け取り0からN-1までの整数列を生成するindex_range

// 以下の2つの型は同じ !index_tuple<0, 1, 2,..., N-1> !typename index_range<0, N>::type

INDEX TUPLE

Page 13: constexpr idioms

テンプレートパラメータパックの展開を利用して処理

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{});

INDEX TUPLE

Page 14: constexpr idioms

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>> {};

INDEX TUPLE

Page 15: constexpr idioms

既に生成した数列を利用し倍々に数列を生成可能

例えば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)

INDEX TUPLE

参考 : index_tuple イディオムにおける index_range の効率的な実装 http://boleros.hateblo.jp/entry/20120406/1333682532

Page 16: constexpr idioms

まとめ

INDEX TUPLE

• パック展開を利用しコンテナ走査の再帰深度を抑制

• 肝となるindex_rangeは計算量・再帰深度O(logN)

• 対象がインデックスアクセス可能な必要あり

• C++14から同様の機能が標準入り(std::integer_sequence)

Page 17: constexpr idioms

パラメータパック分割テンプレートパラメータパックTYPES1をTYPES2とRESTに分割

Page 18: constexpr idioms

パラメータパック分割

Types1の分割の前半となる部分列Types2が作れる場合に分割可能

Page 19: constexpr idioms

パラメータパック分割(実用性は無視した) 例 : 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]

Page 20: constexpr idioms

パラメータパック分割実用的な例 : 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…で 処理する、typename T::type (即ちtypename identity<N番目の型>::type)は欲しかった型

参考 : 対数オーダーでTemplate Parameter Packから要素を取り出す http://fimbul.hateblo.jp/entry/2013/08/28/203833

Page 21: constexpr idioms

パラメータパック分割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)で実装可能

Page 22: constexpr idioms

まとめ

• 分割の前半となる部分列が作れる時、パタメータパックを分割出来る

• N個のT型からなるパラメータパックはindex_tupleを利用して生成出来る

パラメータパック分割

Page 23: constexpr idioms

分割統治法再帰の形が完全二分木型になれば深度は対数オーダーになる

Page 24: constexpr idioms

分割統治法

1. 対象がイテレータの場合

Page 25: constexpr idioms

分割統治法

// 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

Page 26: constexpr idioms

分割統治法

2. 対象がN個の引数の場合

Page 27: constexpr idioms

分割統治法

N個の数の総和関数sumを再び考える

Page 28: constexpr idioms

分割統治法

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に渡す

Page 29: constexpr idioms

分割統治法

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...); } };

パラメータパック分割を用いて引数列を分割しながら再帰

Page 30: constexpr idioms

分割統治法

template <> struct sum_impl<std::index_sequence<>> { template <typename T> static constexpr T eval(const T& arg) { return arg; } };

分割を繰り返し要素数が1になった段階(最も再帰が深い段階)

ここから統治段階に移り、和を計算する 再帰深度はO(logN)

Page 31: constexpr idioms

分割統治法

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

Page 32: constexpr idioms

まとめ

• 再帰深度の改善が見込める

• 対象がイテレータなら比較的簡単、パックの分割を利用しても実装可(最初に引数をコンテナに入れてしまうのも手)

• 継承の形のコントロールも可能

分割統治法

Page 33: constexpr idioms

型のMAP

型(非型テンプレートパラメータ)をキーとして 型(非型テンプレートパラメータ)を得る

Page 34: constexpr idioms

型のMAP

例 : mpl::vectorのようなものを作ってみる

Page 35: constexpr idioms

型のMAPTypesと同じ長さの数列をimplに渡す(0~N-1までの一意なインデックス生成)

template <typename... Types> struct type_vector : type_vector_impl<typename index_range<0, sizeof...(Types)>::type, identity<Types>...> {};

Page 36: constexpr idioms

型の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>のペアとして継承

Page 37: constexpr idioms

型のMAP

あるDerivedという型が一意なインデックス0,1,…,N-1を持つ !Base<T1, 0>, Base<T2, 1>, ..., Base<Tn, N-1> !を多重継承しているときDerived型オブジェクトの暗黙のアップキャストを試みる

要素アクセスの実装方針

Page 38: constexpr idioms

型のMAP

インデックスkさえ与えればインデックスの一意性から継承元Base<T, k>を一意に

特定出来るので型Tの部分は推論させることが出来る

要素アクセスの実装方針

Page 39: constexpr idioms

型の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

要素アクセスの実装

Page 40: constexpr idioms

パフォーマンス

型のMAP

この実装ではコンテナの構築時にindex_rangeをただ1度だけ呼ぶ、それ以降は、template argument deductionで任意のインデックスの型が取れる、要素アクセスは高速で再帰深度も抑えられている

Page 41: constexpr idioms

同様にして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

Page 42: constexpr idioms

まとめ

• 多重継承とアップキャストを利用して型(或いは非型テンプレートパラメータ)でmapのようなものが実現出来る

• Key側は一意な必要がある(例は一意なインデックスを用いた)

• 要素アクセスが速く、再帰深度も抑えられるので非常に有用

型のMAP

Page 43: constexpr idioms

その他の技法• 二分探索法再帰で簡単に実装可能、計算量・再帰深度改善が見込める

• 倍分再帰対象範囲を倍々にしながら再帰的に処理を適用する技法Sproutで再帰深度を抑えたC++11 constexpr対応distanceの実装等に使用、解説はあるので以下を参考にすると良い

参考: constexpr アルゴリズムの実装イディオム その1 http://boleros.hateblo.jp/entry/20130221/1361453624