8

I have a trivial class that has a constructor that looks like:

Event(std::function<void()> &&f) : m_f(std::move(f)) { }

The constructor can be used with std::bind:

Thing thing;
std::unique_ptr<Event> ev(new Event(std::bind(some_func,thing)));

Using it in the manner above results in one copy construction of 'thing', then a move construction on that copy.

However, doing the following:

std::unique_ptr<Event> ev = make_unique<Event>(std::bind(some_func,thing));

Results in two move constructions. My questions are:

  • When exactly is the move constructor on 'thing' called
  • Why is it called twice with make_unique?

Here's the minimal example:

#include <iostream>
#include <memory>
#include <functional>
using namespace std;

class Thing
{
public:
    Thing() : x(0)
    {

    }

    Thing(Thing const &other)
    {
        this->x = other.x;
        std::cout << "Copy constructed Thing!\n";
    }

    Thing(Thing &&other)
    {
        this->x = other.x;
        std::cout << "Move constructed Thing!\n";
    }

    Thing & operator = (Thing const &other)
    {
        this->x = other.x;
        std::cout << "Copied Thing!\n";
        return (*this);
    }

    Thing & operator = (Thing && other)
    {
        this->x = other.x;
        std::cout << "Moved Thing!\n";
        return (*this);
    }

    int x;
};

class Event
{
public:
    Event() { }
    Event(function<void()> && f) : m_f(std::move(f)) { }
    void SetF(function<void()> && f) { m_f = std::move(f); }

private:
    function<void()> m_f;
};

int main() {

    auto lambda = [](Thing &thing) { std::cout << thing.x << "\n"; };

    Thing thing;
    std::cout << "without unique_ptr: \n";
    Event ev(std::bind(lambda,thing));
    std::cout << "\n";

    std::cout << "with unique_ptr, no make_unique\n";
    unique_ptr<Event> ev_p(new Event(std::bind(lambda,thing)));
    std::cout << "\n";

    std::cout << "with make_unique: \n";
    auto ev_ptr = make_unique<Event>(std::bind(lambda,thing));
    std::cout << "\n";

    std::cout << "with SetF: \n";
    ev_ptr.reset(nullptr);
    ev_ptr = make_unique<Event>();
    ev_ptr->SetF(std::bind(lambda,thing));
    std::cout << "\n";

    return 0;
}

With output:

g++ -std=c++14 -O2 -Wall -pedantic -pthread main.cpp && ./a.out
or
clang++ -std=c++14 -O2 -Wall -pedantic -pthread main.cpp && ./a.out

without unique_ptr: 
Copy constructed Thing!
Move constructed Thing!

with unique_ptr, no make_unique
Copy constructed Thing!
Move constructed Thing!

with make_unique: 
Copy constructed Thing!
Move constructed Thing!
Move constructed Thing!

with SetF: 
Copy constructed Thing!
Move constructed Thing!

PS: I tagged this question with C++11 as well as 14 because the same issue occurs when passing the C++11 flag to gcc with the commonly used make_unique function found here (make_unique and perfect forwarding)

Community
  • 1
  • 1
Prismatic
  • 3,338
  • 5
  • 36
  • 59
  • 2
    It's possible that the `Thing` is embedded _inside_ the `std::function`, in which case, `m_f(std::move(f))` would move the `Thing` too. How big is `sizeof(Thing)` compared to `sizeof(std::function)`? – Mooing Duck Jan 14 '15 at 21:25
  • It should be embedded in the function; bind takes 'thing' by value. Also sizeof(std::function) or f in Event is 32 and sizeof(Thing) is 4 – Prismatic Jan 14 '15 at 21:47
  • 1
    I think @dyp's got it - the conversion of the `bind` object to `std::function` happens inside `make_unique`, and so the move-initialization of the `function` constructor parameter cannot be elided as it can in the non-`make_unique` case. – Casey Jan 14 '15 at 22:19

1 Answers1

9

I think the additional move when using make_unique is due to move elision in Event(std::bind(lambda,thing)).

The constructor of Event that is called is Event(function<void()> && f), so a temporary function<void()> has to be created. This temporary is initialized with the return value of the std::bind expression.

The constructor used to perform this conversion from the return type of std::bind to std::function<void()> takes the argument by value:

template<class F> function(F f); // ctor

This means we have to move the return value of std::bind to this parameter f of the constructor of function<void()>. But that move is eligible for move elision.

When we pass that temporary through make_unique, it has been bound to a reference and move elision may not be applied any more. If we therefore suppress move elision:

std::cout << "with unique_ptr, no make_unique\n";
unique_ptr<Event> ev_p(new Event(suppress_elision(std::bind(lambda,thing))));
std::cout << "\n";

std::cout << "with make_unique: \n";
auto ev_ptr = make_unique<Event>(suppress_elision(std::bind(lambda,thing)));
std::cout << "\n";

(We can use std::move as the implementation of suppress_elision.)

We can observe the same number of moves: Live example


Explaining the whole set of operations:

For new Event(std::bind(lambda,thing)):

operation                                             | behaviour 
------------------------------------------------------+----------
`thing` variable        ->  `bind` temporary          | copies
`bind` temporary        ->  `function` ctor param     | moves (*)
`function` ctor param   ->  `function` object (temp)  | moves
`function` temporary    ->  `Event` ctor ref param    | -
`Event` ctor ref param  ->  `function` data member    | *can* move (+)

(*) can be elided
(+) but doesn't, probably because the internal function object is on the heap, and only a pointer is moved. Verify by replacing m_f(std::move(f)) with m_f().

For make_unique<Event>(std::bind(lambda,thing)):

operation                                               | behaviour 
--------------------------------------------------------+----------
`thing` variable         ->  `bind` temporary           | copies
`bind` temporary         ->  `make_unique` ref param    | -
`make_unique` ref param  ->  `function` ctor param      | moves
`function` ctor param    ->  `function` object (temp)   | moves
`function` temporary     ->  `Event` ctor ref param     | -
`Event` ctor ref param   ->  `function` data member     | *can* move (+)
dyp
  • 38,334
  • 13
  • 112
  • 177
  • 1
    `move(std::bind(...))`? I'm fairly certain the return value of `std::bind` is already as rvalue as it can be ;) Oh, nevermind - you're using the cast to suppress elision. I'm an idiot. – Casey Jan 14 '15 at 22:21
  • awesome explanation and chart of the sequence of events – Prismatic Jan 14 '15 at 22:29
  • hmm... why do you think its incorrect? should I wait for another answer then? – Prismatic Jan 14 '15 at 22:35
  • @Pris I forgot that you need to move from the *by value* ctor parameter of `std::function` into the `std::function` object itself (or into the part it manages on the heap). This implies that the last move is not actually happening, as @MooingDuck suggested. – dyp Jan 14 '15 at 22:38