std::invoke_resultの用例
知人にstd::invoke_result(旧std::result_of)の使用例を聞かれたので。
std::invoke_resultを使うと関数の返り値の型を取得できるため、例えば述語関数を引数として受け取り、その述語関数の返り値の型に応じた値を返すような関数を書くことができます。例えば、map/reduceのmap関数のようなものを書くことができます。通常map関数では、入力として配列等の何らかのコレクションと述語関数を受け取り、述語関数が返す値(型は入力とは異なってもいい)を新しいコレクションとして返します。
C++でmap処理(STLのmapコンテナではなく、map/reduceのmapの方)といえばtransform関数で行います。transform関数では処理対象の範囲をイテレーターで受け取り、出力先についてもイテレーターで引数指定します(以下の例参照)。
std::transform(std::cbegin(in), std::cend(in), std::back_inserter(out), [](auto i) {return i*2;});
これをstd::invoke_resultを使い、PHP,Pythonのようなスクリプト言語っぽく、以下のようにmap処理したコンテナを返す関数を書いてみます。
std::vector<int> r = ll_like_map(v, [](auto i) {return i*2;});
※ ll_like_mapのllはLightweight Languageの略
まずは第一段階。std::vector<T>のコンテナを受けて、map処理した同型のstd::vector<T>のコンテナを返す関数です。この時点ではstd::invoke_resultはまだ使っていません。
初版
template<typename T, typename F> std::vector<T> ll_like_map(const std::vector<T>& vec, F&& func) { std::vector<T> results; std::transform(std::cbegin(vec), std::cend(vec), std::back_inserter(results), std::forward<F>(func)); return results; }
呼び出しは以下のように行います。
std::vector<int> v{1, 2, 3}; auto r = ll_like_map(v, [](auto i) {return i*2;});
上の例ではintをmap処理してintを返していますが、mapでは入力コンテナの値型と出力コンテナの値型が異なることも有り得るので、これに対応します。
入力値の型から出力値の型への変換は述語関数で行いますが、ここで、出力値の型を取得するのにstd::invoke_resultを使います。typename R = typename std::invoke_result<F&, const T&>::type>とすることで型Rは述語関数Fが返す返り値の型になります。出力値の型(R)を取得できたので、あとは出力用コンテナstd::vector<R>を構築して返すだけです。
これで、std::vector<T>を受けてstd::vector<R>を返すmap関数ができました。もちろんTとRが同じ型でも問題ありません。
第二版
template<typename T, typename F, typename R = typename std::invoke_result<F&, const T&>::type> std::vector<R> ll_like_map2(const std::vector<T>& vec, F&& func) { std::vector<R> results; std::transform(std::cbegin(vec), std::cend(vec), std::back_inserter(results), std::forward<F>(func)); return results; }
以下の呼び出し例は、入力コンテナの値型がstd::pair<int, int>、出力コンテナの値型がintと型が異なるケースです。各pair要素のfirst,secondの和を返しています。
型Rを述語関数の定義から自動的に取得するので呼び出し元から指定する必要はありません。
std::vector<std::pair<int, int> > v{{1, 2}, {3, 4}, {5, 6}}; auto r = ll_like_map2(v, [](const auto& p) {return p.first + p.second;});
std::invoke_resultの例については以上です。
おまけ
std::invoke_resultとは関係ありませんが、第二版の例では入力と出力のコンテナがstd::vector固定なので、std::list等、他のコンテナも使えるようにしてみます。
まず、入力コンテナを変更できるようにしてみます。
ll_like_map2()に対し'typename C'をパラメータに追加し、入力コンテナの型であるstd::vector<T>をCに置き換えればその他のコンテナを受け付けられるようになります。
第三版
template<typename C, typename F, typename R = typename std::invoke_result<F&, const typename C::value_type&>::type> std::vector<R> ll_like_map3(const C& container, F&& func) { std::vector<R> results; std::transform(std::cbegin(container), std::cend(container), std::back_inserter(results), std::forward<F>(func)); return results; }
出力コンテナがstd::vector固定のままなので、次に出力のコンテナを入力のコンテナと同じ種類になるようにします。
これにはTemplate template parameterを使います。'typename C'を'template <typename, typename ...> typename C'にすることで型Cはコンテナ型(std::vector<T>の'std::vector'の部分)を指すようになります。これで、入力コンテナをC<T>、出力コンテナをC<R>と表すことができるようになります。
第四版
template<template <typename, typename ...> typename C, typename T, typename ... TArgs, typename F, typename R = typename std::invoke_result<F&, const typename C<T, TArgs ...>::value_type&>::type> C<R> ll_like_map4(const C<T, TArgs ...>& container, F&& func) { C<R> results; std::transform(std::cbegin(container), std::cend(container), std::back_inserter(results), std::forward<F>(func)); return results; }
C<T>をC<R>に変換するmap関数ができました。
以下の例では入力がstd::list<std::pair<int, int>>なので、出力コンテナもstd::listとなり、rはstd::list<int>になります。C = std::list, T = std::pair<int, int>, R = int ですね。
std::list<std::pair<int, int> > l{{1, 2}, {3, 4}, {5, 6}}; auto r = ll_like_map4(l, [](const auto& p) {return p.first + p.second;});
投稿日:2019/06/02 23:38
タグ: C++