3

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.

G. Sliepen
  • 7,637
  • 1
  • 15
  • 31
  • Ok. Are you getting errors? Where is the question? – Taekahn May 30 '22 at 13:40
  • I'm not sure what you mean. The (pseudo)code above is what I'd like to achieve, but I don't want to make all my classes inherit from a base class (that introduces a whole set of problems and a likely overuse of casting). So I would like to enforce a common interface on my classes and store them in a variant. My question is: how can I do this? – nightdev123 May 30 '22 at 13:45
  • Are you asking if there is a way to produce a compiler error if they don’t all have the same interface? – Taekahn May 30 '22 at 13:48
  • I wrote an answer recently that I think may be relevant: https://stackoverflow.com/questions/72266073/best-way-to-store-stdvector-of-derived-class-in-a-host-parent-class/72308999#72308999 – Jeff Garrett May 30 '22 at 14:44
  • One more suggestion on the side is instead of having `std::vector>` I suggest having vectors per type for data homogeneity. I.e. you will have `std::tuple, std::vector, std::vector>` and with some helper code you can pick from the correct vector depending on what you're working with. The code will run much faster if you don't need to check the type for every single entry. – Alexander Kondratskiy May 30 '22 at 20:17

2 Answers2

3

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';
G. Sliepen
  • 7,637
  • 1
  • 15
  • 31
  • 1
    I took `std::cout << firstElement.uniqueId() << std::endl;` to be a key part of the question – concepts help enforce that each variant element has some interface, but that still leaves open the question of how to use it this way rather than visitation. – ildjarn May 30 '22 at 13:57
  • 1
    @ildjarn once you know that they implement the same interface it is straightforward to write a template and call `uniqueId(firstElement)` without `std::visit` or similar needed – 463035818_is_not_an_ai May 30 '22 at 13:59
  • @463035818_is_not_a_number : I'm aware of that, nontheless it is a gap in the answer. – ildjarn May 30 '22 at 14:00
2

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();
    }
};
Marek R
  • 32,568
  • 6
  • 55
  • 140
  • A nice solution that avoids needing `std::visit()`. However, the cost is an extra pointer per `VisualElement` object (although this is still better than `std::unique_ptr`). Perhaps missing is a (templated) move constructor. – G. Sliepen May 30 '22 at 20:28