🔖

C++でラムダ式を取り回すパターンあれこれ

2021/08/06に公開
2

自分がよく使うパターンのメモなので、あまり丁寧なドキュメントではありません。
同じようなことで苦労している人の助けになれば。

ラムダ式を引数に受け取る関数

ラムダ式は1つ1つが固有の型を持つので、テンプレートで受ける。

template<typename F>
void procWithLambda(F&& func) {
  for (auto& item : container) {
    func(item);
  }
}	

ユニヴァーサル参照(F&&)で受けると、その場に書いた式ならムーブ構築されて効率が良くなる。
※右辺値参照と概念をごっちゃにしていたので修正しました。yumetodoさんありがとうございます。

std::function で受けると関数のシグニチャが明示できるものの、関数オブジェクトを保持するためにコピーやアロケーションが走り、呼び出し時のオーバーヘッドも増える。この例のように、その場で評価して保持しないのであれば、テンプレートで値のまま受けるのがよい。

C++20 が一般的になれば、コンセプトで型の要件を明示できるので、早く使いたいところ。

template <class T>
concept InvokableWithItem = std::is_invokable<T, ItemType>::value;

template<InvokableWithItem F>
void procWithLamda(F&& func) {
  for (auto& item : container) {
    func(item);
  }
}

こんな感じになるんだろう(たぶん)

返り値のあるラムダ式とないラムダ式を統一的に扱う

返り値の型自体は、std::invoke_result_t で取得できる。
これで得た型を利用し、特殊化と組み合わせることで、特定の型を返り値とする式(返り値を持たない式も含めて)を統一的に扱うことができる。

例として、ラムダ式の返り値型に応じた std::promise<T> に対して set_value() する処理を考える。

std::promise<T> は void 型が特殊化されており、一般的には set_value(T value) が定義されるが、void 型では引数無しの set_value() となり、同一のコードでは処理することができない。

しかし、利用者側に返り値の有無によって異なる名前の関数を使ってもらうのもスマートではない。そこで、次のような特殊化クラスと、振り分けるためのテンプレート関数を用意する。

template<typename TResult>
struct functor_dispatcher {
    template<typename F>
    static task<TResult> create_task(F&& functor) {
        std::promise<TResult> promise;
        return task<TResult>(std::make_shared<functor_storage>([f = std::move(functor), p = std::move(promise)]() mutable {
            auto result = f();
            p.set_value(result);
        }), std::move(promise.get_future()));
    }
};

template<>
struct functor_dispatcher<void> {
    template<typename F>
    static task<void> create_task(F&& functor) {
        std::promise<void> promise;
        return task<void>(std::make_shared<functor_storage>([f = std::move(functor), p = std::move(promise)]() mutable {
            f();
            p.set_value();
        }), std::move(promise.get_future()));
    }  
};

template<typename F>
auto create_task(F&& functor) {
    return functor_dispatcher<typename std::invoke_result_t<F>>::create_task(std::forward<F>(functor));
}

task<T> は、任意のラムダ式を非同期に実行してその結果を std::promise に書き込み、std::future を通じて取得できる型である。functor_storage は std::function に相当する機能を持つ型である。

そもそもの狙いは、ラムダ式の返り値型を抽出した上で、それが void とそれ以外の場合で実装を分岐することにある。これを関数単位で行おうとすると、ラムダ式自体の型パラメータは残しつつ、返り値の型だけ部分特殊化しようとすることになり、言語仕様的に達成できない。

そこで、処理を振り分けるために functor_dispatcher なる型を用意し、これの型パラメータに返り値の型を渡す。テンプレートクラスの特殊化は普通に行えるので、一般型と void 型の2つを定義する。そして、任意のラムダ式を受け取るための型パラメータはメンバ関数の型パラメータとしている。このように、型と関数でパラメータを分離すれば、部分特殊化のような実装が可能となる。

後は、フリーなテンプレート関数である create_task() から functor_dispatcher 型に対するパラメータを指定して呼び出せば、引数に渡したラムダ式の返り値型に応じて呼び出される処理がコンパイル時に分岐する。このフリー関数の返り値は与えたラムダ式の返り値型から推論できるので auto でよい。

Discussion

yumetodoyumetodo

右辺値参照で受けると、その場に書いた式がムーブ構築されて効率が良くなるかもしれない。

それはUniversal(Forwarding) referenceだ・・・


if constexprでうまく行かないもんですかね・・・?脳内コンパイルしかしてないですが。

template<typename F>
auto create_task(F&& functor) {
    using result_t = std::invoke_result_t<F>;
    std::promise<result_t> promise;
        return task<result_t>(std::make_shared<functor_storage>([f = std::move(functor), p = std::move(promise)]() mutable {
            if constexpr (std::is_same_v<void, result_t>) {
                auto result = f();
                p.set_value(result);
            }
            else {
                f();
                p.set_value();
            }
        }), std::move(promise.get_future()));
    }
}
ritarita

それはUniversal(Forwarding) referenceだ・・・

oops! 理解が不正確でした。ありがとうございます。

if constexprでうまく行かないもんですかね・・・?脳内コンパイルしかしてないですが。

やってみたら task<result_t> が不完全な型だとか言われて怒られてしまいましたね。
このdispatcherを思いつくまではSFINAEでゴニョゴニョやってたので、それよりは大幅に短く端的に書けるようになったとは思ってます。
if constexprでいけるなら更にスッキリするのは確かなので、引き続き検証してみます。