Universal reference vs non-template `const&`

Viewed 218

Consider the following code:

#include <iostream>
#include <type_traits>

template <class... Ts>
struct test {
    static void foo(const std::remove_reference_t<Ts>&...) {
        std::cout << "1\n";
    }

    template <class... Us>
    static void foo(Us&&...) {
        std::cout << "2\n";
    }
};

int main() {
    test<int&, double&>::foo(1, 1.0);
}

The above code prints "2". Why is the second overload considered a better match?
The first one boils down to foo(const int&, const double&) and it's a regular function, so it should be preferred, shouldn't it?

I guess it's not an "exact match", but what is not "exact" here exactly?

2 Answers

A specialization of the second foo is prefered because of the way is specified template argument deduction and overload resolution.

Template argument deduction takes place only for the second foo.

Us&& is a forwarding reference [temp.deduct.call]§3

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.

where P is the type of the function parameter without cv qualifier and without reference. A is the type of the argument. An argument is an expression and expressions never have reference type. The first argument is a prvalue of type int and the second a prvalue of type double. A prvalue is not an lvalue so P is deduced to be int for the first argument and double for the second argument.

So after template argument deduction the second overload type is void (int&&,double&&)

The first overload has type void (const int &, const double &).

After template argument deduction, the compiler must choose which of the too overloads is the best viable one. The list of rules is long but the difference happen to be that the implicit conversion sequences of the bindings of the materialized temporaries to the the reference parameters is better for the two argument bindings of the second overload than for those of the first overload [over.ics.rank]§3.2.3:

S1 and S2 include reference bindings ([dcl.init.ref]) and neither refers to an implicit object parameter of a non-static member function declared without a ref-qualifier, and S1 binds an rvalue reference to an rvalue and S2 binds an lvalue reference

The exact match is actually int&&, that’s why the second overload gets picked.
The reason is that a literal is a temporary, which can be used as an rvalue directly.
In the case where it’s used for const int &, the literal initializes another temporary which is used for the reference, by calling the move constructor (int(int &&)), akin to:

const int i = 1;
const double d = 1.0;
f(i, d);

Which would call the first overload.

Related