14

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.

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
C.M.
  • 3,071
  • 1
  • 14
  • 33

1 Answers1

11

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.

Vaughn Cato
  • 63,448
  • 5
  • 82
  • 132
Barry
  • 286,269
  • 29
  • 621
  • 977
  • There may be a tiny marginal benefit to having `&`, `const&` and `&&` overloads to your `operator result`. Note that there is a small flaw in this, in that a greedy ctor to result could consume factory without giving the conversion operator a chance, but on aggregates that isn't a problem. – Yakk - Adam Nevraumont Jul 27 '17 at 14:16
  • `invoke_result_t` (or `return std::move(f)();`, your pick.) – T.C. Jul 27 '17 at 15:05
  • @Barry -- marvelous! Besides that little potential conflict between type's ctor and factory's operator (which can be easily worked around with magic ctor) I can't find any fault with this solution :). – C.M. Jul 28 '17 at 06:58
  • @T.C. Can you elaborate a bit? – C.M. Jul 28 '17 at 07:07
  • Is it possible to use `decltype(auto)` in conversion operator? – Tomilov Anatoliy Jul 29 '17 at 04:58
  • @Orient Yes it is. – Barry Jul 29 '17 at 14:08
  • @Barry Maybe is it more expressive in the case above? If it is equivalent to `invoke_result_t< F & >` – Tomilov Anatoliy Jul 29 '17 at 17:07
  • @Barry Can you explain what T.C. meant in his comment? Why? – C.M. Aug 04 '17 at 02:13
  • 1
    @C.M. `invoke_result_t` is the result of invoking an rvalue of type `F` with no arguments. But the body is `f()`, which is invoking an lvalue. Hence, either fix the return type (to use `F&`) or fix the body to use an rvalue (`std::move(f)()`). – Barry Dec 16 '17 at 01:03
  • @Barry deduction guide doesn't work with MSVC2017 and GCC8.2 -- looks like it wants a ctor in `struct factory` :/ – C.M. May 31 '19 at 17:32
  • @C.M. I don't know which version 2017 is, but it does work on [gcc 8.2 and msvc 19.14](https://godbolt.org/z/3S07bK)? – Barry May 31 '19 at 17:34
  • @Barry [Here is](https://godbolt.org/z/AVUfxg) a dried out code that refuses to compile for me. godbolt can't compile it either. As of now I still have no idea what is wrong with it. – C.M. May 31 '19 at 18:12
  • @C.M. Well, what's different between what you're doing and what my answer says? It's not the same. – Barry May 31 '19 at 18:16
  • @Barry It is precisely the same code you provided. I thought it simply doesn't work until you showed me your sample. Now I am trying to figure out why my code doesn't compile and (tbh) I am at a loss right now... Thought maybe you could see smth I missed – C.M. May 31 '19 at 18:22
  • oh, crap... I see it. Braces instead of parenthesis. Duh! – C.M. May 31 '19 at 18:23
  • @Barry Alas, all MSVC versions below 19.20 [can't handle this](https://stackoverflow.com/questions/62841404/looks-like-is-nothrow-constructible-v-is-broken-in-msvc-am-i-wrong). :\ – C.M. Jul 10 '20 at 21:10
  • @Barry my hands are sore from writing `data.emplace( inplacer{ [&]{ return ...; } } );` all over the code. We need `template inplace[_back](F&)` family of member functions in STL... :-/ – C.M. Oct 22 '20 at 19:00