Reputation: 2471
The only requirement to type parameter of std::optional<T>
class template mentioned in [optional.optional.general] p3 is that type T
is Destructible.
Suppose I have a very restrictive class which meets that requirement:
struct R
{
R(const R&) = delete;
R& operator=(const R&) = delete;
R(R&&) = delete;
R& operator=(R&&) = delete;
// This works thanks to guaranteed copy elision since C++17.
static R create()
{
return R();
}
private:
R() = default;
};
Yet that class is still perfectly usable:
void test_1()
{
R obj = R::create();
/* use obj */
}
The question is: can I really have a usable object of type std::optional<R>
? By "usable" I mean an object that contains value. If yes, then how do I construct that value there?
The following code obviously fails because every constructor of R
is inaccessible:
void test_2()
{
std::optional<R> o(R::create()); // Error: no matching constructor
}
Edit:
Wouldn't it be nice if std::optional
had a constructor that accepted a builder?
template <typename Builder>
optional::optional(disabmiguating_tag, Builder f)
: my_internal_union(f())
{
}
with apropriate constructor of my_internal_union
of course...
Upvotes: 14
Views: 1044
Reputation: 39869
You can use a a type with an operator R
:
struct Builder {
operator R() const { return R::create(); }
};
int main() {
std::optional<R> o(Builder{});
}
See live example at Compiler Explorer
This works because the std::optional<R> o
is constructed via the constructor #8 on cppreference, which means that the R
that inhabits o
is constructed as if by calling
R(std::move(builder))
... where builder
is a reference to the Builder{}
we have passed in.
operator R
returns a prvalue, so the returned R::create()
and the constructed R
are the same object, by virtue of C++17 guaranteed copy elision.
Upvotes: 23
Reputation: 28490
I know the question is c++17, but apparently c++23 you can use this trick:
auto o = std::optional<int>{0}.transform([](int){ return R::create(); });
where we create a dummy non-empty optional (in this case std::optional<int>
) and use std::optional<T>::transform
to apply a function on its inner int
; that function returns the desired R
object via R::create()
, which is created in place in the memory that std::optional
keeps reserved for its payload.
It is valid because of C++17's mandatory copy elision, because the lambda return type is R
, which is the same type as R::create()
, which in turn is prvalue. I think that means that this R
object is constructed directly in the "final" place, which is inside the std::optional<R> o
that you are constructing.
Probably std::construct_at
from c++20 is the core of the trick, but I don't think you can use it from the outside, because you don't have access to std::optional
's member where the payload is stored, and you can't access to it via .value()
because that's UB on an empty std::optional
, and you can't have a non-empty std::optional<R>
because that's precisely what we are discussing!
Upvotes: 8