recursive inheritance for overloading a method for

2019-07-13 08:03发布

问题:

I need to find a way to recursively build a class given a set of template arguments so that the class inherits from itself and build a method f for the current first template argument in the list of template arguments and then inherits from itself by passing on the rest of the list.

So, basically I want to achieve the following interface for a class C:

C<T1, T2, T3> c;

c now has methods C::f(T1), C::f(T2) and C::f(T3)

My approach so far was like this:

// primary template
template <class H, class...>
class C {};

// base case where class... is empty
template <class H, class...>
class C<H>
{
public:
    void f(const H& h){
        // std::cout << typeid(h).name() << "\n";
    }
};

// recursive case where T is nonempty
template <class H, class... T>
class C : public C<T...>
{
public:
    void f(const H& h){
        // std::cout << typeid(h).name() << "\n";
    }
};

This does not actually compile, as I get

error: redefinition of 'C' class C : public C

Is my approach basically possible and just a matter of semantically and or syntactically invalid code or does this approach not work in principle?

回答1:

For starters, a class cannot inherit from itself.

Secondly, all that you apparently are trying to accomplish is to have each template parameter generate a class method that takes that class as a parameter.

In which case, something like this should work.

template<typename ...> class C;

template<>
class C<> {};

template<typename T, typename ...Args>
class C<T, Args...> : public C<Args...> {

public:

    void f (const T &)
    {
       // Whatever...
    }
};

Note that this is not a class inheriting from itself. It's a template instance that inherits from another template instance. Each template instance is a unique class.

Note that you have a single definition of the class method in question here, and not two, as you were trying to do. This is a slight improvement.

Another improvement would be to consider rearranging the class hierarchy in this manner, if it's possible to do this taking into consideration your other class requirements:

template<typename T> class F {
public:

    void f (const T &)
    {
    }
};


template<typename ...> class C;

template<>
class C<> {};

template<typename T, typename ...Args>
class C<T, Args...> : public C<Args...> , public F<T> {

};

With this approach, whether you use C<int, float>, or C<int, char *>, the class method will always be declared to be a method of F<int>. This cuts down slightly on the resulting code float, since any instance of C that includes int, for example, will generate a single class method instance, instead of two separate methods like C<int, float>::f(const int &) and C<int, char *>::f(const int &), which would, otherwise, be completely identical.



回答2:

As an alternative approach, I'm proposing a solution based on mixins. Feel free to ignore the boilerplate introduced by class type_name, the purpose of which is to show you that the right part is picked up on a per argument base.

It follows a minimal, working example:

#include<type_traits>
#include<utility>
#include<iostream>

template<typename> struct type_name;
template<> struct type_name<int> { static const char *name; };
template<> struct type_name<double> { static const char *name; };
template<> struct type_name<char> { static const char *name; };

const char * type_name<int>::name = "int";
const char * type_name<double>::name = "double";
const char * type_name<char>::name = "char";

template<typename T>
struct Part {
    void func(T t) {
        std::cout << type_name<T>::name << ": " << t << std::endl;
    }
};

template<typename... T>
struct S: private Part<T>... {
    template<typename... Args>
    void f(Args&&... args) {
        using acc_t = int[];
        acc_t acc = { 0, (Part<std::decay_t<Args>>::func(std::forward<Args>(args)), 0)... };
        (void)acc;
    }
};

int main() {
    S<int, double, char> s;
    s.f(42);
    s.f(0.1);
    s.f('c');
    s.f('a', 0.3, 23);
}

Some plus of this method:

  • Part<T> is defined only once for any T, no matter how many times you use it in different packs.

  • S<T, T> is rejected at compile-time and more in general all those packs that contain the same type two or more times. They would otherwise give births to multiple definitions of f(T) and a subsequent call would be probably ambiguous.

  • You can invoke f with a single parameter, as requested. Anyway, as shown in the example, you can invoke f with N parameters and the call is equivalent to N calls to f with a single parameter.
    In other terms, you can either use this:

    s.f('a', 0.3, 23);
    

    Or this:

    s.f('a');
    s.f(0.3);
    s.f(23);
    

    The result will be the same in both cases.
    This feature can be easily turned off if needed by defining S as it follows:

    template<typename... T>
    struct S: private Part<T>... {
        template<typename U>
        void f(U &&u) {
            Part<std::decay_t<U>>::func(std::forward<U>(u));
        }
    };
    

See it running on wandbox.


As a side note, this is the usual trick used to emulate fold expressions in C++14:

template<typename... Args>
void f(Args&&... args) {
    using acc_t = int[];
    acc_t acc = { 0, (Part<std::decay_t<Args>>::func(std::forward<Args>(args)), 0)... };
    (void)acc;
 }

You can find more about that on SO as well as on the web.