Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is a double convertible to a const-reference of seemingly any primitive?

Consider the following code:

#include <iostream>

float func(char const & val1, unsigned int const & val2)
{
    return val1 + val2;
}

int main() {
    double test1 = 0.2;
    double test2 = 0.3;

    std::cout << func(test1, test2) << std::endl;

    return 0;
}

This compiles and runs despite the fact that I am passing in a double to a function that takes a const-reference to types that are smaller than a double (on my system, sizeof(double) == 8, while sizeof(unsigned int) == 4, and sizeof(char) == 1 by definition). If the reference isn't const, compilation fails (e.g., float func(char & val1, unsigned int & val2) instead of the current definition) with an error:

cannot bind non-const lvalue reference of type 'char&' to an rvalue of type 'char'

I get the exact same behavior when testing this with GCC, Clang, ICC, and MSVC on Godbolt, so it appears standard. What is it about const-references that causes this to be accepted, whereas a reference isn't? Also, I used -Wall -pedantic - why am I not getting a warning about a narrowing conversion? I do when the function passes by value instead of by reference...

like image 833
R_Kapp Avatar asked Jun 12 '19 15:06

R_Kapp


2 Answers

It is indeed standard.

test1 and test2 are converted to anonymous temporary char and unsigned types for which the const references in the function are appropriate bindings. If you set your compiler to warn you of narrowing conversions (e.g. -Wconversion), it would output a message.

These bindings are not possible if the function parameters are non-const references, and your compiler is correctly issuing a diagnostic in that case.

One fix is to delete a better overload match:

float func(double, double) = delete;
like image 93
Bathsheba Avatar answered Nov 20 '22 20:11

Bathsheba


As a complement to the accepted answer, particularly the approach

One fix is to delete a better overload match:

float func(double, double) = delete;

one could also approach it from the other way around: namely deleting all overloads that are not exactly matching your intended types of parameters. If you want to avoid any implicit conversions (including promotions), you could define func as a deleted non-overloaded function template, and define explicit specializations of func only for the specific types of arguments you’d like to have overloads for. E.g.:

// Do not overload the primary function template 'func'.
// http://www.gotw.ca/publications/mill17.htm
template< typename T, typename U >
float func(const T& val1, const U& val2) = delete;

template<>
float func(char const& val1, unsigned int const& val2)
{
    return val1 + val2;
}

int main() {
    double test1 = 0.2;
    double test2 = 0.3;
    char test3 = 'a';
    unsigned int test4 = 4U;
    signed int test5 = 5;

    //(void)func(test1, test2); // error: call to deleted function 'func' (... [with T = double, U = double])
    //(void)func(test2, test3); // error: call to deleted function 'func' (... [with T = double, U = char])
    (void)func(test3, test4); // OK
    //(void)func(test3, test5); // error: call to deleted function 'func' (... [with T = char, U = int])
    return 0;
}

Emphasizing again to take care if intending to overload the primary function template, as overload resolution for overloaded and explicitly specialized function templates can be somewhat confusing, as specializations do not participate in the first step of overload resolution.

like image 44
dfrib Avatar answered Nov 20 '22 19:11

dfrib