literal `0` being a valid candidate for int and const string& overloads causes ambiguous call
Asked Answered
E

2

44

I fixed a bug recently.

In the following code, one of the overloaded function was const and the other one was not. The issue will be fixed by making both functions const.

My question is why compiler only complained about it when the parameter was 0.

#include <iostream>
#include <string>

class CppSyntaxA
{
public:
    void f(int i = 0) const { i++; }
    void f(const std::string&){}
};

int main()
{
    CppSyntaxA a;
    a.f(1);  // OK
    //a.f(0);  //error C2666: 'CppSyntaxA::f': 2 overloads have similar conversions
    return 0;
}
Epoxy answered 18/7, 2019 at 20:0 Comment(8)
@NeilButterworth GCC shows the same behavior.Trotman
@Miles No, it doesn't.Loudermilk
@NeilButterworth sure does: wandbox.org/permlink/mEtMFPt5KMdSVGs3 and wandbox.org/permlink/8UiG8XD7d0ol8U8vHeresiarch
clang seems happy with it for what that's worth.Factional
Obviously, I can't illustrate the lack of an error message, but GCC 8.1.0 compiles it with both -std=c++11 and -std=c++17Loudermilk
@NeilButterworth Try compiling with -pedanticRodie
@Rodie I always do.Loudermilk
@NeilButterworth ¯\_(ツ)_/¯ If there's no diagnostic message, then I suppose it's a bug in that version of GCC.Rodie
C
56

0 is special in C++. A null pointer has the value of 0 so C++ will allow the conversion of 0 to a pointer type. That means when you call

a.f(0);

You could be calling void f(int i = 0) const with an int with the value of 0, or you could call void f(const std::string&) with a char* initialized to null.

Normally the int version would be better since it is an exact match but in this case the int version is const, so it requires "converting" a to a const CppSyntaxA, where the std::string version does not require such a conversion but does require a conversion to char* and then to std::string. This is considered enough of a change in both cases to be considered an equal conversion and thus ambiguous. Making both functions const or non const will fix the issue and the int overload will be chosen since it is better.

Clamworm answered 18/7, 2019 at 20:7 Comment(10)
@FrançoisAndrieux I think f(int) is const and a is a non-const variable. So there is a cast from non-const to const.Epoxy
@NeilButterworth fixed.Clamworm
@Clamworm Never noticed that const. Good observation and excellent answer. Edit : I guess I missed the part of the question that mentions it, must have skipped straight to the code...Longspur
For reference, the rules for determining the best overload can be found here but I personally find them extremely hard to navigate. Edit : It seems that as far as the constness of the function goes, member functions ares treated as if they had an additional implicit this argument. So non-const to const counting as an implicit conversion makes sense.Longspur
@FrançoisAndrieux Don't worry, compiler implementers do as well ;)Clamworm
"A null pointer has the value of 0" - Well, not quite. nullptr is its own distinct type (std::nullptr_t) (which can sometimes be important). It's convertible to 0, but that's not the same thing as being 0. The NULL macro just shouldn't be used. You already know this..Honora
@JesperJuhl A null pointer, and nullptr are different things. A null pointer is an integer with the value of 0 along with also being a prvalue of nullptr_t.Clamworm
@Clamworm I bow to the standards quote. 🙂 You are technically correct - the best kind of correct.Honora
The most infuriating part of this overload issue is that it's UB to call the constructor of std::string with a null pointer :/Multifarious
A null pointer has the value 0? That's a new one. It's implementation-defined what value it has, though as literal 0 can be implicitly converted to any null pointer type, it therefore will compare equal. (intptr_t)(void*)0 == 0 is not guaranteed, though holds on (most?) modern platforms. @JesperJuhl Seems you got off-track with nullptr.Argilliferous
R
11

My question is why compiler only complained about it when the parameter was 0.

Because 0 is not only an integer literal, but it is also a null pointer literal. 1 is not a null pointer literal, so there is no ambiguity.

The ambiguity arises from the implicit converting constructor of std::string that accepts a pointer to a character as an argument.

Now, the identity conversion from int to int would otherwise be preferred to the conversion from pointer to string, but there is another argument that involves a conversion: The implicit object argument. In one case, the conversion is from CppSyntaxA& to CppSyntaxA& while in other case it is CppSyntaxA& to const CppSyntaxA&.

So, one overload is preferred because of one argument, and the other overload is preferred because of another argument and thus there is no unambiguously preferred overload.

The issue will be fixed by making both functions const.

If both overloads are const qualified, then the implicit object argument conversion sequence is identical, and thus one of the overloads is unambiguously preferred.

Rodie answered 18/7, 2019 at 20:12 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.