1

I'm looking at parboiled2 in Scala and it's got an interesting way of encoding parser behavior in the type system using covariant/contravariant subtypes:

https://github.com/sirthias/parboiled2#rule-types-and-the-value-stack

I'm curious if something similar can be accomplished in C++ with template meta-programming.

I suspect the covariant behavior could be emulated with inheritance, but what about contravariant?

gct
  • 14,100
  • 15
  • 68
  • 107

1 Answers1

2

In C++, like in other OO languages, subtypes are generally represented by inheritance.

However, we cannot model covariance and contravariance relationships through inheritance, because there is no way to list the base classes of a class (without any of the various reflection proposals, which have not made their way into the language yet).

The easiest way to allow this kind of behavior is to allow covariant and contravariant templated classes to convert based on the relationship of the related types.

Covariance

If Derived is-a Base, then Covariant<Derived> "should-be-a" Covariant<Base>.

Normally, the solution would be to make Covariant<Derived> inherit from Covariant<Base>, but we currently have no way of finding Base given only Derived. However, we can enable the conversion by writing a constructor for Covariant<Base> taking any Covariant<Derived>:

template <typename T>
struct Covariant {
    template <typename Derived>
    Covariant(const Covariant<Derived>& derived, 
              std::enable_if_t<std::is_base_of_v<T, Derived>>* = nullptr)
    {
        /* Do your conversion here */
    }
};

Contravariance

If Derived is-a Base, then Contravariant<Base> "should-be-a" Contravariant<Derived>

The trick here is much the same - allowing the conversion of any Contravariant<Base> to Contravariant<Derived>:

template <typename T>
struct Contravariant {
    template <typename Base>
    Contravariant(const Contravariant<Base>& base,
                  std::enable_if_t<std::is_base_of_v<Base, T>>* = nullptr)
    {
        /* Do your conversion here */
    }
};

However

This has one major drawback: You need to implement the conversions manually, and be aware that accidental object slicing may ruin your ability to convert back (e.g. if you define a covariant container type, that will be a cause of major headaches).

Essentially, until reflection allows us to automate that sort of inheritance relationship, conversions are the only way to do this, and I would not recommend using this for anything complex. As soon as you store objects of your T in the covariant/contravariant classes, you are in for a world of hurt.

Here is a Godbolt link to show that it works

hlt
  • 6,219
  • 3
  • 23
  • 43