はじめに
C++のstd::transform
はめちゃくちゃ便利です。for文のステートメントよりも関数オブジェクトのほうがポータビリティも高いし、コードの複雑化も避けやすいし、可読性も高いと思います。
しかしstd::transform
は、先にbufferを作ってそこに結果を格納しにいくのがやや気持ち悪い…。できれば結果のオブジェクトをconstにしておきたい。
そこで、std::transform
をラップして結果を戻り値として返すようにしてみようという話です。
forからtransformに
まずは、for文についてです。std::vector
の中身を2倍にしてみます。
#include <vector>
int main() {
const std::vector<int> x{ 1, 9, 8, 6, 12, 14 };
std::vector<int> y{};
y.reserve(x.size());
for (const auto e: x) {
y.emplace_back(e * 2);
}
return 0;
}
これをstd::transform
に置き換えてみましょう。
#include <algorithm>
#include <vector>
int main() {
const std::vector<int> x{ 1, 9, 8, 6, 12, 14 };
std::vector<int> y(x.size());
std::transform(
x.cbegin(),
x.cend(),
y.begin(),
[](const int e) { return e * 2; });
return 0;
}
前者はなんとなくfilterっぽい手続きなんかがforの中に追加されたりとかして、複雑になっていくケースが多いような印象です。後者は、そういった処理は別途することになるでしょうし、その結果読みやすく全体的には保守しやすいコードになると思います。
ラッパー関数の実装
冒頭にも書きましたが、目的はstd::transform
をラップして戻り値として結果のオブジェクトを返却するようにすることです。
シンプルに実装してみる
まずはシンプルに関数化してみます。
#include <algorithm>
#include <vector>
inline std::vector<int> transform(const std::vector<int>& v)
{
std::vector<int> y(v.size());
std::transform(
v.cbegin(),
v.cend(),
y.begin(),
[](const int e) { return e * 2; });
return y;
}
と、これでは2倍することしかできないので、関数オブジェクトをうけとるようにしましょう。
#include <algorithm>
#include <vector>
template <typename F>
inline std::vector<int>
transform(const std::vector<int>& v, F&& f)
{
std::vector<int> y(v.size());
std::transform(
v.cbegin(),
v.cend(),
y.begin(),
f);
return y;
}
ついでに、inputする配列の型を自由に、outputする配列の方は指定できるようにしましょう。
#include <algorithm>
#include <vector>
template <typename R, typename A, typename F>
inline R transform(const A& v, F&& f)
{
R y(v.size());
std::transform(
std::cbegin(v),
std::cend(v),
std::begin(y),
f);
return y;
}
#include <vector>
#include "transform.h"
int main() {
const std::vector<int> x{ 1, 9, 8, 6, 12, 14 };
const std::vector<int> y
= transform<std::vector<int>>(x, [](const int e) { return e * 2; }); // AとFは推論される
return 0;
}
これでも大分いい感じですが実はR
には、要素の型までではなくコンテナの型のみを指定したい気持ちになります。要素の型はf
にA::value_type
を適用したものですので、いちいち人間が指定する必要がないということです。
ここでは、テンプレートテンプレートパラメータを使ってRをもう少しスリムにします。注意すべきは、通常R
のテンプレート引数は2以上になるので、valiadic templateとして記述しておくことです。
#include <algorithm>
#include <vector>
#include <type_traits>
template <
template <typename ...> typename C,
typename A,
typename F
>
inline auto transform(const A& v, F&& f)
{
using result_type
= C<decltype(std::declval<F>()(std::declval<typename A::value_type>()))>;
result_type y(v.size());
std::transform(
std::cbegin(v),
std::cend(v),
std::begin(y),
f);
return y;
}
#include <vector>
#include "transform.h"
int main() {
const std::vector<int> x{ 1, 9, 8, 6, 12, 14 };
const std::vector<int> y
= transform<std::vector>(x, [](const int e) { return e * 2; });
return 0;
}
result_type
の定義がやや複雑ですが、要するに「F
にA
の要素を適用した戻り値の型」のオブジェクトを要素として持つコンテナ型ということですね。
もう少し型チェックをする
上記まででも普通に使えるのですが、このままではA
やらF
やらになんでも入ってしまいますので、手前でこいつらがちゃんと配列っぽいものかとか、関数ぽいものかをチェックするコードを入れてみます。それにあたり、関数の中身を構造体に移します。
#include <algorithm>
#include <type_traits>
#include <experimental/type_traits>
template <typename F, typename ...Args>
using call_operator_type = decltype(std::declval<F>()(std::declval<Args>()...));
template <typename F, typename ...Args>
using is_callable = std::experimental::is_detected<call_operator_type, F, Args...>; // f(a0, a1, ...)できるかどうか
template <typename T>
using begin_type = decltype(std::declval<T>().begin());
template <typename T>
using end_type = decltype(std::declval<T>().end());
template <typename T>
using is_iterable = std::conjunction< // 要するにand、なんでこんなわかりにくい名前に…
std::experimental::is_detected<begin_type, T>, // v.begin()できるかどうか
std::experimental::is_detected<end_type, T> // v.end()できるかどうか
>;
template <
template <typename ...> typename R,
typename F,
typename V
>
struct transform_impl {
static_assert(
is_iterable<V>::value,
"Please input iterable object.");
static_assert(
is_callable<F, typename V::value_type>::value,
"Please input unary function.");
using functor_result_type = call_operator_type<F, typename V::value_type>;
using result_type = R<functor_result_type>;
static result_type apply(const V& v, F&& unary) {
result_type ret(v.size());
std::transform(
std::cbegin(v),
std::cend(v),
std::begin(ret),
unary);
return ret;
}
};
template <
template <typename ...> typename R,
typename F,
typename V
>
inline typename transform_impl<R, V, F>::result_type
transform(const V& v, F&& unary)
{
return transform_impl<R, F, V>::apply(v, std::move(unary));
}
std::enable_if
を使う方法なんかもありますが、コンパイルエラーのメッセージがこっちのほうが読みやすくなるので、こんな感じでstatic_assert
するほうが好きです。
へんなものをいれると、Please input iterable object.とかPlease input unary function.とかコンパイルエラー出してくれていい感じです。
std::mapに対応する
このままだとresult_type
の指定の仕方が悪く、functorにstd::pair
を返す関数を入れるだけではstd::map
には格納できません。ややこしい型指定を頑張ってもいいですが、ここは特殊化でのりきりましょう。
#include <algorithm>
#include <type_traits>
#include <map>
#include <experimental/type_traits>
template <typename F, typename ...Args>
using call_operator_type = decltype(std::declval<F>()(std::declval<Args>()...));
template <typename F, typename ...Args>
using is_callable = std::experimental::is_detected<call_operator_type, F, Args...>;
template <typename T>
using begin_type = decltype(std::declval<T>().begin());
template <typename T>
using end_type = decltype(std::declval<T>().end());
template <typename T>
using is_iterable = std::conjunction<
std::experimental::is_detected<begin_type, T>,
std::experimental::is_detected<end_type, T>
>;
template <
typename F,
typename V
>
struct transform_type_check {
static_assert(
is_iterable<V>::value,
"Please input iterable object.");
static_assert(
is_callable<F, typename V::value_type>::value,
"Please input unary function.");
};
template <
template <typename ...> typename R,
typename F,
typename V
>
struct transform_impl: private transform_type_check<F, V> {
using functor_result_type = call_operator_type<F, typename V::value_type>;
using result_type = R<functor_result_type>;
static result_type apply(const V& v, F&& unary) {
result_type ret(v.size());
std::transform(
std::cbegin(v),
std::cend(v),
std::begin(ret),
unary);
return ret;
}
};
template <
typename F,
typename V
>
struct transform_impl<std::map, F, V>: private transform_type_check<F, V> { // map用に特殊化
using functor_result_type = call_operator_type<F, typename V::value_type>;
using result_type = std::map<
typename functor_result_type::first_type,
typename functor_result_type::second_type
>;
static result_type apply(const V& v, F&& unary) {
result_type ret{};
std::transform(
std::cbegin(v),
std::cend(v),
std::inserter(ret, ret.begin()),
unary);
return ret;
}
};
template <
template <typename ...> typename R,
typename F,
typename V
>
inline typename transform_impl<R, V, F>::result_type
transform(const V& v, F&& unary)
{
return transform_impl<R, F, V>::apply(v, std::move(unary));
}
static_assert
を一個にまとめるために、実装をtransform_type_check
にくくりだしてます。
まとめ
こちらができあがったものです。
https://github.com/IgnorantCoder/stl-return-value
今回解説したソースに、二項演算版を追加してあります。
std::transform
したものをconstオブジェクトで受け取れるのは精神衛生上もとても良いですし、通常最適化オプションさえきちんと設定すればNRVOも効きますのでパフォーマンス上の問題も起きません。
std::transform
以外の、関数もこんな感じでラップして用意しておくと便利ですね。