Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

When move-constructing a std::function object from a lambda, where that lambda has by-value captures, it appears that the move-constructor of the object that is value-captured is called twice. Consider

#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;
}

The output is

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

I work on Mac OS X Catalina and my compiler is

g++-9 (Homebrew GCC 9.3.0) 9.3.0

I compile with g++ -std=c++17.

I guess this behavior might be somewhat compiler-implementation-dependent, but I am still curious about the mechanism.

Can someone please explain why the move constructor was called twice and what really happened there?

like image 803
aafulei Avatar asked Jun 26 '20 08:06

aafulei


2 Answers

This is caused by how std::function is implemented. Consider the following much simpler example:

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));    
}

It prints out MM, therefore, move constructor of Lambda is invoked twice when storing its instance into std::function.

Live demo: https://godbolt.org/z/XihNdC

In your case, the Foo member variable of that lambda (captured by value) is moved twice since the whole lambda is moved twice. Note that the capturing itself does not invoke any move constructor, it invokes copy constructor instead.


Why the constructor of std::function moves the argument twice? Note that this constructor passes its argument by value, and then, it internally needs to store that object. It can be kind-of simulated with the following function:

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

This code:

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

then perform two moves.

Live demo: https://godbolt.org/z/qZvVWA

like image 162
Daniel Langr Avatar answered Sep 19 '22 09:09

Daniel Langr


I think, it is because the std::function move construct the its argument T(that is here lambda).

This can be seen, simply replacing the std::function with a simple version of it.

#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;
}

outputs:

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

if not move constructed, it will copy the arguments, which in turn, copies the captures variables too. See here: https://godbolt.org/z/yyDQg_

like image 26
Const Avatar answered Sep 20 '22 09:09

Const