There sometimes is a desire to have access to the base class of a polymorphic object without wanting the overhead of dynamic allocation, see for example this question. For a more value-like way of interacting with polymorphic objects, c++26 will introduced std::polymorphic, however that still does a dynamic allocation under the hood. There is also std::variant, but it can only be accessed using a visitor. The solution seems to be something in the middle:
template<class Base, class... Derived>
requires (std::derived_from<Derived, Base> && ...)
class static_polymorphic: public std::variant<Derived...>
{
public:
using std::variant<Derived...>::variant;
Base* operator->() {
return std::visit([](auto& arg){return static_cast<Base*>(&arg);}, *this);
}
const Base* operator->() const {
return std::visit([](auto& arg){return static_cast<const Base*>(&arg);}, *this);
}
Base& operator*() {
return *operator->();
}
const Base& operator*() const {
return *operator->();
}
};
It can be used like so:
struct Base {
virtual void bla() const = 0;
};
struct Derived1: B {
void foo() const override {
std::cout << "Derived1::foo()\n";
}
};
struct Derived2: B {
void foo() const override {
std::cout << "Derived2::foo()\n";
}
};
static_polymorphic<Base, Derived1, Derived2> bar = Derived2{};
bar->foo(); // prints Derived2::foo()
The compiler seems to be able to mostly optimize away the std::visit(), as it can see that it returns the same pointer to base for all possible derived types. However, since a std::variant can be valueless, it still adds a tiny check for that.
User code can still use std::visit() on objects of type static_polymorphic<>. There is the possibility of object slicing, consider:
struct Derived3: Derived2 {
void foo() const override {
std::cout << "Derived3::foo()\n";
}
};
static_polymorphic<Base, Derived1, Derived2> bar = Derived3{};
bar->foo(); // prints Derived2::foo(), not Derived3::foo()
However, that issue also exists in plain std::variant. Some questions are:
- Should the constructors and assignment operators be overloaded to prevent object slicing?
- Are there any other safety issues?
- Is there anything that can be improved?