Sometimes I use std::unique_ptr<BaseClass>
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 <algorithm>
#include <type_traits>
template <typename B, size_t Size, size_t Align>
struct PolyUnionSize {
template <typename D>
explicit PolyUnionSize(D&& d) noexcept (std::is_nothrow_copy_constructible_v<D>) {
new(&storage) D(std::forward<D>(d));
}
template <typename D>
PolyUnionSize& operator=(D&& d) noexcept(std::is_nothrow_assignable_v<B, D>) {
this->~PolyUnionSize();
*(new(&storage) D) = std::forward<D>(d);
return *this;
}
~PolyUnionSize() noexcept {
(*this)->~B();
}
B const& operator*() const noexcept {
return *reinterpret_cast<B const*>(&storage);
}
B& operator*() noexcept {
return *reinterpret_cast<B*>(&storage);
}
B const* operator->() const noexcept {
return reinterpret_cast<B const*>(&storage);
}
B* operator->() noexcept {
return reinterpret_cast<B*>(&storage);
}
private:
typename std::aligned_storage<Size, Align>::type storage;
};
constexpr auto max(std::initializer_list<std::size_t> const& t) noexcept {
return *std::max_element(std::begin(t), std::end(t));
}
template <typename B, typename ... Ds>
struct PolyUnion : public PolyUnionSize<B, max({sizeof(Ds)... }), max({alignof(Ds)... })> {
using PUSize = PolyUnionSize<B, max({sizeof(Ds)... }), max({alignof(Ds)... })>;
template <typename D>
explicit PolyUnion(D&& d) noexcept(std::is_nothrow_copy_constructible_v<D>)
: PUSize(std::forward<D>(d))
{
static_assert((std::is_base_of_v<B, Ds> && ...));
AssertDinDs<D>();
}
template <typename D>
PolyUnion& operator=(D&& d) noexcept(std::is_nothrow_assignable_v<B, D>) {
AssertDinDs<D>();
PUSize::operator=(std::forward<D>(d));
return *this;
}
private:
template <typename D>
constexpr void AssertDinDs() const& noexcept{
static_assert((std::is_same_v<D, Ds> || ...));
}
};
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<Base, A, B> pu(A{});
pu = B{};
return pu->Foo();
}
Compiles with -std=c++17
or -std=c++2a