Skip to content

Instantly share code, notes, and snippets.

@faithandbrave
Last active June 1, 2017 04:48
Show Gist options
  • Save faithandbrave/2fc2ed26bbc517a0aa16 to your computer and use it in GitHub Desktop.
Save faithandbrave/2fc2ed26bbc517a0aa16 to your computer and use it in GitHub Desktop.
apply review

Library Fundamentals TS v1 apply関数のレビュー

レビュアー:高橋 晶(Akira Takahashi, faithandbrave@longgate.co.jp)

概要

apply()関数は、タプルを引数として関数を呼び出すユーティリティ関数です。しかしこの関数には、標準アルゴリズムと組み合わせて使用できないという問題があります。このレビューでは、apply()のユースケースを整理し、いくつかの有用なケースでapply()を適用できないことを示します。その後、その改善のために、関数オブジェクトをタプルを引数として受け取れるよう変換する機能と、その引数適用という2段階化の提案を行います。

applyの概要

// <experimental/tuple> header

namespace std {
namespace experimental {
inline namespace fundamentals_v1 {

  template <class F, class Tuple>
  constexpr decltype(auto) apply(F&& f, Tuple&& t);

}}}

apply()は、引数としてタプルを与えると、タプルを展開して関数オブジェクトを呼び出すユーティリティ関数です。

void f(int, char, double);

apply(f, std::make_tuple(1, 'a', 3.14));

applyのユースケース

###ユースケース1 引数の転送 apply()は、関数の引数を一旦、クラスのメンバ変数として保持しておき、あとで関数の引数としてそれを渡して呼び出す、というケースで使用します。

可変個メンバ変数というのが定義できないため、引数をメンバ変数に保持する場合、タプルを使用することになります。

class X {
    std::tuple<int, char, double> args_;
public:
    void setArgs(int a, char b, double c)
    {
        args_ = std::make_tuple(a, b, c);
    }

    template <class F>
    void call(F f)
    {
        apply(f, args_);
    }
};

ユースケース2 mapのイテレーション

mapの全要素に対して処理を行う場合、for_each()アルゴリズムを使用して以下のようなコードを書くことになります。

std::map<int, std::string> m;
std::for_each(m.begin(), m.end(), [](const std::pair<const int, std::string>& x) {
    // …x.first, x.secondを使ってキー、値にアクセスする…
});

apply()は、アルゴリズムと組み合わせて使用することができず、このユースケースをサポートしていません。

ジェネリックラムダとapply()を組み合わせればこのユースケースでも扱えますが、毎回このようなコードを書くのは苦痛です。

std::for_each(m.begin(), m.end(), [](const auto& args) { apply([](int key, const std::string& value) {
    // …keyを使ってキーに、valueを使って値にアクセスする…
}, args); });

ユースケース3 複数のRangeを綴じ合わせる

Boost.Rangeのcombine()、またその前身となったHaskell言語のzip関数は、複数のRangeを合成します。

たとえば、std::vector<T>のRangeと、0から始まるインデックスのRangeをcombine()で綴じ合わせると、ZipRange<std::tuple<T, std::size_t>>のようになり、for_each()アルゴリズム内で現在のインデックスを取得できるようになります。

std::vector<T> v;
auto r = boost::combine(v, boost::irange(0u));
std::for_each(r.begin(), r.end(), [](std::tuple<T, std::size_t> args) {
    T value = std::get<0>(args);
    std::size_t index = std::get<1>(args);
});

このケースもまた、apply()が有用ですが、ユースケース2と同様にapply()を適用できません。

これに類するケースは、ほかに無数にあります:

  1. グラフの辺リストを走査する際、辺の2つの頂点を抽出する。
Graph g;
auto edge_range = edges(g);
std::for_each(begin(edge_range), end(edge_range), [](std::tuple<Vertex, Vertex> e) {
    Vertex source = std::get<0>(e);
    Vertex target = std::get<1>(e);
});
  1. 隣接する値の比較
std::vector<T> v = {1, 2, 3};
auto r = v | adjacent_zipped; // {(1, 2), (2, 3)}
std::for_each(r.begin(), r.end(), [](const std::tuple<T, T>& x) {
    T a = std::get<0>(x);
    T b = std::get<1>(x);
	if (a < b) {
	    //
	}
});

解決策

この問題を解決し、apply()をアルゴリズムと組み合わせられるようにするには、「複数引数を受け取る関数を、タプルを引数として実行する形に変換する」という機能とその引数適用を、2段階に分けて行える必要があります。

その実装は、apply()関数のラッパーとして実装できます。

#include <tuple>
#include <utility>

template<typename F, typename Tuple, size_t... I>
auto apply_impl(F&& f, Tuple&& args, std::index_sequence<I...>)
{
    return std::forward<F>(f)(std::get<I>(std::forward<Tuple>(args))...);
}

template<typename F, typename Tuple,
         typename Indices = std::make_index_sequence<std::tuple_size<Tuple>::value>>
auto apply(F&& f, Tuple&& args)
{
    return apply_impl(std::forward<F>(f), std::forward<Tuple>(args), Indices());
}

template<typename F, typename Tuple, size_t... I>
auto apply_impl(F&& f, const Tuple& args, std::index_sequence<I...>)
{
    return std::forward<F>(f)(std::get<I>(args)...);
}

template<typename F, typename Tuple,
         typename Indices = std::make_index_sequence<std::tuple_size<Tuple>::value>>
auto apply(F&& f, const Tuple& args)
{
    return apply_impl(std::forward<F>(f), args, Indices());
}

template <typename F>
class apply_functor {
    F f_;
public:
    explicit apply_functor(F&& f)
        : f_(std::forward<F>(f)) {}

    template <typename Tuple>
    auto operator()(Tuple&& args)
    {
        return apply(std::forward<F>(f_), std::forward<Tuple>(args));
    }

    template <typename Tuple>
    auto operator()(const Tuple& args)
    {
        return apply(std::forward<F>(f_), args);
    }
};

template <typename F>
apply_functor<F> make_apply(F&& f)
{
    return apply_functor<F>(std::forward<F>(f));
}

使用例:

#include <iostream>
#include <vector>
#include <string>
#include <algorithm>

int main()
{
    std::vector<std::tuple<int, char, std::string>> v = {
        {1, 'a', "Alice"},
        {2, 'b', "Bob"},
        {3, 'c', "Carol"}
    };
    
    std::for_each(v.begin(), v.end(),
      make_apply([](int a, char b, const std::string& c) {
          std::cout << a << ' ' << b << ' ' << c << std::endl;
      }
    ));
}

C++はその設計思想において、直交性を重視しています。すなわち、機能同士が、容易に組み合わせて使用できる必要があります。apply()関数を標準化するのであれば、標準アルゴリズムと組み合わせて使用できるべきだと考えます。

Boostにおける前例

この提案におけるapply()make_apply()の関係は、Boost Fusion Libraryですでに前例があります。Fusionライブラリでは、fusedmake_fused()という機能で、タプルを関数の引数として適用できるようにしています。

Boostは標準化における実験場としての役割も持っているので、この前例を調査する必要があるでしょう。

英語のコメント

apply() can't combine with algorithms

apply() is useful for many use cases. However, apply() is not support some use cases.

Use case 1 : map iteration (not support)

std::map<int, std::string> m;
std::for_each(m.begin(), m.end(), [](const std::pair<const int, std::string>& x) {
    // key access with `x.first`, and value access with `x.second`...
});

If we use apply() in this case, we need follow code:

std::for_each(m.begin(), m.end(), [](const auto& args) {
  apply([](int key, const std::string& value) {
    // key access with `key`, and value access with `value`...
  },
  args);
});

The code is very verbose.

Use case 2 : zip range (not support)

Boost.Range's combine() function and Haskell's zip function combine multiple range.

For example, if we zip with std::vector<T> and index range from 0, we can get index in for_each() algorithm.

std::vector<T> v;
auto r = boost::combine(v, boost::irange(0u));
std::for_each(r.begin(), r.end(), [](std::tuple<T, std::size_t> args) {
    T value = std::get<0>(args);
    std::size_t index = std::get<1>(args);
});

This case is useful, but apply() can't combination with algorithm.

Proposal Solution

We need a translation function object to acceptable tuple as argument. We propose make_apply() function.

#include <tuple>
#include <utility>

template<typename F, typename Tuple, size_t... I>
auto apply_impl(F&& f, Tuple&& args, std::index_sequence<I...>)
{
    return std::forward<F>(f)(std::get<I>(std::forward<Tuple>(args))...);
}

template<typename F, typename Tuple,
         typename Indices = std::make_index_sequence<std::tuple_size<Tuple>::value>>
auto apply(F&& f, Tuple&& args)
{
    return apply_impl(std::forward<F>(f), std::forward<Tuple>(args), Indices());
}

template<typename F, typename Tuple, size_t... I>
auto apply_impl(F&& f, const Tuple& args, std::index_sequence<I...>)
{
    return std::forward<F>(f)(std::get<I>(args)...);
}

template<typename F, typename Tuple,
         typename Indices = std::make_index_sequence<std::tuple_size<Tuple>::value>>
auto apply(F&& f, const Tuple& args)
{
    return apply_impl(std::forward<F>(f), args, Indices());
}

template <typename F>
class apply_functor {
    F f_;
public:
    explicit apply_functor(F&& f)
        : f_(std::forward<F>(f)) {}

    template <typename Tuple>
    auto operator()(Tuple&& args)
    {
        return apply(std::forward<F>(f_), std::forward<Tuple>(args));
    }

    template <typename Tuple>
    auto operator()(const Tuple& args)
    {
        return apply(std::forward<F>(f_), args);
    }
};

template <typename F>
apply_functor<F> make_apply(F&& f)
{
    return apply_functor<F>(std::forward<F>(f));
}

Usage:

#include <iostream>
#include <vector>
#include <string>
#include <algorithm>

int main()
{
    std::vector<std::tuple<int, char, std::string>> v = {
        {1, 'a', "Alice"},
        {2, 'b', "Bob"},
        {3, 'c', "Carol"}
    };
    
    std::for_each(v.begin(), v.end(),
      make_apply([](int a, char b, const std::string& c) {
          std::cout << a << ' ' << b << ' ' << c << std::endl;
      }
    ));
}

Boost experience

Boost Fusion Library already has make_apply() like function as boost::fusion::make_fused(). Boost has an aspect as experimental standard. We should study from Boost's experience.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment