Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Avoiding extra move in make_unique/make_shared/emplace/etc for structures that use aggregate initialization

Tags:

c++

c++17

std::make_unique() (and similar functions) have a little problem:

#include <cstdio>
#include <memory>

using namespace std;

struct S
{
    S()         { printf("ctor\n"); }
    ~S()        { printf("dtor\n"); }
    S(S const&) { printf("cctor\n"); }
    S(S&&)      { printf("mctor\n"); }
};

S foo() { return S(); }

int main()
{
    {
        printf("--------------- case 1 ---------------\n");
        unique_ptr<S> s1 = make_unique<S>( foo() );
    }

    {
        printf("--------------- case 2 ---------------\n");
        unique_ptr<S> s2 { new S( foo() ) };
    }
}

Output:

--------------- case 1 ---------------
ctor
mctor
dtor
dtor
--------------- case 2 ---------------
ctor
dtor

As you see we have an extra move that can be avoided. Same problem exists with emplace() in optional/variant/etc -- if object gets returned by other function, you have to move it.

This can be addressed with a trick:

#include <cstdio>
#include <optional>

using namespace std;

struct S
{
    S()         { printf("ctor\n"); }
    ~S()        { printf("dtor\n"); }
    S(S const&) { printf("cctor\n"); }
    S(S&&)      { printf("mctor\n"); }

    template<class F, enable_if_t<is_same_v<invoke_result_t<F>, S>>...>
    S(F&& f) : S(forward<F>(f)()) {}
};

S foo() { return S(); }

int main()
{
    optional<S> s;
    s.emplace( []{ return foo(); } );
}

This avoids unnecessary move (enable_if hides constructor unless f() returns an instance of S). You effectively end up constructing your values inside of std::variant/std::optional/etc via a call to your constructing function.

This fix has a little problem -- adding a constructor breaks aggregate initialization. See example. I.e. if given structure had no constructor and you add one -- you can no longer initialize it like this:

struct D
{
    float m;
    S s;

    // adding new constructor here will break existing bar() functions
};

D bar() { /*...lots of code with multiple return statements...*/ return {2.0, foo()}; }

Question: Is there a way around this problem? Something that doesn't introduce new constructors...

I'd like to be able to efficiently put my structures into optional/variant/shared_ptr-block/etc without breaking (rather non-trivial) code that creates them.


Edit: All MSVC versions can't handle exceptions escaping from Barry's factory. See details here.

like image 661
C.M. Avatar asked Jul 27 '17 08:07

C.M.


1 Answers

Instead of adding a constructor to your type that takes a factory function, instead create a new external factory object with a conversion operator to your type. With C++17, that takes minimal work:

template <class F>
struct factory {
    F f;

    operator invoke_result_t<F&>() { return f(); }
};

template <class F>
factory(F ) -> factory<F>;

For your earlier example, S doesn't need the constrained constructor anymore. You would instead do:

optional<S> s;
s.emplace( factory{[]{ return foo(); }} ); // or really just factory{foo}

Which prints just ctor and dtor. Since we're not modifying S in any way, we could use this in aggregates as well - like D.

like image 81
Barry Avatar answered Sep 19 '22 14:09

Barry