c++referenceoverloadingrvalue-referencelvalue-to-rvalue

Integer parameter calls float overload and float parameter calls integer overload


Today I ran into roughly the following code:

#include <iostream>

void f(float&& f) { std::cout << f << "f "; }
void f(int&& i) { std::cout << i << "i "; }

int main()
{
    int iv = 2; float fv = 1.0f;
    f(2);  f(1.0f);
    f(iv); f(fv);
}

Godbolt link

The first two f-calls print 2i 1f, as expected.

Now for the second line, I would have expected that it either doesn’t compile at all, since iv and fv are not temporaries (and thus can't bind to an r value reference), or that it creates a copy of the variable to pass to the function, and thus print 2i 1f a second time.

However, somehow it prints 2f 1i, which is just about the last thing I would have expected.

If you copy the code into cppinsights, it transforms the calls into

f(static_cast<float>(iv));
f(static_cast<int>(fv));

So it seemingly very intentionally casts the integer to a float, and the float to an integer, but I don't have any idea why it does that, and don't really know how to google that either. Why does this happen? What are the rules that lead to this result?


Solution

  • The behavior of the program can be understood from reference-initialization.

    From dcl.init#ref-5.4:

    [Example 6:

    double d2 = 1.0;
    double&& rrd2 = d2;                 // error: initializer is lvalue of related type
    int i3 = 2;
    double&& rrd3 = i3;                 // rrd3 refers to temporary with value 2.0
    

    -end example]


    Case 1

    Here we discuss why void f(int&& i) isn't viable for the call f(iv).

    The lvalue iv cannot be bind to the rvalue reference parameter i in void f(int&& i) for the call f(iv) and so the overload f(int&&) isn't viable. Basically, int&& i = iv; isn't allowed because iv is an lvalue of related type.


    Case 2

    Here we discuss why void f(float&& i) is viable for the call f(iv).

    For the call f(iv) the overload void f(float&& f) is viable because here first the initializer expression iv is implicitly converted to a prvalue of the destination type(float) and then temporary materialization can happen such that the parameter f can be bound to that materialized temporary 2.0f(which is an xvalue).


    Similarly for the call f(fv), the overload void f(float&& i) isn't viable because fv is an lvalue of related type. And for the call f(fv) the overload void f(int&& i) can be used because first the initializer is implicitly converted to a prvalue and then temporary materialization happens such that i can be bound to the materialized temporary 1(of type int).