Home > Software engineering >  Enforcing a common interface with std::variant without inheritance
Enforcing a common interface with std::variant without inheritance

Time:05-31

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.

CodePudding user response:

The first thing to do is to define a concept that ensures a given type has the interface you want. There are many ways to do that, you could for example have the types inherit from BaseElement, and then write:

template<typename T>
concept VisualElementInterface = std::is_base_of_v<BaseElement, T>;

If you don't want to use inheritance, you can write your own checks that T has the required interface.

After creating the concept, you could create an alias for std::variant that restricts the allowed types it holds to those that satisfy that concept:

template<VisualElementInterface... Ts>
using VisualElementVariant = std::variant<Ts...>;

Then you can declare:

using VisualElement = VisualElementVariant<Circle, Image, Polygon>;

Note that you can't use .uniqueId() on a std::variant. However, you could write a free function to get the ID of the element contained in a VisualElement:

auto uniqueId(const VisualElement& element) {
    return std::visit([](auto&& el){ return el.uniqueId(); }, element);
}

And use it like so:

std::cout << uniqueId(firstElement) << '\n';

CodePudding user response:

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();
    }
};
  • Related