【问题标题】:从具有按值捕获的 lambda 移动构造 std::function 时调用了两次移动构造函数
【发布时间】:2020-10-16 20:27:38
【问题描述】:

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

#include 
#include 

结构 Foo
{
    整数值 = 1;

    Foo() = 默认值;

    Foo(const Foo &) {}

    富(富 &&)
    {
        std::cout  func(std::move(lambda));
    std::cout 

输出是

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

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

g++-9 (Homebrew GCC 9.3.0) 9.3.0

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

我猜这种行为可能有点依赖于编译器实现,但我仍然对这种机制感到好奇。

谁能解释一下为什么移动构造函数被调用了两次以及那里到底发生了什么?

【问题讨论】:

标签: c++ lambda c++17 move-semantics std-function


【解决方案1】:

我认为,这是因为std::function move 构造了它的参数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    
---------

如果不是 move 构造,它将复制参数,进而复制捕获变量。见这里:https://godbolt.org/z/yyDQg_

【讨论】:

  • 这个答案更清楚,因为通常的 stdlib 习惯用法是按值获取参数并尽可能移入。另一个答案暗示他们采用右值引用,而他们没有。
  • @underscore_d,你知道为什么他们没有临时对象的构造函数吗?我得到副本和移动就足够了。但既然我们为不必要的移动付出了代价,为什么不也为临时对象创建一个构造函数呢?
  • 如果我理解正确的解释,“副本”被错误地指出。在这种情况下,std::function 创建具有移动语义的 T(因为 std::move())。额外的副本来自 lambda 的初始化 - foo 由 copy 获取。您可以通过删除 std::function 看到这一点 - 仍然输出副本。
  • @MikaelH 我想这是实现难度/乏味的组合,而就关心的用户数量和添加的代码行而言,它可能是现实世界的实用工具......而不是事实尚未有人提议以委员会喜欢的方式解决这些问题。 this 之类的主题似乎是反驳论点的一个很好的总结。
【解决方案2】:

这是由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

在您的情况下,该 lambda 的 Foo 成员变量(由值捕获)移动了两次,因为 整个 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

【讨论】:

  • 您好,感谢您更新答案以使其更清晰。我仍然感到有些困惑。为了更清楚,你能解释一下我的std::function是如何连接到你的template&lt; class F &gt; void function( F f )的吗?特别是,你能做一个像move #1: from xxx to yyymove #2: from yyy to zzz这样的列表吗?
  • 我的理解是std::function 构造函数采用function( function&amp;&amp; other ); 的形式,如cppreference 中所述。我从您的回答中推断出您的意思是 function( function&amp;&amp; other ) 构造函数,在通过右值引用获取参数后,在其实现中按值传递此参数。如果是这样,这应该是第一步。然后std::function 需要将对象存储在堆中,以便将对象传递给new 有第二步。我对你的理解正确吗?
  • @aafulei,它没有调用function( function&amp;&amp; other );。我们没有调用move constructor,因为我们不是从 function 对象本身构造的。构造来自可调用的F,即void function( F f )(如解释中所述)。
  • @aafulei 没错,function( function&amp;&amp; other ); 表示 std::function 本身的移动构造函数。我们不会在此处移动任何std::function 对象。
  • @aafulei 至于你的第一个问题,first move发生在lambda(在main中定义)被移动到std::function构造函数的参数时。第二次移动 从该参数移动到由std::function 对象管理的存储空间(动态分配的或用于小型函数对象的一些固定大小的缓冲区)。
猜你喜欢
  • 1970-01-01
  • 2015-01-17
  • 2011-05-22
  • 2016-03-06
  • 2023-04-08
  • 2020-04-23
  • 2011-11-25
  • 2015-03-16
相关资源
最近更新 更多