在lambda中移动捕获

208

我如何在C++11的lambda中使用move语义(也称为右值引用)进行捕获?

我正在尝试编写类似于以下代码:

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

std::function<void(void)> example = [std::move(myPointer)]{
   *myPointer = 4;
};
7个回答

247

C++14中的广义lambda捕获

在C++14中,我们将拥有所谓的广义lambda捕获。这使得移动捕获成为可能。以下代码将在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 ); } ); 

请注意,如果您需要将对象从lambda移动到其他函数,则需要使lambda mutable

go.run( [ u = move(u) ] mutable { do_something_with( std::move(u) ); } );

广义 lambda 捕获更加通用,因为捕获的变量可以使用任何初始化方式,例如:
auto lambda = [value = 0] mutable { return ++value; };

在C++11中,这是不可能的,但可以通过一些技巧来实现。幸运的是,Clang 3.4编译器已经实现了这个很棒的功能。如果最近发布的速度保持不变,该编译器将于2013年12月或2014年1月发布。 更新:Clang 3.4编译器已于2014年1月6日发布,其中包含所述功能。

移动捕获的解决方法

下面是一个帮助人工移动捕获的辅助函数make_rref的实现。
#include <cassert>
#include <memory>
#include <utility>

template <typename T>
struct rref_impl
{
    rref_impl() = delete;
    rref_impl( T && x ) : x{std::move(x)} {}
    rref_impl( rref_impl & other )
        : x{std::move(other.x)}, isCopied{true}
    {
        assert( other.isCopied == false );
    }
    rref_impl( rref_impl && other )
        : x{std::move(other.x)}, isCopied{std::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> p{new 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捕获的另一个想法是使用函数capture()(其实现可以在下面找到),使用方式如下:
#include <cassert>
#include <memory>

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

这里lambda是一个函数对象(几乎是真正的lambda),它捕获了std::move(p)并传递给了capture()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 )
        : x{std::forward<T>(x)}, f{std::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
@RnMss 你指的是哪个功能?广义 lambda 捕获吗? - Ralph Tandetzky
@RnMss 要么使用moveCapture包装器将它们作为参数传递(这种方法在上面和由protobuffs的创建者开发的Capn'Proto库中使用),要么接受您需要支持它的编译器 :P - Christopher Tarquini
1
我不明白为什么要这样绕弯子,当我们可以写成这样:[&p]() mutable -> std::unique_ptr<int> { return std::move(p); }。它不是做同样的事情吗? - Nawaz
12
不,实际上它们并不是同一件事。例如:您想要生成一个使用move-capture独占指针的Lambda线程。生成线程的函数可能会返回,而unique_ptr在函数对象执行之前就超出了其作用域。因此,您将得到一个悬空的unique_ptr引用。欢迎来到未定义行为的世界。 - Ralph Tandetzky
显示剩余5条评论

82

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

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

4
你检查过代码是否可以编译吗?在我看来似乎不行,因为首先变量名缺失,其次unique_ptr的右值引用不能绑定到int * - Ralph Tandetzky
9
请注意,Visual Studio 2013中将std::bind转换为std::function仍会导致所有绑定变量(在此示例中为“myPointer”)被复制。因此,上述代码在VS2013中无法编译通过。但在GCC 4.8中可以正常工作。 - Alan

27

您可以使用std::bind来实现大部分所需功能,例如:

std::unique_ptr<int> myPointer(new int{42});

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

这里的诀窍是,我们不是在捕获列表中捕获您的移动对象,而是将其作为参数,并通过std::bind进行部分应用,使其消失。请注意,lambda使用引用进行传递,因为它实际上存储在绑定对象中。我还添加了一些代码,用于写入实际可移动对象,因为这是您可能想要做的事情。
在C++14中,您可以使用广义lambda捕获来实现相同的目的,代码如下:
std::unique_ptr<int> myPointer(new int{42});

auto lambda = [myPointerCapture = std::move(myPointer)]() mutable {
    *myPointerCapture = 56;
    myPointerCapture.reset(new int{237});
};

但是,这段代码并没有为您提供C++11中通过std::bind获得的任何东西。 (在某些情况下,广义lambda捕获更强大,但不适用于此情况。)
现在只有一个问题; 您想将此函数放入std::function中,但该类要求函数为CopyConstructible,但它不是,它仅为MoveConstructible,因为它存储了一个不可CopyConstructiblestd::unique_ptr
你可以通过使用包装类和另一层间接来解决问题,但也许你根本不需要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>::type{std::forward<F>(f)}};
}


int main()
{
    std::unique_ptr<size_t> myPointer(new size_t{42});
    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()> f{std::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()> f{make_shared_fn(std::move(lambda))};
    std::cout << "#4: f()\n";
    f();
    std::cout << "#5: f()\n";
    f();
    std::cout << "#6: f()\n";
    f();
#endif
}

我已经将上述程序放在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中。

希望这能帮到您!


7
晚些了,但有些人(包括我在内)仍然停留在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.

如果极其罕见的情况出现,必须要“移动”指针(例如,由于删除时间较长或性能绝对关键,您想在单独的线程中显式删除指针),这几乎是我仍然使用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;

是的,现在原始指针(raw pointers)相当不受欢迎(也不无道理),但我真的认为在这些罕见的(而且暂时的!)情况下,它们是最好的解决方案。


谢谢,使用C++14,其他解决方案都不好。救了我的一天! - Yoav Sternberg
原始指针技巧是使用 std::function 的完美解决方法。 - Matt Eding

3

这似乎在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;
}

2
我看了这些答案,但是我发现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()()
预期输出:
value: 1
object still not deleted
value: 1
Destroying 000000DFDD172280
object was deleted
好的,指针地址可能会有所不同。 ;) 演示

1
Lambdas只是匿名函数对象的语法糖。你可以声明一个函数局部的struct,并提供一个operator()成员函数:
void someFunction(std::unique_ptr<Foo> foo)
{
    struct Anonymous
    {
        std::unique_ptr<Foo> ptr;

        void operator()()
        {
            ptr->doSomething();
        }
    };

    std::function<void(void)> f = Anonymous{std::move(foo)};
}

为了捕获更多的变量,只需向结构体添加更多的成员变量,并在实例化时进行初始化。
我知道这样很啰嗦,但这是一种可行的替代方案,在C++11中可以工作,而不需要使用大量的黑客技巧、元编程或std::bind。
这种方法的一个变体是使用模板来代替通用lambda表达式,但是C++11不允许在函数局部结构体中使用成员函数模板,所以结构体必须在类、命名空间或全局范围内声明。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接