xmllmx
xmllmx

Reputation: 42379

Why does it not work when an lvalue-ref-arg and an rvalue-ref-arg are passed as the same forwarding-ref type?

#include <type_traits>

template<typename T>
void f(T&& a, T&& b)
{}

int main()
{
    int n;
    f(n, std::move(n));
}

T&& is a forwarding reference type, so I think decltype(a) should be int& and decltype(b) should be int&&.

However, the code above generates the following error:

main.cpp(13,2): error : no matching function for call to 'f' f(n, std::move(n));

main.cpp(7,6): note: candidate template ignored: deduced conflicting types for parameter 'T' ('int &' vs. 'int')

void f(T&& a, T&& b)

1 error generated.

Why does it not work when an lvalue-ref-arg and an rvalue-ref-arg are passed as the same forwarding-ref type?

My compiler is clang 4.0.

Upvotes: 4

Views: 160

Answers (2)

Dimitrios Bouzas
Dimitrios Bouzas

Reputation: 42929

The compiler error is pretty straight forward, T cannot be deduced in int& and int at the same time. In order for this to work you would have to provide an additional template argument:

template<typename T1, typename T2>
void f(T1&& a, T2&& b)
{}

Reasoning

Template argument deduction follows special rules when dealing with forwarding references (a.k.a Universal references) which are based on reference collapsing rules

  • U& & becomes U&
  • U& && becomes U&
  • U&& & becomes U&
  • U&& && becomes U&&

for example if you have a function template:

template<typename T>
void foo(T&&) {}
  1. If foo input is an lvalue of type U, T will be deducted to U&. Following the reference collapsing rules above the argument type will become U&.
  2. If foo input is a rvalue of type U, T will be deducted to U and consequently the argument type will be U&&.

Now following the reasoning above, template argument deduction will deduce T into int& for your first argument since input is an lvalue.

Template argument deduction will try to match the type of the second argument but for the second argument since input is an rvalue following the rules above T will be deduced to int.

At this point the compiler throws its hands up in the air and screams "dude T's deducted type must match for all input arguments".

Upvotes: 4

When working to non-superficial depth with forwarding references(1), it's important to understand how they actually work and not treat them as "magic."

All the "magic" of forwarding references is this ruling in the standard:

When type deduction happens for parameter type T && where T is a deduced type, and an lvalue of type U is used as the argument, the type U & is used for type deduction instead of U.

Let's take this example:

template <class T>
void foo(T&& a);

int i = 42;

What happens when you call foo(42): 42 is an rvalue of type int. Therefore, T will be deduced to int and the type of a will therefore be int &&, an rvalue reference to int.

What happens when you call foo(i): i is an lvalue of type int. Because of the forwarding reference rules, deduction will happen as if int & was the type. T is therefore deduced to int &; the type of a is therefore "int & &&", which reference-collapses to int &.

With this in mind, it's clear why your example fails. The same T would have to be deduced as both int & (because of n) and as int (because of std::move(n)). So naturally, deduction fails.

If you want each of the parameters to be a forwarding reference on its own, you need a separate template parameter for each:

template<class T, class U>
void f(T&& a, U&& b)
{}

(1) Notice that as per the proposal N4164, which has been accepted into the working paper, the preferred term is forwarding references.

Upvotes: 2

Related Questions