What are the ranking rules for reference bindings? Let’s consider the following example:

void f(int);         // #1
void f(int&);        // #2
void f(int const&);  // #3
void f(int&&);       // #4
void f(int const&&); // #5

Here are some key points:

  1. #1 is a function that accepts borrowed int arguments.
  2. #2 is a function that takes an lvalue reference.
  3. #3 is a function that takes a const lvalue reference, which accepts both lvalue and xvalue arguments.
  4. #4 is a function that takes an rvalue reference.
  5. #5 is a function that takes a const rvalue reference, which accepts an xvalue with a const qualifier.
  6. All references bind to lvalues/xvalues.
  7. Numeric literals are non-const.

Case 1 (#1 and #2)

int x = 1;
f(x); // ambiguous!

It’s ambiguous because binding to a reference of the correct type is treated as an exact match, just like binding to a non-reference of the correct type.

To avoid ambiguity, we have a few tricks:

f(+x);                            // calls #1 by using the unary plus
f(int(x));                        // calls #1 by explicitly copying the object
f(const_cast<int const&>(x));     // calls #1 by using const_cast
f(std::cref(x));                  // calls #1 by using std::cref
f(static_cast<int&&>(x));         // calls #1 by casting the argument type
static_cast<void(*)(int)>(f)(x);  // calls #1 by casting the function type
static_cast<void(*)(int&)>(f)(x); // calls #2 by casting the function type

Case 2 (#1, #2, and #3)

int x = 1;
f(x); // ambiguous!

To avoid ambiguity, we must cast the function type(best avoided):

static_cast<void(*)(int)>(f)(x);  // calls #1 by casting the function type
static_cast<void(*)(int&)>(f)(x); // calls #2 by casting the function type

Case 3 (#2 and #3)

#2 is the better choice for lvalue references, while #3 is preferable for const lvalue references. The prvalue is materialized and the const lvalue reference binds it.

int x = 1;
int const& y = x;
f(x);                         // calls #2
f(+x);                        // calls #3
f(const_cast<int const&>(x)); // calls #3
f(1);                         // calls #3
f(y);                         // calls #3

Case 4 (#1 and #3)

There is absolutely no way to distinguish between #1 and #3 for the compiler.

int x = 1;
int const& y = x;
f(x);                                   // ambiguous
f(y);                                   // ambiguous
f(const_cast<int const&>(x));           // ambiguous
f(+x);                                  // ambiguous
f(static_cast<int&&>(x));               // ambiguous
f(1);                                   // ambiguous
static_cast<void(*)(int)>(f)(x);        // calls #1
static_cast<void(*)(int const&)>(f)(x); // calls #3

Case 5 (#3 and #5)

The rank of #5 is better than the rank of #3 for rvalues.

f(x);                                    // calls #3
f(+x);                                   // calls #5
f(static_cast<int&&>(x));                // calls #5
f(const_cast<int const&>(x));            // calls #3
f(int(x));                               // calls #5
static_cast<void(*)(int const&)>(f)(x);  // calls #3
static_cast<void(*)(int const&&)>(f)(x); // error! cannot bind rvalue reference to lvalue

Case 6 (#3 and #4)

The behavior is similar to Case 5.

Case 7 (#4 and #5)

#5 is less good than #4 because binding int const&& to xvalues requires a qualification conversion.

int x = 1;
int const&& y = 1;
f(x);                           // error! cannot bind rvalue reference to lvalue
f(+x);                          // calls #4
f(static_cast<int&&>(x));       // calls #4
f(int(x));                      // calls #4
f(const_cast<int const&&>(y));  // calls #5
f(const_cast<int&&>(y));        // calls #4
f(static_cast<int const&&>(y)); // calls #5
f(static_cast<int&&>(y));       // error! casts `const int` to `int&&`
f(std::move(y));                // calls #5
f(1);                           // calls #4
f(static_cast<int const&&>(1)); // calls #5

Case 8 (#1 and #4)

It is ambiguous even though the by-value candidate is clearly the better choice.

int x = 1;
int&& y = 1;
f(x);                                         // calls #1
f(+x);                                        // ambiguous
f(y);                                         // calls #1
f(std::move(y));                              // ambiguous
f(1);                                         // ambiguous
f(static_cast<int>(1));                       // ambiguous
f(static_cast<int&&>(1));                     // ambiguous
f(const_cast<int const&&>(y));                // calls #1
f(std::move(x));                              // ambiguous
static_cast<void(*)(int&&)>(f)(std::move(y)); // calls #4
static_cast<void(*)(int&&)>(f)(+x);           // calls #4
static_cast<void(*)(int)>(f)(+x);             // calls #1
static_cast<void(*)(int&&)>(f)(1);            // calls #4
static_cast<void(*)(int)>(f)(1);              // calls #1

Leave a Reply

Your email address will not be published. Required fields are marked *

You can use the Markdown in the comment form.