Rvalue Reference Parameters and Template Functions

Rvalue reference parameters and template functions

The behavior of deduction in template parameters is unique, and is the reason your template version works. I've explained exactly how this deduction works here, in the context of another question.

Summarized: when the argument is an lvalue, T is deduced to T&, and T& && collapses to T&. And with the parameter at T&, it is perfectly valid to supply an lvalue T to it. Otherwise, T remains T, and the parameter is T&&, which accepts rvalues arguments.

Contrarily, int&& is always int&& (no template deduction rules to coerce it to something else), and can only bind to rvalues.

How to pass a rvalue reference parameter to a template operator() function in C++?

Problem 1

One problem is at the line std::apply(m_function, all_args); where you are passing all_args as an lvalue to std::apply, which will pass it as an lvalue to func_1's third parameter, which will fail, because func_1's third parameter is an rvalue ref, which can't bind to an lvalue argument.

Indeed, changing that line to std::apply(m_function, std::move(all_args)); makes the first two // Compile Error lines actually compile and generate the correct output. Likewise, I would call std::move also on the other usage of all_args.

Problem 2

It looks like std::make_tuple(std::forward<NewArgs>(args)...); is not doing what you think it does. Changing it to std::tuple<NewArgs&&...>(std::forward<NewArgs>(args)...); solves the problem; that is equivalent to std::forward_as_tuple(std::forward<NewArgs>(args)...);.

The details of why this change works lie in the return types of std::make_tuple vs. std::forward_as_tuple: the latter returns a tuple of references, whereas the former returns a tuple of values which have been copied/moved from the arguments.

Now, follow my reasoning:

  • First, look at curried(Function function, std::tuple<CapturedArgs...> args): it takes the paramter args which should be of type std::tuple<CaptureArgs...>. Are we sure args has that type? Well, if template type deduction took place, it would be obvious that the answer is yes. However, the call to that constructor never takes advantange of type deduction, because the only call is in return curried<Function, CapturedArgs..., NewArgs...>(m_function, all_args); where the template arguments are explicitly provided.
  • So the question remains: is all_args of the type the construtor expects? Well, the template arguments CapturedArgs..., NewArgs... in the recursive call correspond to the class... CapturedArgs template parameters of the class, which are used to form the type of the argument of the constructor, std::tuple<CaptureArgs...>.
  • So the answer to this question is given by static_asserting, before the recursive return, that all_args is of type std::tuple<CapturedArgs..., NewArgs...>:
    static_assert(std::is_same_v<decltype(all_args), std::tuple<CapturedArgs..., NewArgs...>>);
    return curried<Function, CapturedArgs..., NewArgs...>(m_function, all_args);
  • Unfortunately you can't put this assertion in the code, as long as you pass values wrapped in std::ref/std::cref, because those fail the static_assertion but are still valid inputs, exactly because of how std::reference_wrappers work. You could write a more complicated assertion or you can momentarily change std::ref(bla) to bla and so on, and check that the static_assert I gave you passes when using std::forward_as_tuple and fails when using std::make_tuple.

Thanks for asking this question. It was a great opportunity for me to dive again into this complicated topic and finally understand it!

One more point

Above I suggested that you use std::forward_as_tuple(std::forward<NewArgs>(args)...);.

Well, probably this suggestion is wrong.

At page 238, the author explicitly states that he wants the tuple to store copies, to prevent scenarios where the curried function survives its arguments. Therefore, it's probably best to go with this instead (notice, there's no && in the template argument passed to std::tuple):

        auto new_args = std::tuple<NewArgs...>(std::forward<NewArgs>(args)...);

rvalue reference in class template vs function template

Your confusion is probably rooted in your assumption that in both cases T is int. This is why you presume that these two cases as similar. In reality they are not.

In the class version you are manually specifying what T is. You explicitly tell the compiler that T is int. Constructor parameter type T && in this case becomes int &&, which cannot bind to a regular lvalue. Hence the error.

In the function version you don't tell the compiler what T is, but instead you expect the compiler to deduce it. In situations like yours the language is deliberately designed to deduce T as int & (note: not as int, but rather as int &). Once T is deduced as int &, the so called "reference collapsing" rules lead to function parameter type T && becoming int & - an ordinary lvalue reference. This parameter can successfully bind to lvalue argument i.

That explains the difference you observe.

For the sake of experiment, in the latter case you can suppress template argument deduction and specify the template argument explicitly

succeed<int>(i); 

That will forcefully specify T as int and lead to the very same error as in the class version for the very same reason.

Similarly, you can "simulate" function's behavior for your class by specifying the template argument as int &

test<int &> t(i);

The same "reference collapsing" rules will make your constructor invocation to compile successfully.

How to have a templated function require its argument be passed by rvalue reference?

template<class T>
void f(T &&)
requires(!std::is_lvalue_reference_v<T>);

C++, rvalue references in function parameters

Like @Peter said, the type of T is deduced as string&, and C++’s reference-collapsing rule says:

T& & ⇒ T& // from C++98

T&& & ⇒ T& // new for C++0x

T& && ⇒ T& // new for C++0x

T&& && ⇒ T&& // new for C++0x

So func’s instantiation is actually:

void func(string& str)

And it works.

How to make template rvalue reference parameter ONLY bind to rvalue reference?

You can restrict T to not be an lvalue reference, and thus prevent lvalues from binding to it:

#include <type_traits>

struct OwnershipReceiver
{
template <typename T,
class = typename std::enable_if
<
!std::is_lvalue_reference<T>::value
>::type
>
void receive_ownership(T&& t)
{
// taking file descriptor of t, and clear t
}
};

It might also be a good idea to add some sort of restriction to T such that it only accepts file descriptor wrappers.

Why a template argument of a rvalue reference type can be bound to a lvalue type?

You can read this article Universal References in C++11 to understand. Here some part of it:

If a variable or parameter is declared to have type T&& for some deduced type T, that variable or parameter is a universal reference.

Widget&& var1 = someWidget;      // here, “&&” means rvalue reference

auto&& var2 = var1; // here, “&&” does not mean rvalue reference

template<typename T>
void f(std::vector<T>&& param); // here, “&&” means rvalue reference

template<typename T>
void f(T&& param); // here, “&&”does not mean rvalue reference

Here a relevant for your case excerpt from the Standard:

... function template parameter type (call it P) ... If P is a forwarding reference and the argument is an lvalue, the type “lvalue reference to A” is used in place of A for type deduction.

C++ pass parameter by rvalue reference if possible, otherwise copy the lvalue reference

The reference says that it only accepts rvalue reference parameters.

No, this is forwarding reference, which could serve as both lvalue reference and rvalue reference, according to the value category of the passed-in argument.

How can I make foo work like this?

The point of declaring a forwarding reference is (1) type deduction is necessary, that means you need to make foo a function template here; (2) the parameter x has the exact form of T&& for the template parameter T. e.g.

template <typename T>
void foo(T&& x){
x = 2;
}

then

int x = 1;
foo(x); // lvalue passed, T is deduced as int&, parameter's type is int&
foo(1); // rvalue passed, T is deduced as int, parameter's type is int&&

Note this is true for std::make_tuple too, even it's using template parameter pack. And better to bear in mind that even forwarding reference looks like rvalue reference but they're different things.

BTW: std::forward is usually used with forwarding reference to preserve the value category of the argument e.g. when forwarding it to other functions.

C++ lvalues and rvalues in template functions

Yes it's possible, just add a const to your second overload:

template<typename T>
void overloaded (const T& x);
template<typename T>
void overloaded (const T&& x);
// ^^^^^

The reason why you need to const is to make x not a forwarding reference. Forwarding references are very greedy, and if you don't pass in the exact same type (including any cv qualifiers) then the forwarding reference overload will get chosen.

In your case, because you do not pass a const object to overload, the second overload will always be a better match.

But if you add a const there, then it's not a forwarding reference anymore and can only accept rvalues and no lvalues, and won't be a better match for an lvalue as a result but will be a better match for any rvalues than the const& overload.


If you need to move from x, then you will have to do something else. Remove the const& overload and branch in the forwarding reference whether you have an rvalue or lvalue:

template <typename T> void overloaded(T &&x) {
if /*constexpr*/ (std::is_lvalue_reference_v<T>)
std::cout << "[lvalue]";
else
std::cout << "[rvalue]";
}

Note: You'll need to use if constexpr if you do specific stuff that is not valid for a branch or the other.



Related Topics



Leave a reply



Submit