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

Posted

技术标签:

【中文标题】如果 lambda 使用 std::move() 捕获不可复制的对象,为啥它是不可移动的?【英文标题】:Why is a lambda not-movable if it captures a not-copiable object using std::move()?如果 lambda 使用 std::move() 捕获不可复制的对象,为什么它是不可移动的? 【发布时间】:2018-06-08 18:03:44 【问题描述】:

这是我生成错误的示例代码:

#include <functional>
using namespace std;

struct S 
    S() = default;
    S(const S&) = delete;
    S(S&&) = default;
    S& operator=(const S&) = delete;
    S& operator=(S&&) = delete;
;

template <typename F>
void post(F&& func)

    function<void()> fforward<F>(func);


int main()

    S s;
    post([s2 = move(s)]  );

main() 的lambda 内部,我使用std::move() 捕获局部变量s。在调用 post() 之前,s2 必须已经移动构造成功。

但是,在 post() 内部,f 不能使用对此 lambda 类型的右值引用来构造。

如果我删除,s2 = move(s)f 可以用这个右值引用构造。

为什么添加s2 = move(s) 会导致 lambda 不可移动?

这里是link 尝试使用 coliru。

【问题讨论】:

@Lưu Vĩnh Phúc,请告诉我你为什么不喜欢我的问题。 是什么让你觉得我不喜欢它?我只是为语法高亮添加了一个标签 对不起,我误解了修订日志,并认为你给了问题-1。 还有一个事实是 S 有构造函数,它接受 S 实例而不是 S 引用。我必须删除那些才能到达 lambda。 @SornelHaetir,怎么样?可以和S(const S&amp;) = delete共存吗?它们是哪些? 【参考方案1】:

您的 lambda 不会因为移动捕获而变得不可移动。但它确实变得不可复制,这是一个问题。

std::function 不支持将提供的仿函数移动到自身中,它总是进行复制。因此,不可复制的 lambda(和其他可调用对象)不能与 std::function 一起使用。这个限制的原因是标准要求std::function是可复制的,如果用不可复制的可调用对象初始化则无法实现。

【讨论】:

我试过std::function&lt;void()&gt; f = []; auto f2 = move(f); f();f()std::bad_function_call。它是否反驳了您的说法“std::function 不支持将提供的函子移动到自身中,它总是复制。”? @BenjiMizrahi 不。您将 将仿函数移动到 std::function移动 std::function 对象本身混淆了。 前者不是可能,而后者完全没问题(实际上 移动 存储的仿函数)。前者不可能的原因是标准要求std::function 是可复制的,如果它可以从不可复制的函子构造,则无法实现。【参考方案2】:

问题不在于您的 lambda,而在于您的对象不可复制,因为 std::function 要求其对象是可复制的,编译器会抱怨。您几乎应该始终关注rule-of-zero。

一般:

lambda 既可以是可复制的,也可以是可移动的。 如果 lambda 具有不可复制的捕获,它会使 lambda 本身不可复制。这些对象可以包装在一个 smart_pointer 中,但可以在 lambda 捕获中移动(或复制 - shared_ptr)。 如果没有按值捕获,则闭包类型(lambda)通常可以轻松复制和轻松移动。 如果值对象捕获的所有对象都是可轻松复制且可轻松移动的非 const 类型(例如 C 类类型),则闭包类型将可轻松复制和轻松移动。 否则,如果存在按值捕获,则闭包类型的移动构造函数将复制按值捕获的对象。 如果有 const 对象的按值捕获,则捕获列表中的任何移动都会产生副本。 如果 lambda 本身是 const,它永远不会移动,只会复制,甚至复制到其他 const lambda。

示例:

#include <iostream>
#include <type_traits>

struct S

    S() 
        std::cout << "ctor" << '\n';
    
    ~S() noexcept 
        std::cout << "dtor" << '\n';
    
    S(const S&)  
        std::cout << "copy ctor\n";
    
    S(S&&) noexcept noexcept 
        std::cout << "move ctor\n";
    
    S& operator= (const S&) 
        std::cout << "copy aop\n";
    
    S& operator= (S&&) noexcept 
        std::cout << "move aop\n";
    
;

template <typename T>
void getTraits()

    std::cout << std::boolalpha
        << "trivially_copy_constructible? "
        << std::is_trivially_copy_constructible_v<T>
        << "\ntrivially_move_constructible? "
        << std::is_trivially_move_constructible_v<T> << '\n' ;


int main()

    S s ;
    const S cs;
    
        std::cout << "capture by value\n" ;
        //auto closure = [s = std::move(s)]  ; // S::move construct               // 1.
        //auto closure = [cs = std::move(cs)]  ; // S::copy construct             // 2.
        //const auto closure = [s = std::move(s)]  ; // S::move construct         // 3.
        const auto closure = [cs = std::move(cs)]  ; // S::copy construct         // 4.
        getTraits<decltype(closure)>();

        const auto copy_constructed = std::move(closure);
        const auto move_constructed = std::move(closure);
    

    
        std::cout << "\ncapture by reference\n";
        const auto closure = [&s] ;
        getTraits<decltype(closure)>();
    

一次取消注释 1、2、3、4 并检查输出。请记住 std::move 只是将对象转换为右值引用。

【讨论】:

以上是关于如果 lambda 使用 std::move() 捕获不可复制的对象,为啥它是不可移动的?的主要内容,如果未能解决你的问题,请参考以下文章

std::move与std::forward

std::move与std::forward

std::move与std::forward

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

为啥 std::move 不能与 std::list 一起使用

静态转换为右值引用和 std::move 之间有啥区别吗