Search code examples
c++c++20variant

Enforcing a common interface with std::variant without inheritance


Suppose you have some classes like Circle, Image, Polygon for which you need to enforce a common interface that looks like this (not real code):

struct Interface {
    virtual bool hitTest(Point p) = 0;
    virtual Rect boundingRect() = 0;
    virtual std::string uniqueId() = 0;
}

so for example the Circle class would like:

struct Circle {
    // interface
    bool hitTest(Point p) override;
    Rect boundingRect() override;
    std::string uniqueId() override;
    
    double radius() const;
    Point center() const;
    // other stuff
}

I would like to use std::variant<Circle, Image, Polygon> to store instances of my classes in a std::vector and then use it like this:

using VisualElement = std::variant<Circle, Image, Polygon>;

std::vector<VisualElement> elements;
VisualElement circle = MakeCircle(5, 10);
VisualElement image = MakeImage("path_to_image.png");

elements.push_back(circle);
elements.push_back(image);
auto const &firstElement  = elements[0];
std::cout << firstElement.uniqueId() << std::endl;

Using inheritance I could do this by creating a base class and then each of my classes would become a subclass of the base (and obviously if a derive class doesn't implement the interface the program wouldn't compile). Then instead of using variants, I could use smart pointers to store the instances in a vector (e.g. std::vector<std::unique_ptr<BaseElement>>). I would like to avoid this, so I'm wondering what would be the best way (if there is any) to enforce the same design using std::variant and C++20.


Solution

  • The simplest and quite execution time optimal solution is have separate container for each type. Any example showing that Data Oriented Design is better then Object Oriented Programing is using this approach to show difference in performance.

    Other way is to create some wrapper for variant:

    class VisualElement
    {
        BaseElement* self;
        std::variant<Circle, Image, Polygon> item;
    public:
    
        template<typename T, bool = std::is_base_of_v<BaseElement, T>>
        VisualElement(const &T other) {
            item = other;
            self = &item.get<T>();
        }
    
        template<typename T, bool = std::is_base_of_v<BaseElement, T>>
        VisualElement& operator=(const &T other) {
            item = other;
            self = &item.get<T>();
            return *this;
        }
    
        bool hitTest(Point p) {
           return self->hitTest(p);
           // or use of std::visit and drop common interface ancestor.
        }
    
        Rect boundingRect() {
           return self->boundingRect();
        }
        std::string uniqueId() {
           return self->uniqueId();
        }
    };