Home > Software engineering >  CRTP Parameter for Virtual Method of Class Hierarchy
CRTP Parameter for Virtual Method of Class Hierarchy

Time:12-07

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

CodePudding user response:

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 : VirtualBase
{
    private:
    template<typename T>
    void accept_any_crtp(const CRTPBase<T> &o) {
        /* one logic to handle any CRTPBase<T> */
    }
};

CodePudding user response:

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);
    }
};
  • Related