使用 Lambda 捕获类后无法找到合适的拷贝构造函数

3

我有以下代码:

#include <iostream>

class Foo
{public:
    Foo() {}
    int a;
};

int main()
{
    Foo foo;

    auto lambda = [=]() mutable { std::cout << foo.a; };

}

一切都很顺利,直到我需要为我的Foo类添加一个复制构造函数:

Foo(Foo& t) {}

它不再编译,并显示以下信息:

类'Foo': 没有可用的复制构造函数或已声明复制构造函数为“显式”

我将lambda设为可变,因为我不想捕获一个const Foo,但我认为发生的事情是lambda无法被复制。另一个编译器提供了更有用的错误消息:

错误:使用已删除的函数'main()::< lambda()>::< lambda>(main()::< lambda()>&&)'

和:

'main()::< lambda()>::< lambda>(main()::< lambda()>&&)'会被隐式删除,因为默认定义将是非法的:

但我真的不明白这是什么意思。那个被隐式删除的函数是lambda的移动构造函数吗?我不明白为什么只是为捕获的类(而不是lambda)添加一个复制构造函数就会发生这种情况。

这就是我对lambda/functor的看法:

class lambda
{public:

     Foo foo; // <---- My captured variable/class
     void operator()(){ std::cout << foo.a; }
}

那么将其中一个lambda表达式复制到另一个lambda表达式中需要调用Foo的赋值运算符或拷贝构造函数?我不明白仅Foo有一个拷贝构造函数是如何导致此失败的,或者什么是“不健全”的。我注意到的另一件事是,在lambda通过引用[&]捕获时没有问题。
编辑:在这个编译器上它不能编译:

https://www.jdoodle.com/online-compiler-c++/

我正在使用Visual Studio,但它无法编译。然而,当我创建一个更小的示例时,它可以编译,但仍会在错误下划线。在我的较大项目中,它无法编译。


1
您的代码(添加了“复制构造函数”)可以在MSVCclang-cl编译器中无错误编译(使用C++17标准)。您使用的是哪个编译器和标准? - Adrian Mole
3个回答

2
复制构造函数 的原型是 A(const A&)。您的复制构造函数缺少了 const 限定符,这就是出错的原因。

我一直在研究这个问题,复制构造函数可以是const或非const的,如果它接管了一个指针或其他东西,我相当确定它必须是非const的。参考链接:https://en.wikipedia.org/wiki/Copy_constructor_(C++)。编辑:在我的Foo复制构造函数中,我修改了另一个Foo,我复制了指针并将其设置为nullptr。 - Zebrafish
@Evg 我该如何复制另一个Foo的指针并将其设置为nullptr,如果它是const? - Zebrafish
1
@Zebrafish “我复制指针并将另一个设置为nullptr”听起来更像是移动而不是复制。 - Gavi Lock
1
@Zebrafish,如果您想在原始对象中修改指针,请使用移动构造函数,该函数接受一个右值引用(Obj&&)。 - Evg
你为什么要一开始就将 other 的指针设置为 nullptr?正如 @Evg 所述,复制构造函数只是复制,不会影响 other 变量。 - Thomas Caissard
显示剩余2条评论

1
auto lambda = [=]() mutable { std::cout << foo.a; };

在右侧,您创建临时闭包。基于这个临时闭包,通过调用编译器生成的默认移动构造函数来构建另一个闭包。
closure c(closure{});

move构造函数的默认实现只是逐一移动所有数据成员:

struct closure {
    Foo foo;

    closure (closure&& theOther) : foo(std::move(theOther.foo))   // <--- [1]
    {}                              // binding rvalue ref to lvalue ref
};

你的Foo构造函数接受Foo&,但不允许将rvalue引用绑定到lvalue引用。

这在MSVC中可以工作,因为它有一个扩展来处理这样的事情。在G++/Clang下,它必须失败。

使用const Foo&,可以正常工作,因为临时对象可以绑定到const lvalue引用。


哇,那个插图真的帮了我很多。我有一个类,它持有一个指针,当我将一个指针赋值给另一个时,新的指针应该拥有这个指针。但是如果我创建移动构造函数Foo(Foo&& other),那么我就不能使用Foo foo1; Foo foo2 = foo1;,因为foo1不是右值。我有一个返回Foo的函数,像这样:Foo foo = createNewFoo(); 那么我应该从这个函数中返回什么? - Zebrafish
在第一种情况下,为什么不使用Foo foo2(std::move(foo1));。关于第二个问题,函数只返回Foo,新的实例将通过移动构造函数构造,然后它将拥有指针的所有权。 - rafix07
如果我没记错的话,C++标准版本在这里扮演了重要角色。在C++17下,代码是有效的,在C++11/14下则不是。我在我的答案中给出了解释。 - Evg

1
要获得编译错误,您需要在C++11或C++14标准下编译此代码。在C++17中是有效的。演示
让我们考虑
struct Foo {
    Foo() {}
    Foo(Foo&) {}
};

在C++17中,我们可以编写以下代码:

auto f = Foo{};

但在C++11/14中,这一行将无法编译。原因是在C++17中我们有了强制复制省略,而复制构造函数的调用正确性将被判定为不合规,因为Foo&无法绑定到临时对象Foo{},并且Foo(Foo&&)被删除,甚至不会被编译器检查。
这直接转化为lambda表达式(rafix07的答案解释了如何实现),因为它通过值捕获了Foo。lambda表达式本身没问题。例如,你可以写:
[=] { std::cout << foo.a; };

但是lambda的移动构造函数是不合法的,在C++11/14中,它必须是合法的,以便在这一行中使用。
auto lambda = [=] { std::cout << foo.a; };

编译。


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