1

I am trying to pass a CRTP type parameter to a virtual method. Consequently, the virtual method would need to be a template. However, this is not allowed by C++ (yet?), because it would mean that the size of the vtable -- the common way how compilers implement dynamic dispatch -- is unknown until all sources have been compiled and are being linked. (I found this reasoning during my search on SO.)

In my particular setting, however, there is a finite and known amount of CRTP specializations. Hence, it is possible to define a virtual method overload per specialization and override these in the subclasses. I have prepared a small MWE to demonstrate my situation. Consider the following CRTP hierarchy:

template<typename Actual>
struct CRTPBase
{
    using actual_type = Actual;
    void foo() { static_cast<actual_type*>(this)->foo(); }
    int bar(int i) const { return static_cast<const actual_type*>(this)->bar(i); }
};

struct A : CRTPBase<A>
{
    void foo() { /* do something A would do */ }
    int bar(int i) const { return i + 1; }
};

struct B : CRTPBase<B>
{
    void foo() { /* do something B would do */ }
    int bar(int i) const { return i - 1; }
};

Next, I want to define a virtual class hierarchy with a virtual method to handle all specializations of CRTPBase<T>. Because I know the particular specializations, I can do as follows:

struct VirtualBase
{
    virtual ~VirtualBase() { }
    virtual void accept_crtp(const CRTPBase<A> &o) = 0;
    virtual void accept_crtp(const CRTPBase<B> &o) = 0;
};

struct VirtualDerived : VirtualBase
{
    void accept_crtp(const CRTPBase<A> &o) override { /* much logic to handle A */ }
    void accept_crtp(const CRTPBase<B> &o) override { /* similar logic to handle B */ }
};

Observe that there is one virtual method per specialization of CRTPBase<T>, both in the purely virtual base and in all its derived classes. This overhead easily blows out of proportion with increasing number of specializations of CRTPBase<T> and more derived classes of VirtualBase.

What I would like to do, is roughly the following:

struct VirtualBase
{
    virtual ~VirtualBase() { }
    template<typename T> virtual void accept_crtp(const CRTPBase<T> &o) = 0;
}

struct VirtualDerived : VirtualBase
{
    template<typename T> void accept_crtp(const CRTPBase<T> &o) override {
        /* one logic to handle any CRTPBase<T> */
    }
};

For the reason mentioned in the beginning, this is not possible. User Mark Essel has faced the same issue in another SO post (in an answer, not a question, though). The user proposes to declare and define the virtual methods for each specialization, but in the derived classes implement the actual logic in an additional template, non-virtual method and then forward calls from the virtual methods to that template method:

struct VirtualBase
{
    virtual ~VirtualBase() { }
    virtual void accept_crtp(const CRTPBase<A> &o) = 0;
    virtual void accept_crtp(const CRTPBase<B> &o) = 0;
};

struct VirtualDerived : VirtualBase
{
    void accept_crtp(const CRTPBase<A> &o) override { accept_any_crtp(o); }
    void accept_crtp(const CRTPBase<B> &o) override { accept_any_crtp(o); }

    private:
    template<typename T>
    void accept_any_crtp(const CRTPBase<T> &o) {
        /* one logic to handle any CRTPBase<T> */
    }
};

While this approach avoids code duplication of the logic to handle the CRTPBase<T> specializations, it still requires explicitly writing one method per specialization in the virtual base and all derived classes.

My question is: How can the implementation overhead be reduced?

I have considered using an X macro of the form

#define CRTP_SPECIALIZATIONS_LIST(X) X(A) X(B) // lists all specializations, here A and B

to generate the methods in the virtual base and derived classes. The problem with that is, if the CRTP hierarchy is defined in CRTP.hpp and the virtual base and derived classes are declared/defined in other source files, then the macro is "being leaked" by the header to all translation units that include it. Is there a more elegant way to solve this? Is there maybe a template way of achieving the same goal, perhaps with a variadic template type?

Your help is appreciated. Kind regards,

Immanuel

  • Is it intended that `foo()` and `bar()` are shadowed, i.e. that the derived class has similarly named memberfunctions? Also, as a first reaction to this, I wonder why you don't simply create a virtual base class with different implementations. The code you show doesn't justify the use of CRTP. One last note: Maybe using a mixin instead of CRTP could also help. – Ulrich Eckhardt Dec 07 '21 at 07:32
  • @UlrichEckhardt Yes, the shadowing is intended and it should not harm the general CRTP design. The reason *why* I need CRTP is to enable aggressive inlining by the compiler. Classes A and B provide different in-memory layouts of the same conceptual entity, e.g. like sparse vs. dense matrices. A or B will be used by some algorithms within hot loops and their methods may be called millions of times. – Immanuel Haffner Dec 07 '21 at 07:37
  • Not sure if that helps, but I think what you're doing is an implementation of the "Visitor Pattern". Anyhow, if you write a CRTP base with the different `accept_crtp()` overloads that all delegate to a derived class' method, that derived class' method can be a template. That CRTP base can also be used to implement a virtual base. – Ulrich Eckhardt Dec 07 '21 at 07:42
  • @UlrichEckhardt Indeed, it is quite similar to the Visitor Pattern. However, I avoid dynamic dispatch in the calls to any method of `A` and `B`. Thanks for pointing out mixin types. I was not aware of the concept. But I fail to see how this would solve my problem. I think it would just shift the code duplication to another type: I would still have `A` and `B` but without `CRTPBase` and the new mixin type to operate on `A` or `B`. The signatures of the virtual methods remain unchanged but internally these would use the mixin type to operate on `A` or `B`. Nothing gained IMHO. – Immanuel Haffner Dec 07 '21 at 07:52

3 Answers3

1

As all types are known, you might use std::variant to have a free visitor implementation:

using MyVariant =
    std::variant<std::reference_wrapper<const CRTPBase<A>>,
                 std::reference_wrapper<const CRTPBase<B>>,
                 // ...
                >
struct VirtualBase
{
    virtual ~VirtualBase() { }
    virtual void accept_crtp(MyVariant) = 0;
};

struct VirtualDerived : VirtualBase
{
    void accept_crtp(MyVariant var) override
    {
        std::visit([/*this*/](const auto& crtp){ /*...*/ }, var);
    }
};
Jarod42
  • 203,559
  • 14
  • 181
  • 302
  • Well, `std::variant` -- or more precisely `std::visit()` -- implements a dynamic dispatch, too, much like virtual inheritance just without a vtable. So this is as good as not using CRTP but regular virtual inheritance in the first place. – Immanuel Haffner Dec 07 '21 at 09:33
0

If you write a CRTP base with the different accept_crtp() overloads that all delegate to a derived class' method, that derived class' method can be a template. That CRTP base can also be used to implement a virtual base:

// declare virtual interface
struct VirtualBase
{
    virtual ~VirtualBase() { }
    virtual void accept_crtp(const CRTPBase<A> &o) = 0;
    virtual void accept_crtp(const CRTPBase<B> &o) = 0;
};

// implement virtual interface by delegating to derived class generic method
template<typename DerivedType>
struct CRTPDerived : VirtualBase
{
    using derived_type = DerivedType;

    virtual void accept_crtp(const CRTPBase<A> &o)
    { static_cast<derived_type*>(this)->accept_any_crtp(o); }

    virtual void accept_crtp(const CRTPBase<B> &o)
    { static_cast<derived_type*>(this)->accept_any_crtp(o); }
};

// implement generic method
struct VirtualDerived : CRTPDerived<VirtualDerived>
{
    private:
    template<typename T>
    void accept_any_crtp(const CRTPBase<T> &o) {
        /* one logic to handle any CRTPBase<T> */
    }
};
Ulrich Eckhardt
  • 16,572
  • 3
  • 28
  • 55
0

I have found a convenient solution to my problem. It scales well, meaning that the amount of code grows linearly with the number of virtual methods (rather than having number of virtual methods times number of CRTP classes). Further, my solution resolves the actual type of CRTPBase<T> at compile time; no dynamic dispatch except for the virtual method call. Thanks to Ulrich Eckhardt for pointing me in the right direction with his idea of using CRTP in the class hierarchy of VirtualBase.

I will describe how to solve this for a single method. This process can then be repeated for each method. The idea is to generate a purely virtual method in the VirtualBase for each concrete type of CRTPBase<T> and to generate implementations of these methods in all derived classes. The problem with generating methods at compile time is that templates do not allow us to generate method names. The trick here is to exploit overloading semantics and use a tag type to perform tag dispatching.

Let me explain along the example. Given the CRTP hierarchy (note that i slightly changed it for demonstrational purpose)

template<typename Actual>
struct CRTPBase
{
    using actual_type = Actual;
    actual_type & actual() { return *static_cast<actual_type*>(this); }
    const actual_type & actual() const {
        return *static_cast<const actual_type*>(this);
    }

    void foo() const { actual().foo(); }
    int bar(int i) const { return actual().bar(i); }
    void baz(float x, float y) { actual().baz(x, y); }
};

struct A : CRTPBase<A>
{
    void foo() const { }
    int bar(int i) const { return i + 'A'; }
    void baz(float x, float y) { }
};

struct B : CRTPBase<B>
{
    void foo() const { }
    int bar(int i) const { return i + 'B'; }
    void baz(float x, float y) { }
};

we want to declare a virtual method bark() in the class hierarchy VirtualBase, that accepts any subclass of CRTPBase<T> as parameter. We create a helper tag type bark_t to enable overload resolution.

struct VirtualBase
{
    private:
    virtual void operator()(bark_t, const A&) const = 0;
    virtual void operator()(bark_t, const B&) const = 0;

    public:
    template<typename T>
    void bark(const T &o) const { operator()(bark_t{}, o); }
};

The template method is generic and calls to the proper operator() thanks to overload resolution. The tag type is used here to select the correct implementation. (We want to support multiple methods, not just bark().)

Next we define an implementation of operator() in the derived classes using CRTP:

template<typename Actual>
struct VirtualCRTP : VirtualBase
{
    using actual_type = Actual;
    actual_type & actual() { return *static_cast<actual_type*>(this); }
    const actual_type & actual() const {
        return *static_cast<const actual_type*>(this);
    }

    void operator()(bark_t{}, const A &o) const override { actual()(bark_t{}, o); }
    void operator()(bark_t{}, const B &o) const override { actual()(bark_t{}, o); }
};

Note that the implementation calls to some method operator() of static type Actual. We need to implement this next in the implementations of VirtualBase:

struct VirtualDerivedX : VirtualCRTP<VirtualDerivedX>
{
    template<typename T>
    void operator()(bark_t, const T &o) { /* generic implementation goes here */ }
};
struct VirtualDerivedY : VirtualCRTP<VirtualDerivedY>
{
    template<typename T>
    void operator()(bark_t, const T &o) { /* generic implementation goes here */ }
};

At this point you might wonder "What did we gain here?". So far, we need to write one method operator() per actual type of CRTPBase<T>. Only in VirtualBase and VirtualCRTP, but still more than we want to write. The neat thing is, we can now generate methods operator(), both the purely virtual declarations in VirtualBase and the implementation in VirtualCRTP. To do so, I have defined a generic helper class. I put the full code with this helper class and the example on Godbolt.

We can use this helper class to declare new virtual methods that take as first parameter an instance of a list of types, as well as additional parameters. It also takes care of const-ness of the parameters and the methods.

struct bark_t : const_virtual_crtp_helper<bark_t>::
                crtp_args<const A&, const B&>::
                args<> { };
struct quack_t : virtual_crtp_helper<quack_t>::
                 crtp_args<const A&, const B&>::
                 args<int, float> { };
struct roar_t : const_virtual_crtp_helper<roar_t>::
                crtp_args<A&, B&>::
                args<const std::vector<int>&> { };

/*----- Virtual Class Hierarchy taking CRTP parameter ------------------------*/
struct VirtualBase : bark_t::base_type
                   , quack_t::base_type
                   , roar_t::base_type
{
    virtual ~VirtualBase() { }

    /* Declare generic `bark()`. */
    using bark_t::base_type::operator();
    template<typename T>
    void bark(const T &o) const { operator()(bark_t{}, o); }

    /* Declare generic `quack()`. */
    using quack_t::base_type::operator();
    template<typename T>
    void quack(const T &o, int i, float f) { operator()(quack_t{}, o, i, f); }

    /* Declare generic `roar()`. */
    using roar_t::base_type::operator();
    template<typename T>
    void roar(T &o, const std::vector<int> &v) const { operator()(roar_t{}, o, v); }
};

template<typename Actual>
struct VirtualCRTP : VirtualBase
                   , bark_t::derived_type<Actual>
                   , quack_t::derived_type<Actual>
                   , roar_t::derived_type<Actual>
{ };

struct VirtualDerivedX : VirtualCRTP<VirtualDerivedX>
{
    private:
    /* Implement generic `bark()`. */
    friend const_virtual_crtp_helper<bark_t>;
    template<typename T>
    void operator()(bark_t, const T&) const { /* generic bark() goes here */ }

    /* Implement generic `quack()`. */
    friend virtual_crtp_helper<quack_t>;
    template<typename T>
    void operator()(quack_t, const T&, int, float) { /* generic quack() goes here */ }

    /* Implement generic `roar()`. */
    friend const_virtual_crtp_helper<roar_t>;
    template<typename T>
    void operator()(roar_t, T&, const std::vector<int>&) const { /* generic roar() goes here */ }
};

struct VirtualDerivedY : VirtualCRTP<VirtualDerivedY>
{
    private:
    /* Implement generic `bark()`. */
    friend const_virtual_crtp_helper<bark_t>;
    template<typename T>
    void operator()(bark_t, const T&) const { /* generic bark() goes here */ }

    /* Implement generic `quack()`. */
    friend virtual_crtp_helper<quack_t>;
    template<typename T>
    void operator()(quack_t, const T&, int, float) { /* generic quack() goes here */ }

    /* Implement generic `roar()`. */
    friend const_virtual_crtp_helper<roar_t>;
    template<typename T>
    void operator()(roar_t, T&, const std::vector<int>&) const { /* generic roar() goes here */ }
};

In the example I declare three helper types for the three methods I want to implement. The base_type introduces the purely virtual methods to VirtualBase and the derived_type<Actual> imports the implementations of these methods. To do so, I use virtual inheritance to resolve the occuring dreaded diamond ;)

One downside is that one has to declare virtual_crtp_helper types as friend in the derived classes. Maybe someone knows how to avoid that?

To sum up: To add a method to the class hierarchy, one has to

  1. Declare a helper type for the method using virtual_crtp_helper<T> or const_virtual_crtp_helper<T>.
  2. Have VirtualBase inherit from this type's base_type and define the method as generic template.
  3. Have VirtualCRTP<Actual> inherit from the helper type's derived_type<Actual>.
  4. For each derived class, implement the actual logic in templated and tagged operator().

I am happy to hear your thoughts and am looking forward to improvements.

Immanuel