Home > Software engineering >  How to reduce recursive variadic inheritance code bloat?
How to reduce recursive variadic inheritance code bloat?

Time:10-13

Let's say I want to create a variadic interface with different overloads for the structs A,B,C:

struct A{};
struct B{};
struct C{};

template <typename ... Ts>
class Base;

template <typename T>
class Base<T>{
  public:
    virtual void visit(const T& t) const
    {
      // default implementation
    }
};

template<typename T, typename ... Ts>
class Base<T, Ts...>: Base<T>, Base<Ts...>{
public:
  using Base<T>::visit;
  using Base<Ts...>::visit;   
};

int main()
{
    A a;
    B b;
    auto base = Base<A,B,C>{};
    auto base2 = Base<A,C,B>{};
    base.visit(a);
    base2.visit(b);
}

Now funtionally Base<A,B,C> is identical to Base<A,C,B> but the compiler still generates the different combinations. Of course with more template parameters it gets worse.

I assume there is some meta programming magic which can cut this code bloat down. One solution might be to define template<typename T, typename U> Base<T,U> in a way that it checks if Base<U,T> already exists. This could reduce at least some combinations and can probably be done by hand for triplets as well. But I am missing some meta programming magic and hoping for a more general approach.

Edit: I would like to have the variadic Interface for a (simplified) use case like that:

class Implementation:public Base<A,B,C>
{
  public:
       void visit(const A& a) const
       {
           std::cout <<"Special implementation for type A";
       }

       void visit(const B& a) const
       {
           std::cout <<"Special implementation for type B";
       }

       // Fall back to all other types.
};

using BaseInterface = Base<A,B,C>;
void do_visit(const BaseInterface& v)
{
    v.visit(A{});
    v.visit(B{});
    v.visit(C{});
}

int main()
{
    std::unique_ptr<BaseInterface> v= std::make_unique<Implementation>();
    do_visit(*v);
}

The reason why I want to do this is that there could be potentially a lot of types A,B,C,... and I want to avoid code duplication to define the overload for each type.

CodePudding user response:

Looks like the member function should be a template rather than the class.

struct A{};
struct B{};
struct C{};

class Foo {
  public:
    template<typename T>
    void visit(const T& t) const
    {
      // default implementation
    }
};


int main()
{
    A a;
    B b;
    auto foo = Foo{};
    foo.visit(a);
    foo.visit(b);
}

https://godbolt.org/z/nTrYY6qcn

I'm not sure what is your aim, since there is not enough details. With current information I think this is best solution (there is a also a lambda which can address issue too).

CodePudding user response:

Base<A, B, C> instantiates Base<A>, Base<B, C>, Base<B>, Base<C> and
Base<A, C, B> instantiates Base<A>, Base<C, B>, Base<B>, Base<C>

Whereas final nodes are needed, intermediate nodes increase the bloat.

You can mitigate that issue with:

template <typename T>
class BaseLeaf
{
public:
    virtual ~BaseLeaf() = default;
    virtual void visit(const T& t) const
    {
      // default implementation
    }
};

template <typename... Ts>
class Base : public BaseLeaf<Ts>...
{
public:
    using BaseLeaf<Ts>::visit...;
};

Demo

Base<A,B,C> and Base<A,C,B> are still different types.

To be able to have same type, they should alias to the same type, and for that, ordering Ts... should be done in a way or another.

CodePudding user response:

It's necessary to enforce some sort of discipline on the order of template parameters. You can do this with a template variable and a few static_asserts:

#include <type_traits>

template <typename ... Ts>
class Base;

struct A
{
};

struct B
{
};

struct C
{
};

struct D
{
};

template <class T>
static constexpr int visit_sequence_v = -1;

template <>
constexpr int visit_sequence_v<A> = 0;

template <>
constexpr int visit_sequence_v<B> = 1;

template <>
constexpr int visit_sequence_v<C> = 2;

template <typename T>
class Base<T>{
  public:
    static_assert(visit_sequence_v<T> >= 0, "specialize visit_sequence_v for this type");
    virtual void visit(const T& t) const 
    { 
       // do nothing by default
    }
};

template<typename T1, typename T2>
class Base<T1, T2>: Base<T1>, Base<T2>
{
public:
  static_assert(std::is_same_v<T1, T2> || visit_sequence_v<T1> < visit_sequence_v<T2>);
  using Base<T1>::visit;
  using Base<T2>::visit;   
};

template<typename T1, typename T2, typename ... Ts>
class Base<T1, T2, Ts...>: Base<T1>, Base<T2, Ts...>
{
public:
  static_assert(std::is_same_v<T1, T2> || visit_sequence_v<T1> < visit_sequence_v<T2>);
  using Base<T1>::visit;
  using Base<Ts...>::visit;   
};

int main()
{
    A a;
    B b;
    auto base = Base<A,B,C>{};
    //auto base2 = Base<A,C,B>{}; // static_assert fails 
    //auto base3 = Base<A,B,C,D>{}; // forgot to specialize
    base.visit(a);
}

Notice the point here is to cause a compilation failure if you get the order wrong. If someone has the chops to do a compile-time sort it may be possible to cobble up a traits class (or a template function that you can use decltype on the return type) that selects an implementation of Base in the correct order.

One alternative is to declare a full specialization of Base for every individual type that can be visited (supplying a "default implementation" for visit) and declare a static constexpr visit_sequence within each specialzation.

A problem inherent in your method is that in the case of multiple inheritance, visit can be ambiguous:

struct E: public A, public B
{
};

// 5 MiNuTES LATeR...

DescendantOfBase<A, B> a_b;
E e;
a_b.visit (e); // ambiguous
  • Related