Search code examples
c++templatesvirtual-functionsvariantstrong-typedef

Sum types in C++


At work, I ran into a situation where the best type to describe the result returned from a function would be std::variant<uint64_t, uint64_t> - of course, this isn't valid C++, because you can't have two variants of the same type. I could represent this as a std::pair<bool, uint64_t>, or where the first element of the pair is an enum, but this is a special case; a std::variant<uint64_t, uint64_t, bool> isn't so neatly representable, and my functional programming background really made me want Either - so I went to try to implement it, using the Visitor pattern as I have been able to do in other languages without native support for sum types:

template <typename A, typename B, typename C>
class EitherVisitor {
    virtual C onLeft(const A& left) = 0;
    virtual C onRight(const B& right) = 0;
};

template <typename A, typename B>
class Either {
    template <typename C>
    virtual C Accept(EitherVisitor<A, B, C> visitor) = 0;
};

template <typename A, typename B>
class Left: Either<A, B> {
private:
    A value;
public:
    Left(const A& valueIn): value(valueIn) {}

    template <typename C>
    virtual C Accept(EitherVisitor<A, B, C> visitor) {
        return visitor.onLeft(value);
    }
};

template <typename A, typename B>
class Right: Either<A, B> {
private:
    B value;
public:
    Right(const B& valueIn): value(valueIn) {}
    
    template <typename C>
    virtual C Accept(EitherVisitor<A, B, C> visitor) {
        return visitor.onRight(value);
    }
};

C++ rejects this, because the template method Accept cannot be virtual. Is there a workaround to this limitation, that would allow me to correctly represent the fundamental sum type in terms of its f-algebra and catamorphism?


Solution

  • Perhaps the simplest solution is a lightweight wrapper around T for Right and Left? Basically a strong type alias (could also use Boost's strong typedef)

    template<class T>
    struct Left
    {
        T val;
    };
    
    template<class T>
    struct Right
    {
        T val;
    };
    

    And then we can distinguish between them for visitation:

    template<class T, class U>
    using Either = std::variant<Left<T>, Right<U>>;
    
    Either<int, int> TrySomething()
    {
        if (rand() % 2 == 0) // get off my case about rand(), I know it's bad
            return Left<int>{0};
        else
            return Right<int>{0};
    }
    
    struct visitor
    {
        template<class T>
        void operator()(const Left<T>& val_wrapper)
        {
            std::cout << "Success! Value is: " << val_wrapper.val << std::endl;
        }
        
        template<class T>
        void operator()(const Right<T>& val_wrapper)
        {
            std::cout << "Failure! Value is: " << val_wrapper.val << std::endl;
        }
    };
    
    int main()
    {
        visitor v;
        for (size_t i = 0; i < 10; ++i)
        {
            auto res = TrySomething();
            std::visit(v, res);
        }
    }
    

    Demo