C++と色々

主にC++やプログラムに関する記事を投稿します。

for_each_argument 前半

こちらの動画

www.youtube.com

で紹介されているイディオムを自分なりに解釈を加えてまとめてみようと思います。長いので前半後半に分割する予定です。

for_each_argument

C++14から入った、ジェネリックラムダを用いて、色々な型の変数を走査するfor_eachを実装したいと思います。シグネチャと利用方法は以下のような関数を作成します。

template <typename F, typename ...Args>
void for_each_argument(F&& f, Args&&... args);

利用方法

int a = 0;
for_each_argument([](auto const& x) {
        std::cout << x << std::endl;
    },
    a,
    42,
    "hello"
);

それでは、この関数はどのように実装したらよいでしょうか。

実装

以前私のブログにも乗せた、

http://nekko1119.hatenablog.com/entry/2015/03/12/033403

に近い実装が載っているのですが、可変長引数のラムダ式を使う実装方法があります。

template <typename F, typename... Args>
void for_each_argument(F&& f, Args&&... args)
{
    [](...) {}((f(std::forward<Args>(args)), 0)...);
}

可変長な関数オブジェクト定義し、その呼び出し時にfを呼び出しています。fの戻り値の型がvoidでも大丈夫なように、fを呼び出した結果の式全体の型はカンマ演算子を用いてintにしています。

完全なサンプルを載せます

#include <utility>

template <typename F, typename ...Args>
void for_each_argument(F&& f, Args&&... args)
{
    [](...) {}((f(std::forward<Args>(args)), 0)...);
}

#include <iostream>

int main()
{
    int a = 0;
    for_each_argument(
        [](auto const& x) {
            std::cout << x << std::endl;
        },
        a,
        42,
        "hello"
    );
}

実行結果

0
42
hello

同じシグネチャfor_each_argumentで、異なる実装方法もあります。initializer_listを使う方法です。いきなりですが完全なサンプルを示します。

#include <initializer_list>
#include <utility>

template <typename F, typename ...Args>
void for_each_argument(F&& f, Args&&... args)
{
    return static_cast<void>(std::initializer_list<int> {
        (f(std::forward<Args>(args)), 0)...
    });
}

#include <iostream>

int main()
{
    int a = 0;
    for_each_argument(
        [](auto const& x) {
            std::cout << x << std::endl;
        },
        a,
        42,
        "hello"
    );
}

実行結果は前のサンプルと同じなので省略

動画ではこちらの実装方法を推奨していました。理由はよくわかりませんでした。 なぜinitializer_listを使っているのかというと、パラメータをアンパックするコンテキストが必要だからです。これは前のサンプルのラムダ式の引数も当てはまります。ここでも同じようにfを評価しつつ、カンマ演算子を用いて全体としてはintのinitializer_lsitを作成しています。

make_vector

さて、前項で作成したfor_each_argument関数ですが、使い方を紹介します。 引数のリストから型を推論してstd::vectorを作成するmake_vectorを紹介します。これは以下のように使うことができます。

auto v = make_vector(1, 2, 1.0); // v is std::vector<double>

下記のように書くこともできますが、テンプレート引数を明示する必要があります。

std::vector<double> v = {1, 2, 1.0};

make_vectorの実装

完全なサンプルコードです。出力はありません

#include <initializer_list>
#include <utility>
#include <vector>

template <typename F, typename ...Args>
void for_each_argument(F&& f, Args&&... args)
{
    return static_cast<void>(std::initializer_list<int> {
        (f(std::forward<Args>(args)), 0)...
    });
}

template <typename... Args>
auto make_vector(Args&&... args)
{
    using value_type = std::common_type_t<Args...>;
    std::vector<value_type> result;
    result.reserve(sizeof...(Args));
    for_each_argument(
        [&result](auto&& x)
        {
            result.emplace_back(std::forward<decltype(x)>(x));
        },
        std::forward<Args>(args)...
    );
    return result;
}

int main()
{
    auto const v = make_vector(1, 2, 1.0);
}

まず、戻り値の型ですが、C++14から関数の戻り値の型推論が入りました。複雑になる場合はautoにできます。戻り値のvectorの要素の型ですが、std::common_type_tを使って求めることができます。これはC++11にあるstd::common_typeエイリアスで、可変長テンプレートパラメータを受け取り、全てのパラメータが暗黙に変換可能な型を返すメタ関数です。

戻り値用のvectorを作成できたら引数の数だけメモリを確保します。不必要にメモリの再確保を行わないようにするためです。

最後にfor_each_argument関数を使って要素を1つずつemplace_backしていきます。

std::tupleの走査

for_each_argumentが役立つ2つめの場面を紹介します。 std::tupleには標準で要素を走査する方法が提供されていません。自分でstd::index_sequenceを用いてインデックス走査関数を作成するか、boost::fusionを使う方法などがあります。 そして、for_each_argumentでもstd::tupleを操作するfor_each_tupleを作成することができます。for_each_tupleの使用イメージはこのような感じです。

for_each_tuple(
    [](auto const& x) { std::cout << x << " "; },
    std::make_tuple(1, "hello", 15.f, 'c') // 1 hello 15 cと出力
);

for_each_tupleの実装

N3802にて、applyという、std::getでインデックスアクセス可能なシーケンスについて走査する関数が提案されています

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

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

このような関数で、下のように、タプルの型パラメータと同じ引数を持つ関数を呼び出します。

#include <iostream>

int main()
{
    apply(
        [](int i, double d, char const* str) {
            std::cout << i << d << str << std::endl;
        },
        std::make_tuple(1, 3.14, "hello")
    );
}

もしこれが採用されたと仮定して、これを使ってfor_each_tupleの実装を考えてみます。applyは上記の関数とします。

template <typename F, typename Tuple>
void for_each_tuple(F&& f, Tuple&& tuple)
{
    apply(
        [&f](auto&&... xs) {
            for_each_argument(f, std::forward<decltype(xs)>(xs)...);
        },
        std::forward<Tuple>(tuple)
    );
}

と実装できます。applyがTupleを展開しています。 例えばfor_each_tuple(F, tuple<int, double, char const*>)のような引数の型になった場合、applyに渡している[&f](auto&&...) {...}のラムダの引数は[&f](int&&, double&&, char const*&&){...}となります。これはこのように使用できます。

for_each_tuple(
    [](auto const& x) { std::cout << x << " "; },
    std::make_tuple(1, "hello", 15.f, 'c') // 1 hello 15 cと出力
);

また、applyを使用しない前提でよければ以下のような実装をすることもできます。

#include <utility>
#include <tuple>
#include <type_traits>

template <typename F, typename Tuple, std::size_t... Is>
void for_each_tuple_impl(F&& f, Tuple&& t, std::index_sequence<Is...>)
{
    [](auto&&...) {}((std::forward<F>(f)(std::get<Is>(t)), 0)...);
}

template <typename F, typename Tuple>
void for_each_tuple(F&& f, Tuple&& t)
{
    using indices_type
        = std::make_index_sequence<
            std::tuple_size<std::decay_t<Tuple>>::value
        >;
    for_each_tuple_impl(std::forward<F>(f), std::forward<Tuple>(t), indices_type{});
}

#include <iostream>

int main()
{
    for_each_tuple(
        [](auto const& x) { std::cout << x << " "; },
        std::make_tuple(1, "hello", 15.f, 'c') // 1 hello 15 cと出力
    );
}

実行結果

1 hello 15 c 

こちらのほうが感嘆のような…?

最後に

さて、ここまでに作ったfor_each_argumentは引数を1つ取る関数を受け取り、それに残りの引数を1つずつ適用していく関数でした。後半では任意個数の関数を受け取れるfor_each_argument_nを紹介します。もちろん、動画の内容を紹介しているに過ぎませんから、ネタを知りたい方は動画を見ると知ることができます。