首页 > 解决方案 > 如何移动包含和捕获 lambda 的 std::function?

问题描述

我正在尝试提出以下最有效(即最少数量的副本)的实现。我有一个队列,用于在稍后的某个时间点执行 std::function<void()> 对象。队列只能采用微不足道的数据类型,因此我需要将指针传递给 std::function 而不是 std::function 本身(即队列包含 std::function<void()>* 类型)。

理想情况下,如果在 lambda 中(按值)捕获了一条数据,则在创建 std::function 并将其添加到队列的整个过程中,应该只制作该数据的一个副本。

这是我一直在玩的一个例子:

std::function<void()>* invoke(const std::function<void()>& fn) {
    printf("invoke()\r\n");
    return new std::function<void()>(fn);
}

我会这样使用

class Base {
public:
    virtual void sayHello() const = 0;
};

class A : public Base {
public:
    A() {
        printf("A::A()\r\n");
    }
    A(const A& a) {
        printf("A copy constructor\r\n");
    }
    A(const A&& a) {
        printf("A const move constructor\r\n");
    }
    A(A&& a) {
        printf("A move constructor\r\n");
    }
    A& operator=(A&& other)
    {
        printf("A move assignment operator\r\n");
        return other;
    }

    void sayHello() const override { printf("A says hello\r\n"); }
};

int main() {
    A myA;
    
    printf("invoking lambda which calls myA.sayHello()\r\n");
    std::function<void()> *fn = invoke([myA](){
        myA.sayHello();
    });

    return 0;
}

由于 lambda 是按值捕获对象 (myA),因此在最初创建 lambda 时会制作一份副本。现在,由于您在 main() 中看到的 lambda 是“临时的”(仅在调用调用中使用),因此实际上应该只有一个 lambda 副本挂起,因此只有一个 myA 的(附加)副本。

但是,这是我的输出示例:

invoking lambda which calls myA.sayHello()
A copy constructor
A move constructor
invoke()
A copy constructor

看来,当我在堆上创建一个新的 std::function 时,它正在复制 lambda(以及 myA)而不是仅仅移动它,这是我想要的行为,因为原始 lambda 和 std::function (将 lambda 传递给调用函数时自动创建的那个)无论如何只是暂时的。

我觉得我很接近,但在这里误解了一些东西。有人可以帮忙吗?

谢谢!

编辑

基于这里的所有讨论,我对我的代码进行了一些修改。现在我将其升级为一个更复杂的示例,如下所示:

//Same A and Base classes as above

inline static void invoke(std::function<void()> fn) {
    printf("invoke(std::function<void()> fn)\r\n");

    std::function<void()>* newF = new std::function<void()>(std::move(fn));

    (*newF)();
    delete newF;
    printf("return\r\n");
}
int main() {
    printf("Starting Tests...\r\n");
    
    A myA;

    invoke([myA](){
        myA.sayHello();
    });
    return 0;
}

它具有以下意外输出:

Starting Tests...
        A::A()
        A copy constructor
        A move constructor
invoke(std::function<void()> fn)
        A says hello
        A::~A()
return
        A::~A()
        A::~A()

我不知道为什么要对析构函数进行第三次调用,因为应该只存在 A 对象的 2 个副本(main() 中的原始副本和 lambda 中的复制构造函数制作的副本)。

标签: c++lambdastd-function

解决方案


您的示例不起作用,因为您试图std::moveconst参考中获取。这将产生一个 type std::function<...> const&&,但 move 的构造函数std::function只接受std::function<...>&&(没有const)。这在 C++ 中并不罕见,因为const右值引用是该语言的一个奇怪的极端情况,在概念上没有多大意义。特别是,您不能合理地const从右值引用移动,因为不允许您对源进行任何更改。

如果目的是保证不会发生副本,则应通过右值引用接受参数:

std::function<void()>* invoke(std::function<void()>&& fn) {
    printf("invoke()\r\n");
    return new std::function<void()>(std::move(fn));
}

请注意,这将函数限制为仅使用右值参数。如果您更喜欢更灵活的设计,请考虑max66 的答案


推荐阅读