3

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?

2 Answers 2

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.

Sign up to request clarification or add additional context in comments.

7 Comments

thanks, but with your second design I get an error: error: member 'f' found in multiple base classes of different types
Might be able to work around it by adding using F<T>::f; and using C<Args...>::f; to the main template.
Ok, will try that, but I noticed something else: With your first design for the following program int main(){ C<char, int, double> c; int i = 10; char ch = 'a'; c.f(i); c.f(ch); } I get as output c c... meaning the only the overload for the first template argument gets called. Note that i have std::cout << typeid(T).name() << "\n"; in C::f
You probably need using as well. My answer is limited solely to declaring the methods, which is the only thing that was asked.
Hmm, I'm sorry, but I don't quite get it yet. Why exactly and also where exactly do I need using? If the class inherits the other specialisations then why is it necessary to use using somewhere?
|
1

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.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.