4

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.

5
  • Ok. Are you getting errors? Where is the question? Commented May 30, 2022 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? Commented May 30, 2022 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? Commented May 30, 2022 at 13:48
  • I wrote an answer recently that I think may be relevant: stackoverflow.com/questions/72266073/… Commented May 30, 2022 at 14:44
  • One more suggestion on the side is instead of having std::vector<std::variant<A, B, C>> I suggest having vectors per type for data homogeneity. I.e. you will have std::tuple<std::vector<A>, std::vector<B>, std::vector<C>> 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. Commented May 30, 2022 at 20:17

2 Answers 2

4

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';
Sign up to request clarification or add additional context in comments.

3 Comments

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 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_a_number : I'm aware of that, nontheless it is a gap in the answer.
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();
    }
};

1 Comment

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<BaseElement*>). Perhaps missing is a (templated) move constructor.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.