web-dev-qa-db-ja.com

ラムダで完全に転送された変数をキャプチャする

template<typename T> void doSomething(T&& mStuff)
{
    auto lambda([&mStuff]{ doStuff(std::forward<T>(mStuff)); });
    lambda();
}

完全に転送されたmStuff変数を&mStuff構文でキャプチャすることは正しいですか?

または、完全に転送された変数の特定のキャプチャ構文はありますか?

編集:完全に転送された変数がパラメーターパックの場合はどうなりますか?

47
Vittorio Romeo

&mStuff構文で完全に転送されたmStuff変数をキャプチャすることは正しいですか?

はい、このラムダをdoSomething外で使用しないことを前提としています。コードは参照ごとにmStuffをキャプチャし、ラムダ内に正しく転送します。

MStuffがパラメーターパックである場合は、pack-expansionでシンプルキャプチャーを使用するだけで十分です。

template <typename... T> void doSomething(T&&... mStuff)
{
    auto lambda = [&mStuff...]{ doStuff(std::forward<T>(mStuff)...); };
}

ラムダは、参照ごとにmStuffのすべての要素をキャプチャします。クロージャオブジェクトは、値のカテゴリに関係なく、各引数の左辺値参照を保存します。完全転送は引き続き機能します。実際、名前付き右辺値参照はとにかく左辺値であるため、違いはありません。

24
Columbo

ラムダが作成されるスコープの外で有効にするためには、左辺値と右辺値を別々に処理する、つまり左辺値への参照を保持しながら、右辺値の(移動によって)コピーを作成するラッパークラスが必要です。

ヘッダーファイルcapture.h:

#pragma once

#include <type_traits>
#include <utility>

template < typename T >
class capture_wrapper
{
   static_assert(not std::is_rvalue_reference<T>{},"");
   std::remove_const_t<T> mutable val_;
public:
   constexpr explicit capture_wrapper(T&& v)
      noexcept(std::is_nothrow_move_constructible<std::remove_const_t<T>>{})
   :val_(std::move(v)){}
   constexpr T&& get() const noexcept { return std::move(val_); }
};

template < typename T >
class capture_wrapper<T&>
{
   T& ref_;
public:
   constexpr explicit capture_wrapper(T& r) noexcept : ref_(r){}
   constexpr T& get() const noexcept { return ref_; }
};

template < typename T >
constexpr typename std::enable_if<
   std::is_lvalue_reference<T>{},
   capture_wrapper<T>
>::type
capture(std::remove_reference_t<T>& t) noexcept
{
   return capture_wrapper<T>(t);
}

template < typename T >
constexpr typename std::enable_if<
   std::is_rvalue_reference<T&&>{},
   capture_wrapper<std::remove_reference_t<T>>
>::type
capture(std::remove_reference_t<T>&& t)
   noexcept(std::is_nothrow_constructible<capture_wrapper<std::remove_reference_t<T>>,T&&>{})
{
   return capture_wrapper<std::remove_reference_t<T>>(std::move(t));
}

template < typename T >
constexpr typename std::enable_if<
   std::is_rvalue_reference<T&&>{},
   capture_wrapper<std::remove_reference_t<T>>
>::type
capture(std::remove_reference_t<T>& t)
   noexcept(std::is_nothrow_constructible<capture_wrapper<std::remove_reference_t<T>>,T&&>{})
{
   return capture_wrapper<std::remove_reference_t<T>>(std::move(t));
}

動作することを示すサンプル/テストコード。 「バー」の例は、std::Tuple<...>を使用して、可変長キャプチャに役立つラムダキャプチャ初期化子でのパック拡張の欠如を回避する方法を示していることに注意してください。

#include <cassert>
#include <Tuple>
#include "capture.h"

template < typename T >
auto foo(T&& t)
{
   return [t = capture<T>(t)]()->decltype(auto)
   {
      auto&& x = t.get();
      return std::forward<decltype(x)>(x);
      // or simply, return t.get();
   };
}

template < std::size_t... I, typename... T >
auto bar_impl(std::index_sequence<I...>, T&&... t)
{
   static_assert(std::is_same<std::index_sequence<I...>,std::index_sequence_for<T...>>{},"");
   return [t = std::make_Tuple(capture<T>(t)...)]()
   {
      return std::forward_as_Tuple(std::get<I>(t).get()...);
   };
}
template < typename... T >
auto bar(T&&... t)
{
   return bar_impl(std::index_sequence_for<T...>{}, std::forward<T>(t)...);
}

int main()
{
   static_assert(std::is_same<decltype(foo(0)()),int&&>{}, "");
   assert(foo(0)() == 0);

   auto i = 0;
   static_assert(std::is_same<decltype(foo(i)()),int&>{}, "");
   assert(&foo(i)() == &i);

   const auto j = 0;
   static_assert(std::is_same<decltype(foo(j)()),const int&>{}, "");
   assert(&foo(j)() == &j);

   const auto&& k = 0;
   static_assert(std::is_same<decltype(foo(std::move(k))()),const int&&>{}, "");
   assert(foo(std::move(k))() == k);

   auto t = bar(0,i,j,std::move(k))();
   static_assert(std::is_same<decltype(t),std::Tuple<int&&,int&,const int&,const int&&>>{}, "");
   assert(std::get<0>(t) == 0);
   assert(&std::get<1>(t) == &i);
   assert(&std::get<2>(t) == &j);
   assert(std::get<3>(t) == k and &std::get<3>(t) != &k);

}
6
Hui

TTBOMK、C++ 14の場合、上記のライフタイム処理のソリューションは、次のように簡略化できると思います。

template <typename T> capture { T value; }

template <typename T>
auto capture_example(T&& value) {
  capture<T> cap{std::forward<T>(value)};
  return [cap = std::move(cap)]() { /* use cap.value *; };
};

以上の匿名:

template <typename T>
auto capture_example(T&& value) {
  struct { T value; } cap{std::forward<T>(value)};
  return [cap = std::move(cap)]() { /* use cap.value *; };
};

ここでそれを使用しました(確かに、この特定のコードブロックはかなり役に立たない:P)

https://github.com/EricCousineau-TRI/repro/blob/3fda1e0/cpp/generator.cc#L161-L176

4
Eric Cousineau

はい、完璧なキャプチャはできますが、直接行うことはできません。型を別のクラスでラップする必要があります。

#define REQUIRES(...) class=std::enable_if_t<(__VA_ARGS__)>

template<class T>
struct wrapper
{
    T value;
    template<class X, REQUIRES(std::is_convertible<T, X>())>
    wrapper(X&& x) : value(std::forward<X>(x))
    {}

    T get() const
    {
        return std::move(value);
    }
};

template<class T>
auto make_wrapper(T&& x)
{
    return wrapper<T>(std::forward<T>(x));
}

次に、値としてパラメーターをキャプチャするネストされたラムダを返すラムダにパラメーターとしてそれらを渡します。

template<class... Ts>
auto do_something(Ts&&... xs)
{
    auto lambda = [](auto... ws)
    {
        return [=]()
        {
            // Use `.get()` to unwrap the value
            some_other_function(ws.get()...);
        };
    }(make_wrapper(std::forward<Ts>(xs)...));

    lambda();
}
4
Paul Fultz II

これは 演繹ガイド を使用して簡単にするC++ 17のソリューションです。私はVittorio Romeoの(OP) ブログ投稿 について詳しく説明しています。彼は自分の質問に対する解決策を提供しています。

_std::Tuple_を使用して、完全に転送された変数をラップし、必要に応じて、コピーを作成するか、変数ごとにそれらの変数の参照を保持できます。タプル自体はラムダによって値が取得されます。

簡単かつ簡潔にするために、_std::Tuple_から派生した新しい型を作成して、ガイド付きの構造を提供します(これにより、_std::forward_およびdecltype()ボイラープレートを回避できます) )、キャプチャする変数が1つしかない場合のポインタのようなアクセサ。

_// This is the generic case
template <typename... T>
struct forwarder: public std::Tuple<T...> {
    using std::Tuple<T...>::Tuple;        
};

// This is the case when just one variable is being captured.
template <typename T>
struct forwarder<T>: public std::Tuple<T> {
    using std::Tuple<T>::Tuple;

    // Pointer-like accessors
    auto &operator *() {
        return std::get<0>(*this);
    }

    const auto &operator *() const {
        return std::get<0>(*this);
    }

    auto *operator ->() {
        return &std::get<0>(*this);
    }

    const auto *operator ->() const {
        return &std::get<0>(*this);
    }
};

// std::Tuple_size needs to be specialized for our type, 
// so that std::apply can be used.
namespace std {
    template <typename... T>
    struct Tuple_size<forwarder<T...>>: Tuple_size<Tuple<T...>> {};
}

// The below two functions declarations are used by the deduction guide
// to determine whether to copy or reference the variable
template <typename T>
T forwarder_type(const T&);

template <typename T>
T& forwarder_type(T&);

// Here comes the deduction guide
template <typename... T>
forwarder(T&&... t) -> forwarder<decltype(forwarder_type(std::forward<T>(t)))...>;
_

そして、次のように使用できます。

可変バージョン:

_// Increment each parameter by 1 at each invocation and print it.
// Rvalues will be copied, Lvalues will be passed as references.
auto variadic_incrementer = [](auto&&... a)
{
    return [a = forwarder(a...)]() mutable 
    { 
        std::apply([](auto &&... args) {
            (++args._value,...);
            ((std::cout << "variadic_incrementer: " << args._value << "\n"),...);
        }, a);
    };
};
_

非可変バージョン:

_// Increment the parameter by 1 at each invocation and print it.
// Rvalues will be copied, Lvalues will be passed as references.
auto single_incrementer = [](auto&& a)
{
    return [a = forwarder(a)]() mutable 
    { 
        ++a->_value;
        std::cout << "single_incrementer: " << a->_value << "\n";
    };
};
_
1
Fabio A.