Skip to main content
We’ve updated our Terms of Service. A new AI Addendum clarifies how Stack Overflow utilizes AI interactions.
Became Hot Network Question
edited title
Link
G. Sliepen
  • 69.5k
  • 3
  • 75
  • 180

A static version of `stdstd::polymorphic`polymorphic

added 15 characters in body
Source Link
G. Sliepen
  • 69.5k
  • 3
  • 75
  • 180
struct Base {
    virtual void blafoo() const = 0;
};

struct Derived1: BBase {
    void foo() const override {
        std::cout << "Derived1::foo()\n";
    }
};

struct Derived2: BBase {
    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 calls to 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.

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.

struct Base {
    virtual void foo() const = 0;
};

struct Derived1: Base {
    void foo() const override {
        std::cout << "Derived1::foo()\n";
    }
};

struct Derived2: Base {
    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 calls to 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.

Source Link
G. Sliepen
  • 69.5k
  • 3
  • 75
  • 180

A static version of `std::polymorphic`

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, 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?