16

While reading through GCC's implementation of std::optional I noticed something interesting. I know boost::optional is implemented as follows:

template <typename T>
class optional {
    // ...
private:
    bool has_value_;
    aligned_storage<T, /* ... */> storage_;
}

But then both libstdc++ and libc++ (and Abseil) implement their optional types like this:

template <typename T>
class optional {
    // ...
private:
    struct empty_byte {};
    union {
        empty_byte empty_;
        T value_;
    };
    bool has_value_;
}

They look to me as they are functionally identical, but are there any advantages of using one over the other? (Except for the obvious lack of placement new in the latter which is really nice.)

Barry
  • 286,269
  • 29
  • 621
  • 977
Ron
  • 1,989
  • 2
  • 17
  • 33

2 Answers2

19

They look to me as they are functionally identical, but are there any advantages of using one over the other? (Except for the obvious lack placement new in the latter which is really nice.)

It's not just "really nice" - it's critical for a really important bit of functionality, namely:

constexpr std::optional<int> o(42);

There are several things you cannot do in a constant expression, and those include new and reinterpret_cast. If you implemented optional with aligned_storage, you would need to use the new to create the object and reinterpret_cast to get it back out, which would prevent optional from being constexpr friendly.

With the union implementation, you don't have this problem, so you can use optional in constexpr programming (even before the fix for trivial copyability that Nicol is talking about, optional was already required to be usable as constexpr).

Barry
  • 286,269
  • 29
  • 621
  • 977
14

std::optional cannot be implemented as aligned storage, due to a post-C++17 defect fix. Specifically, std::optional<T> is required to be trivially copyable if T is trivially copyable. A union{empty; T t}; will satisfy this requirement

Internal storage and placement-new/delete usage cannot. Doing a byte copy from a TriviallyCopyable object to storage that does not yet contain an object is not sufficient in the C++ memory model to actually create that object. By contrast, the compiler-generated copy of an engaged union over TriviallyCopyable types will be trivial and will work to create the destination object.

So std::optional must be implemented this way.

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
  • Why does a byte copy of a TriviallyCopyable object to uninitialized storage not create that object? It sounds to me like it should for most primitive types, can you give an example for why not? – Ron Sep 14 '18 at 22:59
  • 2
    @RonMordechai: Because [intro.object]/1 says so. Or rather, it lists the syntactic constructs that create objects, and "copying memory" isn't among them. Therefore, it cannot create objects. Values in memory do not give rise to objects on their own. This is a common case of UB that "works". – Nicol Bolas Sep 14 '18 at 23:02
  • 2
    Why empty field is required? Will it work simply with ```union { T t; };```? – random Dec 16 '18 at 04:08
  • @random: Because `T` may not be default constructible. Or assignable. When the `optional` doesn't have a `T`, it has to be done in a way that it doesn't have a `T`. And if you have a union with a single element, it always stores an element of that type. – Nicol Bolas Dec 16 '18 at 04:14
  • 1
    @NicolBolas Do you mean that if union has single member, it will be constructed automatically? I've just experimented in Visual Studio, and constructor is not called. So it looks like union stores no object by default. – random Dec 16 '18 at 04:53