Search code examples
c++interfacedispatch

Common interface with a wrapper around std::variant or unions


This question is related to Enforcing a common interface with std::variant without inheritance.

The difference between that question and this one, is that I wouldn't mind inheritance, I am simply looking for the following structs/classes...

struct Parent { virtual int get() = 0; };
struct A : public Parent { int get() { return 1; } };
struct B : public Parent { int get() { return 2; } };
struct C : public Parent { int get() { return 3; } };

... to be AUTOMATICALLY "assembled" into a template:

template<typename PARENT, typename... TYPES>
struct Multi
{
    // magic happens here
}

// The type would accept assignment just like an std::variant would...
Multi<Parent, A, B, C> multiA = A();
Multi<Parent, A, B, C> multiB = B();
Multi<Parent, A, B, C> multiC = C();

// And it would also be able to handle virtual dispatch as if it were a Parent*
Multi<Parent, A, B, C> multiB = B();
multiB.get(); // returns 2

Is this possible? If so, how? I would like to avoid working with handling pointers, as the use of std::variant/unions is intended to make memory contiguous.


Solution

  • You can't automagically set this up to allow multiB.get(), but you can allow multiB->get() or (*multiB).get() and even implicit conversion, by providing operator overloads:

    template<typename Base, typename... Types>
    struct Multi : std::variant<Types...>
    {
        using std::variant<Types...>::variant;
    
        operator Base&()               { return getref<Base>(*this); }
        Base& operator*()              { return static_cast<Base&>(*this); }
        Base* operator->()             { return &static_cast<Base&>(*this); }
    
        operator const Base&() const   { return getref<const Base>(*this); }
        const Base& operator*() const  { return static_cast<const Base&>(*this); }
        const Base* operator->() const { return &static_cast<const Base&>(*this); }
    
    private:
        template<typename T, typename M>
        static T& getref(M& m) {
            return std::visit([](auto&& x) -> T& { return x; }, m);
        }
    };
    

    You've probably encountered this kind of thing before when using iterators from the standard library.

    Example:

    int main()
    {
        Multi<Parent, A, B, C> multiA = A();
        Multi<Parent, A, B, C> multiB = B();
        Multi<Parent, A, B, C> multiC = C();
    
        // Dereference
        std::cout << (*multiA).get();
        std::cout << (*multiB).get();
        std::cout << (*multiC).get();
    
        // Indirection
        std::cout << multiA->get();
        std::cout << multiB->get();
        std::cout << multiC->get();
    
        // Implicit conversion
        auto fn = [](Parent& p) { std::cout << p.get(); };
        fn(multiA);
        fn(multiB);
        fn(multiC);
    }
    

    Output:

    123123123