从具有按值捕获的 lambda 移动构造 std::function 时调用两次移动构造函数

Move constructor called twice when move-constructing a std::function from a lambda that has by-value captures

当从 lambda 移动构造一个 std::function 对象时,其中 lambda 具有按值捕获,看起来对象的移动构造函数is value-captured 被调用了两次。考虑

#include <functional>
#include <iostream>

struct Foo
{
    int value = 1;

    Foo() = default;

    Foo(const Foo &) {}

    Foo(Foo &&)
    {
        std::cout << "move ctor" << std::endl;
    }
};

int main()
{
    Foo foo;
    auto lambda = [=]() { return foo.value; };
    std::cout << "---------" <<  std::endl;
    std::function<int()> func(std::move(lambda));
    std::cout << "---------" <<  std::endl;
    return 0;
}

输出为

---------
move ctor
move ctor
---------

我在 Mac OS X Catalina 上工作,我的编译器是

g++-9 (Homebrew GCC 9.3.0) 9.3.0

我用g++ -std=c++17编译。

我想这种行为可能在某种程度上依赖于编译器实现,但我仍然对这种机制感到好奇。

有人可以解释为什么移动构造函数被调用两次以及那里到底发生了什么吗?

我认为,这是因为 std::function 移动构造了它的参数 T(这里是 lambda)。

这个可以看出来,简单的把std::function换成一个简单的版本就可以了

#include <iostream>

struct Foo
{
   int value = 1;
   Foo() = default;
   Foo(const Foo&) { std::cout << "Foo: copy ctor" << std::endl; }
   Foo(Foo&&)
   {
      std::cout << "Foo: move ctor" << std::endl;
   }
};


template<typename T>
class MyFunction
{
   T mCallable;

public:
   explicit MyFunction(T func)
      //  if mCallable{ func}, it is copy constructor which has been called
      : mCallable{ std::move(func) }  
   {}
};

int main()
{
   Foo foo;
   auto lambda = [=]() { return foo.value; };
   std::cout << "---------" << std::endl;
   MyFunction<decltype(lambda)> func(std::move(lambda));
   std::cout << "---------" << std::endl;
   return 0;
}

输出:

Foo: copy ctor    
---------    
Foo: move ctor    
Foo: move ctor    
---------

如果不移动构造,它将复制参数,这反过来也复制捕获变量。看这里:https://godbolt.org/z/yyDQg_

这是由 std::function 的实现方式引起的。考虑以下更简单的示例:

struct Lambda
{
  Lambda() = default;
  Lambda(const Lambda&) { std::cout << "C"; }
  Lambda(Lambda&&) { std::cout << "M"; }
  void operator()() const { }
};

int main()
{
  auto lambda = Lambda();
  std::function<void()> func(std::move(lambda));    
}

它打印出MM,因此,Lambda的移动构造函数在将其实例存储到std::function时调用了两次。

现场演示:https://godbolt.org/z/XihNdC

在你的例子中,Foo 那个 lambda 的成员变量(按值捕获)被移动了两次,因为 整个 lambda 被移动了两次。请注意,捕获本身不会调用任何移动构造函数,而是调用复制构造函数。


为什么 std::function 的构造函数移动参数两次?请注意,此 构造函数通过值 传递其参数,然后,它 在内部需要存储该对象 。它可以使用以下函数进行模拟:

template< class F >
void function( F f )
{
    F* ptr = new F(std::move(f));
    delete ptr;
}

此代码:

  auto lambda = Lambda();
  function(std::move(lambda));

然后执行两个动作

现场演示:https://godbolt.org/z/qZvVWA