GGS
GGS

Reputation: 67

Is there a clean(er) way to mix CRTP with variadic inheritance?

Originally I wasn't able to find a way to do this, but as I was formulating this question new search terms came to mind and I finally found that answer. I figure this post can both act as a redirect for anyone with the same problem (since it took a while to find) and I'd also like to see if there's any way to improve the syntactic sugar a bit as that answer is 9 years old and some modern features weren't available at the time. The code from that answer:

#include <utility>

template <template<class> class... Mixins>
class Host : public Mixins<Host<Mixins...>>...
{
  public:
    Host(Mixins<Host>&&... args) : Mixins<Host>(std::forward<Mixins<Host>>(args))... {}
};

template <class Host> struct Mix1 {};
template <class Host> struct Mix2 {};

int main (void)
{
  typedef Host<Mix1, Mix2> TopHost;
  delete new TopHost(Mix1<TopHost>(), Mix2<TopHost>());
}

The goal is to remove the need for redundant TopHost usage in every mixin, as it's both slightly annoying and (more importantly) could really screw something up if the wrong type was accidentally used with CRTP. I have a sneaking suspicion it might be possible since calling a templated type's constructor with variadic arguments is certainly possible (e.g. new T(args...)).

Ideally we'd be able to use syntax like:

auto mix = TopHost(Mix1(args1...), Mix2(args2...), ...);

Or even (somehow) have the mixins deduced from the using statement:

auto mix = TopHost({args1...}, {args2...}, ...);

EDIT: So, yes, indeed, I offered the solution to my own problem without knowing. The second syntax works. As mentioned, it doesn't require that use so there's still the possibility of user error. @Evg's solution does force that, and although it's more verbose it technically answers the question so I'll accept that answer (for now).

Now the problem I run into is that in my application the mixins have deleted copy constuctors and both methods create copies (of course, that's how the original class is designed). So now the question becomes: Is there a way to achieve the syntax without making copies? I've been trying to get something like this to work but can't seem to wrap my head arround how to expand differently sized variadic templates:

template < typename... > struct TypeList {};
template<typename TypeList> struct Variad;
template<typename... Types> struct Variad<TypeList<Types...>> {};

template<template<class> typename ...Args> struct Mixins;
template< class TemplList, class... Lists> struct Host;

template< template<class> class ...Components,  template<class...> class T, class... Variads>
struct Host<Mixins<Components...>, T<Variads>...> : 
  public Components<Host<Mixins<Components...>, T<Variads>...>>...
{
    Host(T<Variads>&&... args) : Components<Host<Mixins<Components...>, T<Variads>...>>(std::forward<T<Variads>...>(args))... {}
};

which would be used like:

int main() {
  using f1 = TypeList<int,int>;
  using f2 = TypeList<int>;
  using m1 = Mixins<Mix1, Mix2>;
  Host<m1, Variad<f1>, Variad<f2>> obj(1,2,3);
}

Not necessarily clean, but preferable in my case. I'm not quite sure how exactly the expansion in that works, but I based it on my attempt to create nested variads which appears to work correctly:


#include <iostream>
#include <typeinfo>
#include <cxxabi.h>
#define get_type(x) (abi::__cxa_demangle(typeid(x).name(), NULL, NULL, NULL))

template < typename... > struct TypeList {};
template<typename TypeList> struct Variad;
template<typename... Types> struct Variad<TypeList<Types...>> {};

template<typename ...TypeListOne> struct NestedVariad;
template<template<class...> class T, typename...Types>
struct NestedVariad<T<Types>...> { };


int main() {
  using f1 = TypeList<int,int>;
  using f2 = TypeList<int>;

  NestedVariad<Variad<f1>, Variad<f2>> obj;
  cout << get_type(obj) << endl;
}

which outputs: NestedVariad<Variad<TypeList<int, int> >, Variad<TypeList<int> > >

But using a similar approach for the mixin class passes all three arguments to each constructor instead of 2 to the first and 1 to the second

Upvotes: 2

Views: 342

Answers (1)

Evg
Evg

Reputation: 26342

As Oliv said in a comment, the second syntax works:

template <class Host> struct Mix1 {
    Mix1(int, int) {}
};

template <class Host> struct Mix2 {
    Mix2(int) {}
};

using TopHost = Host<Mix1, Mix2>;
auto mix = TopHost({1, 1}, {2});

Alternatively, you can do this:

template<class TopHost, class Mixes, class Tuple, std::size_t... ii>
auto make_mix_impl(std::index_sequence<ii...>, Tuple&& tuple)
{
    return TopHost(std::make_from_tuple<std::tuple_element_t<ii, Mixes>>(
        std::get<ii>(std::forward<Tuple>(tuple)))...);
}

template<template<class> class... Mixes, class... Tuples>
auto make_mix(Tuples&&... tuples)
{
    static_assert(sizeof...(Mixes) == sizeof...(Tuples));
    using TopHost = Host<Mixes...>;
    return make_mix_impl<TopHost, std::tuple<Mixes<TopHost>...>>(
        std::make_index_sequence<sizeof...(Mixes)>{}, 
        std::make_tuple(std::forward<Tuples>(tuples)...));
}

auto mix = make_mix<Mix1, Mix2>(std::make_tuple(1, 1), std::make_tuple(2));

Upvotes: 1

Related Questions