Search code examples
c++constructorinitializationoption-typestdoptional

How does std::optional delay initialization? / How is std::optional implemented?


Lately I've taken interest in initialization. One of the things I'm particularly interested in, is std::optional for its ability to initialize an instance of a type after it's been declared. I have tried reading the code inside the optional header, but the code is just too 'bombastic' for me to comprehend.

How is std::optional able to delay the initialization of an object on the stack? I assume it just reserves sizeof(<whichever_type) number of bytes on the stack, and then reinterprets those bytes for the initialization of <whichever_bytes>. But how does it do that specificially? How is it implemented? How can I implement that myself?

Edit: to clarify, I know that std::optional basically has a bool member to keep track of whether the object is initialized or not, and another member, which contains the data.

What I don't understand, however, is how optional is able to manually initialze something.

How is it able to destruct an object? How is it able to reconstruct a new one again after the old one is destructed?


Solution

  • The "obvious" way to represent an std::optional<T> is to use an indication whether the value is set together with a union containing a T, i.e., something like this:

    template <typename T>
    class optional {
        bool isSet = false;
        union { T value; };
    public:
        // ...
    };
    

    By default the members in the union are not initialized. Instead, you'll need to use placement new and manual destruction to manage the life-time of the entity within the union. Conceptually that is similar to using an array of bytes but the compiler handles any alignment requirements.

    Here a program with some of the operations shown:

    #include <iostream>
    #include <memory>
    #include <string>
    #include <utility>
    #include <cassert>
    
    template <typename T>
    class optional {
        bool isSet = false;
        union { T value; };
        void destroy() { if (this->isSet) { this->isSet = true; this->value.~T(); } }
    
    public:
        optional() {}
        ~optional() { this->destroy(); }
        optional& operator=(T&& v) {
            this->destroy();
            new(&this->value) T(std::move(v));
            this->isSet = true;
            return *this;
        }   
    
        explicit operator bool() const { return this->isSet; }
        T&       operator*()       { assert(this->isSet); return this->value; }
        T const& operator*() const { assert(this->isSet); return this->value; }
    };  
    
    int main()
    {   
        optional<std::string> o, p;
        o = "hello";
        if (o) {
            std::cout << "optional='" << *o << "'\n";
        }   
    }