A polymorphic union in C++ Sometimes I use std::unique_ptr&lt;BaseClass&gt; when all I really want is polymorphism.  For fun, I made this container that skips the heap allocation.  The downside is that it needs to know all possible derived types it will store in order to know how much space to reserve.

Any feedback is appreciated, but I'm especially interested in how this implementation enables/prevents compiler optimizations you would otherwise have with virtual types.

Edit: I am also intersted in supporting constexpr access, but right now the reinterpret_cast prevents constexpr dereference.

#include &lt;algorithm&gt;
#include &lt;type_traits&gt;

template &lt;typename B, size_t Size, size_t Align&gt;
struct PolyUnionSize {
    template &lt;typename D&gt;
    explicit PolyUnionSize(D&amp;&amp; d) noexcept (std::is_nothrow_copy_constructible_v&lt;D&gt;) {
        new(&amp;storage) D(std::forward&lt;D&gt;(d));
    }

    template &lt;typename D&gt;
    PolyUnionSize&amp; operator=(D&amp;&amp; d) noexcept(std::is_nothrow_assignable_v&lt;B, D&gt;) {
        this-&gt;~PolyUnionSize();
        *(new(&amp;storage) D) = std::forward&lt;D&gt;(d);
        return *this;
    }

    ~PolyUnionSize() noexcept {
        (*this)-&gt;~B();
    }

    B const&amp; operator*() const noexcept {
        return *reinterpret_cast&lt;B const*&gt;(&amp;storage);
    }

    B&amp; operator*() noexcept {
        return *reinterpret_cast&lt;B*&gt;(&amp;storage);
    }

    B const* operator-&gt;() const noexcept {
        return reinterpret_cast&lt;B const*&gt;(&amp;storage);
    }

    B* operator-&gt;() noexcept {
        return reinterpret_cast&lt;B*&gt;(&amp;storage);
    }

private:
    typename std::aligned_storage&lt;Size, Align&gt;::type storage;
};

constexpr auto max(std::initializer_list&lt;std::size_t&gt; const&amp; t) noexcept {
    return *std::max_element(std::begin(t), std::end(t));
}

template &lt;typename B, typename ... Ds&gt;
struct PolyUnion : public PolyUnionSize&lt;B, max({sizeof(Ds)... }), max({alignof(Ds)... })&gt; {
    using PUSize = PolyUnionSize&lt;B, max({sizeof(Ds)... }), max({alignof(Ds)... })&gt;;

    template &lt;typename D&gt;
    explicit PolyUnion(D&amp;&amp; d) noexcept(std::is_nothrow_copy_constructible_v&lt;D&gt;)
        : PUSize(std::forward&lt;D&gt;(d))
    {
        static_assert((std::is_base_of_v&lt;B, Ds&gt; &amp;&amp; ...));
        AssertDinDs&lt;D&gt;();
    }

    template &lt;typename D&gt;
    PolyUnion&amp; operator=(D&amp;&amp; d) noexcept(std::is_nothrow_assignable_v&lt;B, D&gt;) {
        AssertDinDs&lt;D&gt;();
        PUSize::operator=(std::forward&lt;D&gt;(d));
        return *this;
    }

private:
    template &lt;typename D&gt;
    constexpr void AssertDinDs() const&amp; noexcept{
        static_assert((std::is_same_v&lt;D, Ds&gt; || ...));
    }
};



Sample usage:

struct Base {
    virtual int Foo() const { return 1; }
};

struct A : Base {
    int Foo() const final { return 2; }
};

struct B : Base {
    int Foo() const final { return 3; }
};

int main() {
    PolyUnion&lt;Base, A, B&gt; pu(A{});
    pu = B{};
    return pu-&gt;Foo();
}



Compiles with -std=c++17 or -std=c++2a
I suggest not codifying the derived types suggested in the type. Only note the ultimate base, size and alignment. Thus you are open to later change.
Add a templated alias to get the proper type from base plus candidates.

Assure that no over-sized object is ever assigned in PolyUnionSize, probably best using SFINAE. No need to defer to the user.

Don't assume constructing the new object will never fail.

PolyUnionSize::PolyUnionSize&lt;class D&gt;(D&amp;&amp; d) and PolyUnionSize::operator=&lt;class D&gt;(D&amp;&amp; d) use perfect forwarding. You don't account for that by using std::decay_t where needed, nor heed it when computing noexcept.

The implicit copy-/move- ctor / assignment are only appropriate for trivial types. And in that case, why override the dtor?

As casts should be used sparingly, consider delegating between op* and op-&gt;.