Maikel covered some issues, but since title says "reinventing std::optional", I'll assume that you want full blown std::optional (which is quite hard to get right, but not as hard as std::variant). I want to add that your code as it stands probably is good for your needs already. I'll also try to answer some of the questions you mentioned in the comments.
edit: @L.F. noticed that placement new is not
constexpr friendly. For possibilities of getting that feature please have a look at this question and it's accepted answer.
Currently value is stored in the variable of its own type. To make it more robust (such as handling non default constructible types, or types that are expensive to construct), it is possible to store the value in the byte array. Yes, it might sound scary, but it is the easiest approach in my opinion.
using byte = unsigned char;
alignas(T) byte[sizeof(T)] container;
Then it is possible to use placement new to construct the value. Now we control when object will be constructed.
To access the value already stored in it,
reinterpret_cast<> is needed. I know this starts sounding dangerous, but it is the way it should be.
T& get() noexcept
const T& get() const noexcept
The destructor will need to be changed then:
noexcept means that the function promises that it won't throw, and if it will,
std::terminate is called. It is good because people will know that they can call the function without worrying that it will throw. For example, they could use it in destructors, exception handlers, where throwing again would call
As a side note, if there is any member variable that has throwing destructor, the destructor of the enclosing class will be marked as
noexcept(false) (link, see the explanation part), which might get into trouble in generic code.
With current interface users will have to construct the value and then copy it into the optional, which is wasteful. One way is to create move constructor, the other way is to create conversion constructor (there is no real agreement on how those are called).
template <typename ... ArgTypes>
Optional(std::in_place_t<T>, ArgTypes ... args)
new (&container) T(std::forward<ArgTypes>(args)...);
Notice how I used
std::in_place_t<T>. It is there because compiler will treat every call to constructor as conversion constructor. The reason is that
ArgTypes... can be anything, which includes
std::in_place_t<T> will disambiguate that. As an example:
// ^^ ArgTypes is T&, not const T&,
// so conversion constructor will be called
// which is plain wrong.
The way to work around that would be:
But it is almost the same typing as
The class would benefit from implementing
operator=(...). Move assigning would also be great. Additionally, there is no way to assign one
Optional to another
Optional, which would be great to have.