C++ My implementation of a simple sum type (variant) Here is a simple variant type I have made. For simplicity, it only can handle two different values. I am planning on using this to create a variadic variant template, using inheritance and recursion.

The requirements for my variant type:


It must have performance similar to std::variant
It must not dynamically allocate memory
There must be no undefined behavior
It should check as much as possible at compile-time, and throw an exception if access is attempted to the wrong type
It must support nontrivial types


Here is the source:

#include &lt;iostream&gt;
#include &lt;cstddef&gt;
#include &lt;typeindex&gt;
#include &lt;optional&gt;
#include &lt;algorithm&gt;
#include &lt;stdexcept&gt;
#include &lt;functional&gt;

/** A variant (sum type) that can contain one of two different types at any one time**/
template&lt;typename T1, typename T2&gt;
class Either {

    using Bigest = std::conditional_t&lt;sizeof(T1) &lt;= sizeof(T2), T1, T2&gt;;
    alignas(sizeof(Bigest)) std::byte storage[sizeof(Bigest)];
    std::optional&lt;std::type_index&gt; conatinedType;
    std::function&lt;void(std::byte*)&gt; destructor;

public:
    Either() : conatinedType(std::nullopt) {}

    template&lt;typename T&gt;
    Either(const T&amp; value) : conatinedType(typeid(T)) {
        static_assert(std::is_same&lt;T1,T&gt;::value || std::is_same&lt;T2,T&gt;::value, "Either cannot contain type T");
        const auto src = reinterpret_cast&lt;const std::byte*&gt;(&amp;value);
        for(size_t i = 0; i &lt; sizeof(T); i++) {
            storage[i] = src[i];
        }
        destructor = [](std::byte* data){ (*reinterpret_cast&lt;T*&gt;(data)).~T(); };
    }

    class BadVariantAccess : public std::exception {};

    template&lt;typename T&gt;
    const T&amp; getAs() const {
        static_assert(std::is_same&lt;T1,T&gt;::value || std::is_same&lt;T2,T&gt;::value, "Either cannot contain type T");
        if(conatinedType == typeid(T)) {
            const auto ptr = reinterpret_cast&lt;const T*&gt;(&amp;storage);
            return *ptr;
        }
        else throw BadVariantAccess();
    };
};


And a test:

struct MyInt {
    int val;
    MyInt(const int&amp; val) : val(val) {
        std::cout &lt;&lt; "making int!" &lt;&lt; std::endl;
    }
    ~MyInt() {
        std::cout &lt;&lt; "destructing int!" &lt;&lt; std::endl;
    }
};

struct MyDouble {
    double val;
    MyDouble(const double&amp; val) : val(val) {
        std::cout &lt;&lt; "making double!" &lt;&lt; std::endl;
    }
    ~MyDouble() {
        std::cout &lt;&lt; "destructing double!" &lt;&lt; std::endl;
    }
};

int main()
{
    Either&lt;MyInt, MyDouble&gt; e = MyDouble(5.0987);

    try{
        std::cout &lt;&lt; e.getAs&lt;MyDouble&gt;().val &lt;&lt; std::endl;
    }
    catch(...) {
        std::cerr &lt;&lt; "Bad Variant Access" &lt;&lt; std::endl;
    }

    return 0;
}
Construction and destruction of non-trivial types

Unfortunately, your class does not really support non-trivial types, because your class copies the representation of the supplied values and instead of invoking the proper copy constructor.  In other words, you create no object in the Either class, so the behavior of reinterpret_casting the storage and calling the destructor is undefined.

Interestingly, you store the destructor function object but never actually call it, so the class ends up working for trivial types, which do not require invocations to the corresponding constructor or destructor.  However, making your code work by letting two errors cancel out each other is not really a good idea.

The copy and move semantics of your class is also broken.  We'll try to fix this problem later.

Use of runtime type information (typeid)

The usage of typeid is a bit overkill here and not optimal, because it forces the user to pass exactly the same type as one of the stored types, disallowing conversions.  In fact, simply storing an index seems to suffice.

std::variant uses overload resolution to deduce the type, which is hard to implement.  You may consider supporting in_place construction.  (On the other hand, std::variant is typically implemented with unions because of constexpr requirements.)

The storage


using Bigest = std::conditional_t&lt;sizeof(T1) &lt;= sizeof(T2), T1, T2&gt;;
alignas(sizeof(Bigest)) std::byte storage[sizeof(Bigest)];
std::optional&lt;std::type_index&gt; conatinedType;
std::function&lt;void(std::byte*)&gt; destructor;



Consider using std::aligned_storage.  As I said above, we can simply store an index.

using storage_type = std::aligned_storage_t&lt;std::max( sizeof(T1),  sizeof(T2)),
                                            std::max(alignof(T1), alignof(T2))&gt;;
storage_type storage;
std::size_t index;


For valueless variants, we can provide a special index:

static constexpr std::size_t npos = std::numeric_limits&lt;std::size_t&gt;::max();


The constructors

The default constructor of std::variant default-constructs the first alternative type.  You can go into the valueless state instead:

Either() noexcept
    : index{npos}
{
}


Here's how you'd support in place constructors:

template &lt;std::size_t I, typename... Args&gt;
explicit Either(std::in_place_t&lt;I&gt;, Args&amp;&amp;... args)
{
    construct&lt;I&gt;(std::forward&lt;Args&gt;(args)...);
}

template &lt;std::size_t I, typename U, typename... Args&gt;
explicit Either(std::in_place_t&lt;I&gt;, std::initializer_list&lt;U&gt; ilist, Args&amp;&amp;... args)
{
    construct&lt;I&gt;(ilist, std::forward&lt;Args&gt;(args)...);
}


where the private member function template construct is defined like

template &lt;std::size_t I&gt;
struct alternative_type {
    static_assert(I &lt; 2);
    using type = std::conditional_t&lt;I == 0, T1, T2&gt;;
};
template &lt;std::size_t I&gt;
using alternative_type_t = typename alternative_type&lt;I&gt;::type;

// not exception safe
template &lt;std::size_t I, typename... Args&gt;
void construct(Args&amp;&amp;... args)
{
    index = I;
    ::new (static_cast&lt;void*&gt;(&amp;storage))
        alternative_type_t&lt;I&gt;(std::forward&lt;Args&gt;(args));
}


Special member functions

As I said before, you need to implement the copy/move/destruction operations.  Here's the copy constructor for example: (SFINAE and explicit issues are omitted for simplicity.)

Either(const Either&amp; other)
{
    if (other.index == 0) {
        construct&lt;T1&gt;(other.get&lt;0&gt;());
    } else if (other.index == 1) {
        construct&lt;T2&gt;(other.get&lt;1&gt;());
    } else {
        index = npos;
    }
}