1

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?

  • This is the relevant part of the optional header in libc++: https://github.com/llvm-mirror/libcxx/blob/78d6a7767ed57b50122a161b91f59f19c9bd0d19/include/optional#L214-L219 – jtbandes Dec 24 '20 at 18:20

1 Answers1

4

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";
    }   
}   
Dietmar Kühl
  • 150,225
  • 13
  • 225
  • 380
  • Neat! I think this is the first use case of `union` that I'd consider using in my own code. – Eljay Dec 24 '20 at 18:28
  • Thank you! A union it is! However, I have a couple of questions. How can I use 'new' to initialize objects at a specified location on the stack? I don't know much about memory management yet, so I apologize if this a dumb question; the only case I know the keyword 'new' of is for dynamic allocation. Also, is it safe to implement an optional this way? I don't use unions often because I hear a lot about them being unsafe. But I don't know why. So I'll look that up. Still, is there anything particularly unsafe about this implementation of an optional class that you know of? – INEEDANSWERS Dec 24 '20 at 18:39
  • 1
    @Eljay: obviously, `std::variant` is the same thing just with member descriptor instead of a boolean flag. `union`s which can hold interesting C++ types were necessary to unlock some interesting functionality. There are a few other cases where delayed construction of members is beneficial. – Dietmar Kühl Dec 24 '20 at 18:39
  • 1
    @INEEDANSWERS: C++ `union`s are entities you need to look after explicitly. You need to construct/destroy the currently set member and accessing a different one than the currently set is undefined behavior (in the case of `optional` there is just one so you just need to make sure the member is set when you use it). There are more things you need to be aware of and in that sense they are less safe than other facilities which are designed for ease of use. They are tools to create safe entities (like `std::optional`), though. BTW, I enhanced my answer to contain a minimal implementation. – Dietmar Kühl Dec 24 '20 at 18:46
  • Thank you for your elaborate update to your answer. I consider my question answered. I didn't know unions could delay initialization and if I did, I would have used it before. One more question: when you say "There are more (...) ease of use.", all of the dangers you're talking about have to do with the use of unions, right? So, once I've read up on unions and know what to look out for, I should be good to go, right? Or are you talking about errors that have to do with other dangers of this implementation? – INEEDANSWERS Dec 24 '20 at 19:01
  • 1
    @INEEDANSWERS: Well, there are other things beyond what the `union` does you'll need to know to write a correct `optional` class template. Actually, I just spotted a silly error I made and I'll correct: after `destroy()`ing the `value` the code doesn't clear `isSet` but the construction of the new object may throw leaving `value` uninitialized while `isSet` would claim there is an error. That has nothing to do with `union`s but it is something you need to be vigilant about when implement classes explicitly meddling with object life-time (containers in general are in that space). – Dietmar Kühl Dec 24 '20 at 19:10
  • @DietmarKühl Ah right. Well of course those kind of bugs are important to look out for as well, very true. Thank you for your help. Now that I know that that implementation is really all a basic optional is, I'll start looking into the 'new' keyword, move semantics (spotted a std::move there), and unions. Thanks! – INEEDANSWERS Dec 24 '20 at 19:21