19

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?

Const
  • 1,306
  • 1
  • 10
  • 26
aafulei
  • 2,085
  • 12
  • 27
  • similar for `std::thread` and the bound arguments: https://stackoverflow.com/questions/59601840/why-arguments-moved-twice-when-constructing-stdthread – underscore_d Jun 26 '20 at 09:15
  • Dupe candidate from ~2 weeks ago: [c++ When/Why are variables captured by value constructed/destroyed](https://stackoverflow.com/questions/62327044/c-when-why-are-variables-captured-by-value-constructed-destroyed/). – dfrib Jun 26 '20 at 09:23
  • @dfri I don't think so. The capturing by value here involves only copy constructor. – Daniel Langr Jun 26 '20 at 09:26
  • 3
    @underscore_d That's a very different case, since the constructor of `std::therad` passes its arguments **by (forwarding) references**, while the constructor of `std::function` passes its argument **by value**. – Daniel Langr Jun 26 '20 at 09:28
  • Possible duplicate? https://stackoverflow.com/questions/10008503/is-stdfunction-allowed-to-move-its-arguments – Tony Tannous Jun 26 '20 at 09:35

2 Answers2

12

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

Daniel Langr
  • 22,196
  • 3
  • 50
  • 93
  • Hi thank you for updating your answer to make it clearer. I am still feeling a little confused. To make it even clearer, can you explain a little bit more on how the `std::function` in my case is connected to the `template< class F > void function( F f )` in your case? In particular, can you do a listing like : `move #1: from xxx to yyy` and `move #2: from yyy to zzz`? – aafulei Jun 26 '20 at 09:34
  • My understanding is that the `std::function` constructor is taking the form of `function( function&& other );` as said in [cppreference](https://en.cppreference.com/w/cpp/utility/functional/function/function). I infer from your answer that you mean the `function( function&& other )` constructor, after taking the argument by rvalue reference, passes this argument by value in its implementation. If so, this should be the first move. Then `std::function` needs to store the object in the heap, so that there is a second move in passing the object to `new`. Am I right in understanding you? – aafulei Jun 26 '20 at 09:56
  • 2
    @aafulei, it's not calling `function( function&& other );`. We are not calling the `move constructor`, since we are not constructing from the *function* object itself. The construction is from the callable *F*, i.e. `void function( F f )` (as written in the explanation.). – Mikael H Jun 26 '20 at 10:21
  • @aafulei Exactly, `function( function&& other );` represents a move constructor of `std::function` itself. We are not moving any `std::function` objects anywhere here. – Daniel Langr Jun 26 '20 at 10:25
  • Thank you I get you immediately. Made a simple mistake but didn't realize until just now. – aafulei Jun 26 '20 at 10:26
  • 1
    @aafulei As for your first question, the **first move** happens when lambda (defined in `main` is moved to the parameter of `std::function` constructor. The, the **second move** moves from that parameter into a storage managed by `std::function` object (either dynamically allocated or some fixed-size buffer for small function objects). – Daniel Langr Jun 26 '20 at 10:27
  • I am hereby correcting my above observation. The constructor version for `std::function` should be exactly `template< class F > function( F f );` instead of `function( function&& other )` which is totally unrelated in this case. – aafulei Jun 26 '20 at 10:28
4

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_

Const
  • 1,306
  • 1
  • 10
  • 26
  • 3
    This answer is clearer because it is the usual stdlib idiom to take arguments by value and move in if possible. The other answer implies they take by rvalue reference, which they don't. – underscore_d Jun 26 '20 at 09:16
  • @underscore_d, do you know *why* they do not have a constructor for temporary objects? I get copy and move is good enough. But since we are paying for an unnecessary move, why not just have a constructor for temporary objects as well? – Mikael H Jun 26 '20 at 10:05
  • And if I understand the explanation correctly, the "copy" is incorrectly pointed out. std::function<> in this case creates T with move semantics (because of std::move()). The extra copy comes from the initialization of the lambda - foo is taken by *copy*. You can see this by removing the std::function - a copy is still output. – Mikael H Jun 26 '20 at 10:10
  • 1
    @MikaelH I guess it is a combo of difficulty/tedium to implement, versus likely real-world utility in terms of number of users who care vs. lines of code added... versus the fact that no one has yet proposed to resolve those in a way the Committee liked. Threads like [this](https://stackoverflow.com/questions/38357159/why-is-templated-functor-passed-as-value-and-not-forwarding-reference) seem like a good summary of counterarguments. – underscore_d Jun 26 '20 at 10:13