1

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.

NetoBF
  • 127
  • 6
  • I'll be watching for an answer, but, [this](https://stackoverflow.com/questions/20583531/lvalue-to-rvalue-reference-binding) seems to address the issue. – lakeweb Nov 26 '21 at 21:11
  • Of course you cannot use forwarding references in a virtual function. They can only be used in function templates. Each combination of value categories in parameters entails a different instantiation, which doesn't make sense for virtual functions. But you don't seem to have any virtual functions or overrides, despite claiming to want them, so it's a bit unclear what exactly you want. – n. m. could be an AI Nov 26 '21 at 21:21
  • Parameter forwarding works with forwarding references, which can only be used in a function. Trying to use it in a class makes very little sense, and on top of that the memory usage will be the same for passing r-value references and l-value references. – super Nov 26 '21 at 21:21
  • @lakeweb thx, I edited with a more special description with a Composit for my case. Seems I can't come around std::move or the additional stack allocation. – NetoBF Nov 26 '21 at 21:25
  • @super are you sure? at least with a composite class I experience additional stack-memory usage with the same pack expansion for a call to an object with the same template parameters' call-function. I want to erase that additional usage because it's not needed. – NetoBF Nov 26 '21 at 21:27
  • 1
    @NetoBF Yes. I'm sure. Passing a reference is just passing a reference. It could be optimized away, it could be inlined or it could simply be passed (implementation defined, but most likely as a pointer). Using perfect forwarding will have no impact on that. It will come down to how aggressively the compiler can optimize your code (not very, if you're planning to use virtual dispatch, witch is a run-time thing after all). Whatever differences you saw in memory usage probably has it's explanation in the code. Not in your assumption about what perfect forwarding is or how it works. – super Nov 26 '21 at 21:31
  • 1
    In your example, you know that `Composit::call` accepts only rvalue references. There is no need in perfect forwarding, you can use `std::move` instead of `std::forward`. You only need perfect forwarding when you don't know which kind of reference your callee accepts. Since ultimately `Composit::call` *only* accepts rvalue references, you can *only* pass it rvalue references, directly or via a wrapper. So if you have an lvalue, you need to `std::move` it. This has nothing to do with the wrapper using or not using perfect forwarding to ultimately pass it to `Composit::call`. – n. m. could be an AI Nov 26 '21 at 21:44
  • 1
    `call()` accepts simple R-value references. No perfect forwarding happens on those. (More precisely, `std::move` equals `std::forward` in their case.) `call1()` accepts universal references (which, unfortunately, use the same, context-dependent `&&` syntax in C++). In that case the effects of `std::move` and `std::forward` differ as expected — `std::move` turns everything into R-value references whereas `std::forward` does perfect-forwarding (i.e. (only) R-value references are passed on as R-value references (rather than the L-value references that R-value-typed variables implicitly become)). – Andrej Podzimek Nov 27 '21 at 07:11
  • Have you tried using references for the explicit template parameters you want to perfectly forward?: `Derived MyDerived;`. Template parameter matching with && should always end up with either a rvalue (&&) or lvalue reference (&) as type, but you are passing neither. – Marc Stevens Nov 27 '21 at 08:33
  • hey @MarcStevens yes that's right because I wanted to present an API to the user which takes the passed types exaclty as they are. Which means an int is an int at the backend and an int& is an int& at the backend. so the concrete used type shall not matter to my implementation. – NetoBF Dec 20 '21 at 15:33

1 Answers1

2

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.

Marc Stevens
  • 1,628
  • 1
  • 6
  • 16