在 lambda 中移动捕获

Posted

技术标签:

【中文标题】在 lambda 中移动捕获【英文标题】:Move capture in lambda 【发布时间】:2012-01-28 06:00:12 【问题描述】:

如何在 C++11 lambda 中通过移动(也称为右值引用)进行捕获?

我正在尝试写这样的东西:

std::unique_ptr<int> myPointer(new int);

std::function<void(void)> example = [std::move(myPointer)]
   *myPointer = 4;
;

【问题讨论】:

【参考方案1】:

C++14 中的广义 lambda 捕获

在 C++14 中,我们将拥有所谓的generalized lambda capture。这将启用移动捕获。以下将是 C++14 中的合法代码:

using namespace std;

// a unique_ptr is move-only
auto u = make_unique<some_type>( some, parameters );  

// move the unique_ptr into the lambda
go.run( [ u = move(u) ]  do_something_with( u );  ); 

但从某种意义上说,捕获的变量可以用类似这样的任何东西初始化:

auto lambda = [value = 0] mutable  return ++value; ;

在 C++11 中这还不可能,但有一些涉及辅助类型的技巧。幸运的是,Clang 3.4 编译器已经实现了这个很棒的功能。编译器将在 2013 年 12 月或 2014 年 1 月发布,如果 recent release pace 将保留。

更新:Clang 3.4 compiler 于 2014 年 1 月 6 日发布,具有上述功能。

移动捕获的解决方法

这是一个辅助函数 make_rref 的实现,它有助于人工移动捕获

#include <cassert>
#include <memory>
#include <utility>

template <typename T>
struct rref_impl

    rref_impl() = delete;
    rref_impl( T && x ) : xstd::move(x) 
    rref_impl( rref_impl & other )
        : xstd::move(other.x), isCopiedtrue
    
        assert( other.isCopied == false );
    
    rref_impl( rref_impl && other )
        : xstd::move(other.x), isCopiedstd::move(other.isCopied)
    
    
    rref_impl & operator=( rref_impl other ) = delete;
    T && move()
    
        return std::move(x);
    

private:
    T x;
    bool isCopied = false;
;

template<typename T> rref_impl<T> make_rref( T && x )

    return rref_impl<T> std::move(x) ;

这是一个在我的 gcc 4.7.3 上成功运行的函数的测试用例。

int main()

    std::unique_ptr<int> pnew int(0);
    auto rref = make_rref( std::move(p) );
    auto lambda =
        [rref]() mutable -> std::unique_ptr<int>  return rref.move(); ;
    assert(  lambda() );
    assert( !lambda() );

这里的缺点是lambda 是可复制的,当复制rref_impl 的复制构造函数中的断言时,会导致运行时错误。以下可能是更好甚至更通用的解决方案,因为编译器会捕获错误。

在 C++11 中模拟广义 lambda 捕获

这里还有一个想法,关于如何实现广义 lambda 捕获。函数capture()(其实现在下面找到)的使用如下:

#include <cassert>
#include <memory>

int main()

    std::unique_ptr<int> pnew int(0);
    auto lambda = capture( std::move(p),
        []( std::unique_ptr<int> & p )  return std::move(p);  );
    assert(  lambda() );
    assert( !lambda() );

这里的lambda 是一个仿函数对象(几乎是一个真正的lambda),它在传递给capture() 时捕获了std::move(p)capture 的第二个参数是一个 lambda,它将捕获的变量作为参数。当lambda 用作函数对象时,传递给它的所有参数将作为捕获变量之后的参数转发给内部 lambda。 (在我们的例子中,没有进一步的参数需要转发)。本质上,与之前的解决方案相同。以下是capture 的实现方式:

#include <utility>

template <typename T, typename F>
class capture_impl

    T x;
    F f;
public:
    capture_impl( T && x, F && f )
        : xstd::forward<T>(x), fstd::forward<F>(f)
    

    template <typename ...Ts> auto operator()( Ts&&...args )
        -> decltype(f( x, std::forward<Ts>(args)... ))
    
        return f( x, std::forward<Ts>(args)... );
    

    template <typename ...Ts> auto operator()( Ts&&...args ) const
        -> decltype(f( x, std::forward<Ts>(args)... ))
    
        return f( x, std::forward<Ts>(args)... );
    
;

template <typename T, typename F>
capture_impl<T,F> capture( T && x, F && f )

    return capture_impl<T,F>(
        std::forward<T>(x), std::forward<F>(f) );

第二个解决方案也更简洁,因为如果捕获的类型不可复制,它会禁用复制 lambda。在第一个只能在运行时使用assert() 进行检查的解决方案中。

【讨论】:

我在 G++-4.8 -std=c++11 中一直使用这个,我认为它是 C++11 的一个特性。现在用惯了这个,突然意识到这是一个C++14的特性……我该怎么办!! @RnMss 您指的是哪个功能?广义 lambda 捕获? @RalphTandetzky 我想是的,我刚刚检查了一下,与 XCode 捆绑的 clang 版本似乎也支持它!它会警告它是 C++1y 扩展,但它确实有效。 @RnMss 要么使用 moveCapture 包装器将它们作为参数传递(此方法在上面和 Capn'Proto 中使用,这是一个由 protobuffs 的创建者创建的库),或者只是接受你需要编译器支持它:P 不,实际上不是一回事。示例:您想使用 lambda 生成一个线程,该线程可以移动捕获唯一指针。生成函数可能会返回,并且 unique_ptr 在仿函数执行之前超出范围。因此,您有一个对 unique_ptr 的悬空引用。欢迎来到 undefined-behavior-land。【参考方案2】:

您也可以使用std::bind 来捕获unique_ptr

std::function<void()> f = std::bind(
                              [] (std::unique_ptr<int>& p)  *p=4; ,
                              std::move(myPointer)
                          );

【讨论】:

你检查过代码是否可以编译吗?在我看来不是这样,因为首先缺少变量名,其次unique_ptr 右值引用无法绑定到int * 请注意,在 Visual Studio 2013 中,将 std::bind 转换为 std::function 仍会导致它复制所有绑定变量(在本例中为myPointer)。因此上述代码在 VS2013 中无法编译。不过,它在 GCC 4.8 中运行良好。【参考方案3】:

您可以使用std::bind 实现大部分您想要的,如下所示:

std::unique_ptr<int> myPointer(new int42);

auto lambda = std::bind([](std::unique_ptr<int>& myPointerArg)
    *myPointerArg = 4;
     myPointerArg.reset(new int237);
, std::move(myPointer));

这里的技巧是,我们不是在捕获列表中捕获您的仅移动对象,而是将其作为参数,然后通过std::bind 使用部分应用程序使其消失。请注意,lambda 通过引用 获取它,因为它实际上存储在绑定对象中。我还添加了写入到实际可移动对象的代码,因为这是您可能想要做的事情。

在 C++14 中,您可以使用广义 lambda 捕获来达到相同的目的,代码如下:

std::unique_ptr<int> myPointer(new int42);

auto lambda = [myPointerCapture = std::move(myPointer)]() mutable 
    *myPointerCapture = 56;
    myPointerCapture.reset(new int237);
;

但这段代码不会通过std::bind 为您购买 C++11 中没有的任何东西。 (在某些情况下,广义 lambda 捕获更强大,但在这种情况下并非如此。)

现在只有一个问题;你想把这个函数放在std::function中,但是那个类要求函数是CopyConstructible,但它不是,它只是MoveConstructible,因为它存储了一个std::unique_ptr而不是CopyConstructible。

您可以使用包装类和另一个间接级别来解决此问题,但也许您根本不需要std::function。根据您的需要,您也许可以使用std::packaged_task;它和std::function 做同样的工作,但它不需要函数是可复制的,只能移动(类似地,std::packaged_task 只能移动)。缺点是因为它打算与 std::future 一起使用,所以只能调用一次。

这是一个展示所有这些概念的简短程序。

#include <functional>   // for std::bind
#include <memory>       // for std::unique_ptr
#include <utility>      // for std::move
#include <future>       // for std::packaged_task
#include <iostream>     // printing
#include <type_traits>  // for std::result_of
#include <cstddef>

void showPtr(const char* name, const std::unique_ptr<size_t>& ptr)

    std::cout << "- &" << name << " = " << &ptr << ", " << name << ".get() = "
              << ptr.get();
    if (ptr)
        std::cout << ", *" << name << " = " << *ptr;
    std::cout << std::endl;


// If you must use std::function, but your function is MoveConstructable
// but not CopyConstructable, you can wrap it in a shared pointer.
template <typename F>
class shared_function : public std::shared_ptr<F> 
public:
    using std::shared_ptr<F>::shared_ptr;

    template <typename ...Args>
    auto operator()(Args&&...args) const
        -> typename std::result_of<F(Args...)>::type
    
        return (*(this->get()))(std::forward<Args>(args)...);
    
;

template <typename F>
shared_function<F> make_shared_fn(F&& f)

    return shared_function<F>
        new typename std::remove_reference<F>::typestd::forward<F>(f);



int main()

    std::unique_ptr<size_t> myPointer(new size_t42);
    showPtr("myPointer", myPointer);
    std::cout << "Creating lambda\n";

#if __cplusplus == 201103L // C++ 11

    // Use std::bind
    auto lambda = std::bind([](std::unique_ptr<size_t>& myPointerArg)
        showPtr("myPointerArg", myPointerArg);  
        *myPointerArg *= 56;                    // Reads our movable thing
        showPtr("myPointerArg", myPointerArg);
        myPointerArg.reset(new size_t*myPointerArg * 237); // Writes it
        showPtr("myPointerArg", myPointerArg);
    , std::move(myPointer));

#elif __cplusplus > 201103L // C++14

    // Use generalized capture
    auto lambda = [myPointerCapture = std::move(myPointer)]() mutable 
        showPtr("myPointerCapture", myPointerCapture);
        *myPointerCapture *= 56;
        showPtr("myPointerCapture", myPointerCapture);
        myPointerCapture.reset(new size_t*myPointerCapture * 237);
        showPtr("myPointerCapture", myPointerCapture);
    ;

#else
    #error We need C++11
#endif

    showPtr("myPointer", myPointer);
    std::cout << "#1: lambda()\n";
    lambda();
    std::cout << "#2: lambda()\n";
    lambda();
    std::cout << "#3: lambda()\n";
    lambda();

#if ONLY_NEED_TO_CALL_ONCE
    // In some situations, std::packaged_task is an alternative to
    // std::function, e.g., if you only plan to call it once.  Otherwise
    // you need to write your own wrapper to handle move-only function.
    std::cout << "Moving to std::packaged_task\n";
    std::packaged_task<void()> fstd::move(lambda);
    std::cout << "#4: f()\n";
    f();
#else
    // Otherwise, we need to turn our move-only function into one that can
    // be copied freely.  There is no guarantee that it'll only be copied
    // once, so we resort to using a shared pointer.
    std::cout << "Moving to std::function\n";
    std::function<void()> fmake_shared_fn(std::move(lambda));
    std::cout << "#4: f()\n";
    f();
    std::cout << "#5: f()\n";
    f();
    std::cout << "#6: f()\n";
    f();
#endif

我已经把上面的程序on Coliru放好了,所以你可以运行和玩代码。

这是一些典型的输出...

- &myPointer = 0xbfffe5c0, myPointer.get() = 0x7ae3cfd0, *myPointer = 42
Creating lambda
- &myPointer = 0xbfffe5c0, myPointer.get() = 0x0
#1: lambda()
- &myPointerArg = 0xbfffe5b4, myPointerArg.get() = 0x7ae3cfd0, *myPointerArg = 42
- &myPointerArg = 0xbfffe5b4, myPointerArg.get() = 0x7ae3cfd0, *myPointerArg = 2352
- &myPointerArg = 0xbfffe5b4, myPointerArg.get() = 0x7ae3cfe0, *myPointerArg = 557424
#2: lambda()
- &myPointerArg = 0xbfffe5b4, myPointerArg.get() = 0x7ae3cfe0, *myPointerArg = 557424
- &myPointerArg = 0xbfffe5b4, myPointerArg.get() = 0x7ae3cfe0, *myPointerArg = 31215744
- &myPointerArg = 0xbfffe5b4, myPointerArg.get() = 0x7ae3cfd0, *myPointerArg = 3103164032
#3: lambda()
- &myPointerArg = 0xbfffe5b4, myPointerArg.get() = 0x7ae3cfd0, *myPointerArg = 3103164032
- &myPointerArg = 0xbfffe5b4, myPointerArg.get() = 0x7ae3cfd0, *myPointerArg = 1978493952
- &myPointerArg = 0xbfffe5b4, myPointerArg.get() = 0x7ae3cfe0, *myPointerArg = 751631360
Moving to std::function
#4: f()
- &myPointerArg = 0x7ae3cfd4, myPointerArg.get() = 0x7ae3cfe0, *myPointerArg = 751631360
- &myPointerArg = 0x7ae3cfd4, myPointerArg.get() = 0x7ae3cfe0, *myPointerArg = 3436650496
- &myPointerArg = 0x7ae3cfd4, myPointerArg.get() = 0x7ae3d000, *myPointerArg = 2737348608
#5: f()
- &myPointerArg = 0x7ae3cfd4, myPointerArg.get() = 0x7ae3d000, *myPointerArg = 2737348608
- &myPointerArg = 0x7ae3cfd4, myPointerArg.get() = 0x7ae3d000, *myPointerArg = 2967666688
- &myPointerArg = 0x7ae3cfd4, myPointerArg.get() = 0x7ae3cfe0, *myPointerArg = 3257335808
#6: f()
- &myPointerArg = 0x7ae3cfd4, myPointerArg.get() = 0x7ae3cfe0, *myPointerArg = 3257335808
- &myPointerArg = 0x7ae3cfd4, myPointerArg.get() = 0x7ae3cfe0, *myPointerArg = 2022178816
- &myPointerArg = 0x7ae3cfd4, myPointerArg.get() = 0x7ae3d000, *myPointerArg = 2515009536

您会看到堆位置被重用,表明std::unique_ptr 工作正常。当我们将函数存储在我们提供给std::function 的包装器中时,您还会看到函数本身在移动。

如果我们切换到使用std::packaged_task,它的最后一部分就变成了

Moving to std::packaged_task
#4: f()
- &myPointerArg = 0xbfffe590, myPointerArg.get() = 0x7ae3cfe0, *myPointerArg = 751631360
- &myPointerArg = 0xbfffe590, myPointerArg.get() = 0x7ae3cfe0, *myPointerArg = 3436650496
- &myPointerArg = 0xbfffe590, myPointerArg.get() = 0x7ae3d000, *myPointerArg = 2737348608

所以我们看到函数已被移动,但不是被移动到堆上,而是在堆栈上的 std::packaged_task 内。

希望这会有所帮助!

【讨论】:

【参考方案4】:

晚了,但有些人(包括我)仍然停留在 c++11 上:

说实话,我不太喜欢发布的任何解决方案。我确信它们会起作用,但它们需要很多额外的东西和/或神秘的std::bind 语法......而且我认为这样一个临时解决方案不值得努力升级时无论如何都会被重构到 c++ >= 14。所以我认为最好的解决方案是完全避免为 c++11 移动捕获。

通常最简单且可读性最好的解决方案是使用std::shared_ptr,它是可复制的,因此完全可以避免移动。缺点是效率稍低,但在很多情况下效率并不那么重要。

// myPointer could be a parameter or something
std::unique_ptr<int> myPointer(new int);

// convert/move the unique ptr into a shared ptr
std::shared_ptr<int> mySharedPointer( std::move(myPointer) );

std::function<void(void)> = [mySharedPointer]()
   *mySharedPointer = 4;
;

// at end of scope the original mySharedPointer is destroyed,
// but the copy still lives in the lambda capture.

.

如果发生非常罕见的情况,那么move 指针确实是强制性的(例如,由于删除持续时间过长,您想在单独的线程中显式删除指针,或者性能绝对至关重要),这几乎是只有在 c++11 中我仍然使用原始指针的情况。这些当然也是可复制的。

通常我用//FIXME: 标记这些罕见的情况,以确保在升级到 c++ 14 后重构它。

// myPointer could be a parameter or something
std::unique_ptr<int> myPointer(new int);

//FIXME:c++11 upgrade to new move capture on c++>=14

// "move" the pointer into a raw pointer
int* myRawPointer = myPointer.release();

// capture the raw pointer as a copy.
std::function<void(void)> = [myRawPointer]()
   std::unique_ptr<int> capturedPointer(myRawPointer);
   *capturedPointer = 4;
;

// ensure that the pointer's value is not accessible anymore after capturing
myRawPointer = nullptr;

是的,这些天原始指针非常不受欢迎(并非没有理由),但我真的认为在这些罕见的(和临时的!)情况下,它们是最好的解决方案。

【讨论】:

谢谢,使用 C++14 和其他解决方案都很好。拯救了我的一天! 原始指针技巧是使用std::function的完美解决方法【参考方案5】:

这似乎适用于 gcc4.8

#include <memory>
#include <iostream>

struct Foo ;

void bar(std::unique_ptr<Foo> p) 
    std::cout << "bar\n";


int main() 
    std::unique_ptr<Foo> p(new Foo);
    auto f = [ptr = std::move(p)]() mutable 
        bar(std::move(ptr));
    ;
    f();
    return 0;

【讨论】:

【参考方案6】:

我正在查看这些答案,但我发现 bind 难以阅读和理解。所以我所做的就是制作一个在副本上移动的课程。这样一来,它在做什么就很明确了。

#include <iostream>
#include <memory>
#include <utility>
#include <type_traits>
#include <functional>

namespace detail

    enum selection_enabler  enabled ;


#define ENABLE_IF(...) std::enable_if_t<(__VA_ARGS__), ::detail::selection_enabler> \
                          = ::detail::enabled

// This allows forwarding an object using the copy constructor
template <typename T>
struct move_with_copy_ctor

    // forwarding constructor
    template <typename T2
        // Disable constructor for it's own type, since it would
        // conflict with the copy constructor.
        , ENABLE_IF(
            !std::is_same<std::remove_reference_t<T2>, move_with_copy_ctor>::value
        )
    >
    move_with_copy_ctor(T2&& object)
        : wrapped_object(std::forward<T2>(object))
    
    

    // move object to wrapped_object
    move_with_copy_ctor(T&& object)
        : wrapped_object(std::move(object))
    
    

    // Copy constructor being used as move constructor.
    move_with_copy_ctor(move_with_copy_ctor const& object)
    
        std::swap(wrapped_object, const_cast<move_with_copy_ctor&>(object).wrapped_object);
    

    // access to wrapped object
    T& operator()()  return wrapped_object; 

private:
    T wrapped_object;
;


template <typename T>
move_with_copy_ctor<T> make_movable(T&& object)

    return std::forward<T>(object) ;


auto fn1()

    std::unique_ptr<int, std::function<void(int*)>> x(new int(1)
                           , [](int * x)
                           
                               std::cout << "Destroying " << x << std::endl;
                               delete x;
                           );
    return [y = make_movable(std::move(x))]() mutable 
        std::cout << "value: " << *y() << std::endl;
        return;
    ;


int main()

    
        auto x = fn1();
        x();
        std::cout << "object still not deleted\n";
        x();
    
    std::cout << "object was deleted\n";

move_with_copy_ctor 类和它的辅助函数make_movable() 将适用于任何可移动但不可复制的对象。要访问包装的对象,请使用operator()()

预期输出:

值:1 对象仍未删除 值:1 销毁 000000DFDD172280 对象被删除

好吧,指针地址可能会有所不同。 ;)

Demo

【讨论】:

以上是关于在 lambda 中移动捕获的主要内容,如果未能解决你的问题,请参考以下文章

在 c++14 lambda 表达式中捕获和移动 unique_ptr

将 packaged_task 对象移动到 lambda 捕获时出错

如果 lambda 使用 std::move() 捕获不可复制的对象,为啥它是不可移动的?

为啥我不能在 C++14 的 lambda 中移动 std::unique_ptr?

在 clang 和 gcc 中移动可分配的 lambda

在移动浏览器中以 60 FPS 的速度捕获视频