c++c++17explicitcopy-elisionstdoptional

`std::optional` factory function with guaranteed copy elision and `private` constructor, without passkey idiom


Consider the following pattern:

class Widget
{
private:
    explicit Widget(Dependency&&);

public:
    static std::optional<Widget> make(std::filesystem::path p)
    {
        std::optional<Dependency> d = loadDependencyFromPath(p);
        if (!d.has_value()) { return std::nullopt; }
        
        return Widget{std::move(*d)};
    }
};

It works, and I like the fact that Widget::Widget(Dependency&&) is private and explicit, and that users have to go through the static factory make function.

However, the make function is not eligible for C++17 guaranteed copy elision (RVO) due to the fact that we are not returning a std::optional<Widget> directly, but a Widget instead.

So, I tried changing make to:

static std::optional<Widget> make(std::filesystem::path p)
{
    std::optional<Dependency> d = loadDependencyFromPath(p);
    if (!d.has_value()) { return std::nullopt; }
    
    return std::make_optional<Widget>(std::move(*d));
}

This would make the function eligible for guaranteed copy elision (RVO), but it doesn't compile as Widget::Widget(Dependency&&) is private and explicit:

 error: no matching function for call to 'make_optional<Widget>(std::remove_reference<Dependency&>::type)'
   19 |         return std::make_optional<Widget>(std::move(*d));
      |                ~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~

live example on Compiler Explorer

Using return std::optional<Widget>(std::in_place, std::move(*d)) results in the same compilation error.

I thought that befriending std::optional via friend std::optional<Widget> would do the trick, but it still fails to compile.

I am aware that the passkey idiom can be used as a workaround, but I don't find it acceptable as it requires making the constructor public and adding a potentially confusing key<T> parameter -- both of these things negatively impact the library's end user experience.


How can I construct a std::optional<T> in place from a factory function keeping guaranteed copy elision (RVO) eligibility while T has a private and explicit constructor, without using the passkey idiom?


Solution

  • I don't really buy the claim that the pass-key idiom negatively impacts user-experience in any way... but you can create a local type that is convertible to Widget and construct from that instead:

    static std::optional<Widget> make(std::filesystem::path p)
    {
        std::optional<Dependency> d = loadDependencyFromPath(p);
        if (!d.has_value()) { return std::nullopt; }
    
        struct X {
            Dependency&& d;
            operator Widget() && { return Widget(std::move(d)); }
        };
        
        return std::optional<Widget>(X{std::move(*d)});
    }
    

    Or:

    
    static std::optional<Widget> make(std::filesystem::path p)
    {
        struct X {
            Dependency&& d;
            operator Widget() && { return Widget(std::move(d)); }
        };
    
        return loadDependencyFromPath(p).transform([](Dependency&& d){
            return X{std::move(d)};
        });
    }
    

    This uses the same private constructor of Widget but in a context where we have access to it. Because the conversion function here is still returning a prvalue you should probably still get guaranteed copy elision on the construction of the internal Widget of optional<Widget>.


    Incidentally, this is why the emplace API that proliferates the standard library where we accept Args&&... is a lot weaker than having a Fn() -> T, since what you really want to write is more like:

    return std::optional<Widget>(std::init_from_invoke, [&]{
        return Widget(std::move(*d));
    });
    

    But we have no such thing, so we need to hack it together with types like X (and hope that Widget isn't itself otherwise constructible from X).