如何确定捕获了不可复制参数的 lambda 的类型?

3

鉴于下面的不可复制Task类和示例代码

#include <functional>
#include <iostream>
#include <string>

class Task
{
public:
    Task()
    {
    }

    Task(const Task& other) = delete;
    Task& operator=(const Task& other) = delete;

    Task(Task&& other) = default;
    Task& operator=(Task&& other) = default;

    void operator()() const
    {
        std::cout << "Task !" << std::endl;
    }
};


int main()
{  
    auto task = Task();

    auto lambda = [task = std::move(task)]
    {
        task();
    };

    std::function<void()> test = std::move(lambda);

    test();
}

如果我使用auto类型而不是std::function来声明test变量,那么程序会编译并运行得很完美。否则,它将拒绝编译并出现以下错误:

functional:1878:34: error: use of deleted function 'main()::<lambda()>::<lambda>(const main()::<lambda()>&)'
    __dest._M_access<_Functor*>() =
                                  ^
31:42: note: 'main()::<lambda()>::<lambda>(const main()::<lambda()>&)' is implicitly deleted because the default definition would be ill-formed:
31:42: error: use of deleted function 'Task::Task(const Task&)'
13:5: note: declared here

我真的需要声明测试的类型,因为它最终将成为另一个类的成员。

我该怎么做?

我是否正确地认为,std::function 应该以某种方式声明为可变的?


太多该死的标签页打开了 :( - NathanOliver
@NathanOliver 啊,太好了。已将其添加到重复列表中。 - cdhowie
1
@dgmz 更直接地回答你的问题,每个 Lambda 都有自己的匿名类型。但是,你可以使用 decltype(lambda) test = std::move(lambda); 来引用该类型。 - jtbandes
我已经重新打开了这个问题,因为建议的重复并没有回答实际的问题。 - SergeyA
@cdhowie 尽管如此,从问题到重复还有很长的路要走,而且链接对我来说并不是显而易见的。我认为,一个适当的答案可能是必要的。 - SergeyA
显示剩余7条评论
2个回答

4

当您想引用foo的类型时,可以使用decltype(foo)作为类型。因此,您可以这样做:

decltype(lambda) test = std::move(lambda);

然而,你所说的目标是将其用作类成员。在这种情况下,你需要从某个地方“窃取”类型。请注意,编译器没有义务(据我所知)统一两个相同lambda表达式的类型。这意味着类型和lambda创建必须来自同一个lambda表达式。
如果你真的想使用lambda,并且你可以访问C++14(用于推导返回类型),那么你可以这样做:
auto make_task_runner(Task task) {
    return [task = std::move(task)]() { task(); };
}

这为我们提供了一个函数,我们可以用它来创建lambda,也可以通过在函数调用上使用decltype()来窃取类型。

然后,在您的类中,您可以拥有:

class SomeClass {
    // Type alias just to make things simpler.
    using task_runner_t = decltype(make_task_runner(std::declval<Task>()));

    task_runner_t task_runner;
}

您可以使用make_task_runner函数将数据成员分配给它:
task_runner = make_task_runner(std::move(some_task));

然而,此时您已经失去了lambda的主要优势:即能够即时创建新的短暂、无名函数。现在我们有一个命名函数来创建lambda对象,并且我们已经给lambda类型命名(task_runner_t),那么使用lambda解决这个问题还有什么意义呢?
在这种情况下,自定义的functor(如Paul's answer中所述)更加合理。
然而,Task 已经是一个functor,因此您已经拥有所需的类型:Task!只需使用它,而不需要为了没有明显好处而发明一个包装器。

现在这才是一个答案! - SergeyA
1
谢谢您的回答。尽管在我的情况下,测试成员是一个类的一部分,而task_runner_t最终会成为构造函数的模板(而不是类)。 但确实,您正在回答这个确切的问题,我已经找到了一种方法,使用唯一指针存储封装函数对象的类中的测试成员,因此现在我没有任何问题可以存储函数对象! - dgmz

2
一种处理这个问题的方法是放弃lambda提供的语法糖,而是使用一个functor来手动实现,例如:
#include <functional>
#include <iostream>
#include <string>

class Task
{
public:
    Task()
    {
    }

    Task(const Task& other) = delete;
    Task& operator=(const Task& other) = delete;

    Task(Task&& other) = default;
    Task& operator=(Task&& other) = default;

    void operator()() const
    {
        std::cout << "Task !" << std::endl;
    }
};

class pseudo_lambda
{
public:
    pseudo_lambda (Task &&task) { m_task = std::move (task); }  // <- capture
    void operator()() const { m_task (); }                      // <- invoke
private:
    Task m_task;                                                // <- captured variable(s)
};

int main()
{  
    auto task = Task();
    pseudo_lambda pl { std::move (task) };
    pl ();
}

演示实况


谢谢你的回答。我理解了你的解决方案,这使我们能够自己确定类型。但在我的情况下,为我拥有的每种任务创建一个伪 lambda 类会非常昂贵(不同的捕获参数等)。 - dgmz
我会说这很繁琐,而不是昂贵的。没有运行时开销。Lambda只是我发布的代码的装饰品。 - Paul Sanders
通常来说,繁琐意味着耗费时间,不仅是我的时间,还有其他接替我来维护代码库的人的时间。而且,由于时间就是金钱,至少对于我的雇主来说,我坚持使用这个词的选择 ;) - dgmz

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