Search code examples
c++templatesparameter-passingperfect-forwarding

Parameter pack template class perfect forwarding is not working


I am trying to use perfect forwarding with parameter pack definitions made for a whole class, not just for the specific function. example:

#include <tuple>

template<typename Ret, typename... Params>
class Example {
public:

    Ret call(Params&&... data)
    {

    }

    template <typename ...TParams> Ret
    call1(TParams&&... data)
    {

    }

    private:
};

int main() {
    Example<void, int, short> example;
    int i = 32;
    //example.call(i, 0);
    example.call1(i, 0);
}

But the compiler makes a difference between call and call1. I thought that both examples shall work. If you uncomment "call" the compiler gives the error:

" rvalue reference to type 'int' cannot bind to lvalue of type 'int'"

My problem exactly: I wanted to create a call-system where you can derive and override the call function. But you can't override templated functions.

If I use it without the Rvalue-References it's working fine. But since my architecture has at least one deeper call since the calling class is just derived by another class which has the same template with that parameter pack which then is std::forwarded. That's where I came to the topic of perfect forwarding, which is not useable this way.

So the idea was to use it like this:

#include <tuple>
template<typename Ret, typename... Params>
class Base {
public:

    Ret call(Params&&... data)
    {

    }

};

template<typename Ret, typename... Params>
class Derived : public Base<Ret, Params...> {
public:

    Ret call(Params&&... data)
    {
        Base<Ret, Params...>::call(std::forward<Params>(data)...);
    }

    template <typename ...TParams> Ret
    call1(TParams&& ...)
    {

    }

    private:
};

int main() {
    Derived<void, int, short> MyDerived;
    int i = 32;
    //MyDerived.call(i, 0);
    MyDerived.call1(i, 0);
}

But of course this does not work.

So using it without "&&" does work but as said with the additional stack-memory per deeper call.

#include <tuple>
template<typename Ret, typename... Params>
class Base {
public:

    Ret call(Params... data)
    {

    }

};

template<typename Ret, typename... Params>
class Derived : public Base<Ret, Params...> {
public:

    Ret call(Params... data)
    {
        Base<Ret, Params...>::call(std::forward<Params>(data)...);
    }

    template <typename ...TParams> Ret
    call1(TParams&& ...)
    {

    }

    private:
};

int main() {
    Derived<void, int, short> MyDerived;
    int i = 32;
    MyDerived.call(i, 0);
    MyDerived.call1(i, 0);
}

It works if you pass the parameters by std::move :

#include <tuple>
template<typename Ret, typename... Params>
class Base {
public:

    Ret call(Params&&... data)
    {

    }

};

template<typename Ret, typename... Params>
class Derived : public Base<Ret, Params...> {
public:

    Ret call(Params&&... data)
    {
        Base<Ret, Params...>::call(std::forward<Params>(data)...);
    }

    template <typename ...TParams> Ret
    call1(TParams&& ...)
    {

    }

    private:
};

int main() {
    Derived<void, int, short> MyDerived;
    int i = 32;
    MyDerived.call(std::move(i), std::move(0));
    MyDerived.call1(i, 0);
}

Besides this produces a load of code, I do not want the Users of my API to always use std::move, just pass the parameters as normal.

To complexify a bit, an additional case which shall also work the same behaviour but with a composit instead of an inheritance. This leads definetely to an additional stack allocation of the pack expansion for the call of the Composit, which I want to optimize, since this is not needed:

#include <tuple>
template<typename Ret, typename... Params>
class Composit {
public:

    Ret call(Params&&... data)
    {

    }

};

template<typename Ret, typename... Params>
class MainClass {
public:

    MainClass() : callee(new Composit<Ret, Params...>())
    {}

    Ret call(Params&&... data)
    {
        callee->call(std::forward<Params>(data)...);
    }

    template <typename ...TParams> Ret
    call1(TParams&& ...)
    {

    }

    private:
    Composit<Ret, Params...>* callee;
};

int main() {
    MainClass<void, int, short> MyClass;
    int i = 32;
    MyClass.call(std::move(i), std::move(0));
    //MyClass.call1(i, 0);
}

Is there any solution to solve this problem without std::move?

Code was run on a clang-compiler: https://godbolt.org/z/vecsqM9sY You can simply copy the examples inside.


Solution

  • TLDR: template parameter matching against T&& results in one of 4 reference types: T&, T&&, const T&, const T&&. When automatic parameter matching is not possible and you specify the template parameter manually at a class level instead, then you basically have to pick one of these 4 reference types. E.g.: Derived<void, const int&, const short&> MyDerived; Perfect forwarding will still work in the sense it will perfectly forward the type you chose. But it will not directly work if you only want to specify the base type T and want the member function to work for either of these 4 reference types.

    More detailed answer following:

    Template parameter with perfect forwarding allows all 4 possible reference types of a base type myclass passed to a function template<typename T> A(T&& var) to be forwarded perfectly to another function template<typename T> B(T&& var). But first let's look at how these possible reference types passed to A are matched to T:

    • myclass& => matches using T=myclass&
    • const myclass& => matches using T=const myclass&
    • myclass&& => matches using T=myclass, but T=myclass&& has the same result
    • const myclass&& => matches using T=const myclass, but T=const myclass&& has the same result

    The basic trick in this matching is that you can combine reference types & and && in type declarations and these are resolved as follows:

    • myclass & && results in myclass &
    • myclass && && results in myclass &&
    • myclass && results in myclass &&

    First, looking at the possible parameter matchings, then the end results are all references, but the deducted template parameter is one of four: myclass&, const myclass&, myclass, const myclass.

    Second, as you can see if you give myclass as template parameter that essentially implies myclass&& as function parameter. This happens in your example: since you specify only the base type (int, short) as template parameter, this implies an rvalue reference parameter in your member function specification.

    I would suggest to avoid using base type names myclass and always choose which reference type you want explicitly, since template parameter T=myclass&& also implies function parameter type myclass&&.

    So what about perfect forwarding? If var was an rvalue reference for A then, as a named variable, it automatically gets converted to an lvalue reference when passed to B. Thus you cannot directly forward rvalue references. This can be solved via std::forward which converts an lvalue reference back to an rvalue reference when T=myclass, T=const myclass, T=myclass&& or T=const myclass&&. But this crucially depends on the original template matching for A.

    Lastly, one of the problems with the current solution is that it is not flexible when you want to pass both named variables (i) and unnamed variables/explicit values (0). This is because named variables of type T=int can be bound to:

    • T&
    • const T&
    • but not to T&&

    While an explicit value (or unnamed variable on the right hand) of type T=int can be bound to:

    • T&&
    • const T&&
    • const T&
    • but not to T&

    So if you are not modifying the variable then T=const int& can be bound to all cases. But when you want to modify the variable then it makes sense to choose int&, but that doesn't allow passing explicit values (like 0).

    Though that does not seem to be question here, but I think it would be possible in theory to specify the base type at class level and have templated member functions that would then take one of the 4 reference types. But that would require a more intricate solution and templated member functions in the base class and derived classes.