Integer parameter calls float overload and float parameter calls integer overload

220 views Asked by At

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?

3

There are 3 answers

2
user12002570 On BEST ANSWER

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).

4
Pepijn Kramer On

To avoid implicit conversions being considered, rewrite your code like this using C++17 syntax (because you used that in your Compiler Explorer link). At least the faulty code would not compile now.

By making explicit constraints, the types have to exactly match and no implicit conversion is considered. (C++20 syntax would be a bit nicer.)

Demo: https://godbolt.org/z/MexsGT55T

#include <iostream>
#include <type_traits>

template<typename type_t>
auto f(type_t&& f) -> std::enable_if_t<std::is_same_v<type_t, float>, void>
{
    std::cout << f << "f ";
}

template<typename type_t>
auto f(type_t&& f) -> std::enable_if_t<std::is_same_v<type_t, int>, void>
{
    std::cout << f << "f ";
}

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

    f(2);  f(1.0f);
    f(iv); f(fv); // <== Will no longer compile now
}
0
j6t On

To avoid the implicit conversions, provide a delete function template that is picked if the non-templated overloads do not fit exactly. That is, just add this overload:

template<typename T>
void f(T&&) = delete;