Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does make_unique have an extra move with a constructor that can take std::bind as an argument?

Tags:

c++

c++11

c++14

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)

like image 218
Prismatic Avatar asked Jan 14 '15 21:01

Prismatic


1 Answers

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 (+)
like image 134
dyp Avatar answered Sep 19 '22 04:09

dyp