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)); | ~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~
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?
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
).