【问题标题】:Capturing perfectly-forwarded variable in lambda在 lambda 中捕获完美转发的变量
【发布时间】:2015-01-06 00:34:16
【问题描述】:
template<typename T> void doSomething(T&& mStuff)
{
    auto lambda([&mStuff]{ doStuff(std::forward<T>(mStuff)); });
    lambda();
}

使用&amp;mStuff 语法捕获完美转发的mStuff 变量是否正确?

或者对于完美转发的变量是否有特定的捕获语法?

编辑:如果完美转发的变量是参数包怎么办?

【问题讨论】:

  • [mStuff = std::forward&lt;T&gt;(mStuff)] {...}
  • @0x499602D2: 如果是参数包呢? mArgs = std::forward&lt;Ts&gt;(mArgs)... 无法编译
  • @0x499602D2 当然是转发。但是您是通过引用捕获的。您的第一条评论按价值衡量。
  • @VittorioRomeo 如果你想通过引用捕捉,当然是。
  • @VittorioRomeo 我看到you wrote an article about this,链接自isocpp.org - 我建议在此处的答案中进行总结,因为(根据我的有限理解!)它似乎比更详细/准确任何现有的答案。

标签: c++ c++11 lambda c++14 perfect-forwarding


【解决方案1】:

捕获完美转发的 mStuff 变量是否正确 &mStuff 语法?

是的,假设您不在doSomething 之外使用此 lambda。您的代码会捕获每个引用的 mStuff 并将其正确地转发到 lambda 中。

对于作为参数包的 mStuff,使用带有包扩展的简单捕获就足够了:

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

lambda 捕获每个引用的 mStuff 的每个元素。闭包对象为每个参数保存一个左值引用,无论其值类别如何。完美转发仍然有效;事实上,甚至没有区别,因为命名的右值引用无论如何都是左值。

【讨论】:

  • 在第一种情况下,在 lambda 中,您拥有对所有内容的左值引用。而且您有一个奇怪的事实,即您的 lambda 仅在您离开当前范围之前有效。两者都意味着它不是 OP 问题的通用解决方案。在第二种和第三种情况下,您捕获按值,这也与完美转发不同(您将完美转发到按值捕获中)。第四种情况类似,但没有发生完美转发。简而言之,它们都不是“完美转发”的完美类似物。试试forward_as_tuple 可能吗?
  • @Yakk 我确实重写了我的答案。但是:保存左值引用正是这里应该做的,forward_as_tuple 不合适。考虑到它通过引用捕获的事实,lambda 仅在我离开范围之前才有效的“奇怪”事实在某种程度上是不言自明的。
  • 标准中的引用捕获生命周期规则引用捕获的变量,而不是数据及其范围。这允许进行实际的实际优化(仅捕获堆栈指针),这使得它是否是缺陷成为一个小问题。
  • std::tuple&lt;T&gt; where T&amp;&amp; is deduced 为您提供右值的值和左值的引用,并且可以安全地返回,并且对应于从具有完美转发的函数返回的手动函数对象通常是如何实现的。
【解决方案2】:

要使 lambda 在创建它的范围之外有效,您需要一个以不同方式处理左值和右值的包装类,即保留对左值的引用,但复制(通过移动)右值。

头文件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));
}

显示它有效的示例/测试代码。请注意,“bar”示例展示了如何使用 std::tuple&lt;...&gt; 来解决 lambda 捕获初始化程序中缺少包扩展的问题,这对于可变参数捕获很有用。

#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);

}

【讨论】:

    【解决方案3】:

    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

    【讨论】:

    • 甚至更短的[cap = capture&lt;T&gt; {std::forward&lt;T&gt; (value)}] { /* use cap.value */ }
    • 也可以使用std::tuple 代替定制的capture 类型:[cap = std::tuple&lt;T&gt; (std::forward&lt;T&gt; (value))] { /* use std::get&lt;0&gt; (cap) */ }
    【解决方案4】:

    是的,您可以完美捕捉,但不能直接捕捉。您需要将类型包装在另一个类中:

    #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));
    }
    

    然后将它们作为参数传递给一个 lambda,该 lambda 返回一个按值捕获参数的嵌套 lambda:

    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();
    }
    

    【讨论】:

    【解决方案5】:

    这是一个针对 C++17 的解决方案,它使用 deduction guides 来简化它。我正在详细阐述 Vittorio Romeo(OP)blog post,他为自己的问题提供了解决方案。

    std::tuple 可用于包装完美转发的变量,根据需要对每个变量进行复制或保留每个变量的引用。元组本身是由 lambda 捕获的值。

    为了使它更简单、更简洁,我将创建一个从std::tuple 派生的新类型,以便提供指导构造(这将使我们避免std::forwarddecltype() 样板)和指针式访问器,以防只有一个变量要捕获。

    // 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";
        };
    };
    

    【讨论】:

      猜你喜欢
      • 2020-12-04
      • 2015-10-03
      • 2022-07-21
      • 2017-08-05
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-11-28
      相关资源
      最近更新 更多