LoginSignup
9
8

More than 5 years have passed since last update.

transformをラップして変換後のオブジェクトを返すようにしてみる

Last updated at Posted at 2018-06-09

はじめに

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をラップして戻り値として結果のオブジェクトを返却するようにすることです。

シンプルに実装してみる

まずはシンプルに関数化してみます。

transform.h
#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倍することしかできないので、関数オブジェクトをうけとるようにしましょう。

transform.h
#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する配列の方は指定できるようにしましょう。

transform.h
#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;
}
main.cpp
#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には、要素の型までではなくコンテナの型のみを指定したい気持ちになります。要素の型はfA::value_typeを適用したものですので、いちいち人間が指定する必要がないということです。
ここでは、テンプレートテンプレートパラメータを使ってRをもう少しスリムにします。注意すべきは、通常Rのテンプレート引数は2以上になるので、valiadic templateとして記述しておくことです。

transform.h
#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;
}
main.cpp
#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の定義がやや複雑ですが、要するに「FAの要素を適用した戻り値の型」のオブジェクトを要素として持つコンテナ型ということですね。

もう少し型チェックをする

上記まででも普通に使えるのですが、このままではAやらFやらになんでも入ってしまいますので、手前でこいつらがちゃんと配列っぽいものかとか、関数ぽいものかをチェックするコードを入れてみます。それにあたり、関数の中身を構造体に移します。

transform.h
#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には格納できません。ややこしい型指定を頑張ってもいいですが、ここは特殊化でのりきりましょう。

transform.h
#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以外の、関数もこんな感じでラップして用意しておくと便利ですね。

9
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
8