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);
}
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?
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]
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.
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
).