2

I am baffled by this error:

#include <string>
#include <iostream>

void f(int, std::string) {std::cout << "f1\n";}
template <class T>
void f(std::string, T&&) {std::cout << "f2\n";}

void f(int) {std::cout << "f3\n";}
void f(std::string) {std::cout << "f4\n";}

int main() {
    // f(0, "as"); // call of overloaded 'f(int, const char [3])' is ambiguous
    f(1, "as");
    f(0);
}

The first line does not compile and I can't figure out why.
The second line does compile, even though it is the same types.
The third line does compile, even though it is the same conversion.

Huh?

Baruch
  • 20,590
  • 28
  • 126
  • 201
  • 1
    *"even though it is the same types"* Literal `0` can be converted to a pointer, while non-zero or non-literal ints can't. – HolyBlackCat Aug 03 '22 at 12:22
  • 1
    `0` is the null pointer constant and `string` can be constructed from a pointer. – NathanOliver Aug 03 '22 at 12:23
  • also see: https://stackoverflow.com/questions/58909097/prevent-function-taking-const-stdstring-from-accepting-0 – NathanOliver Aug 03 '22 at 12:26
  • 1
    Note that the ambiguity of the `const char[]` to `std::string` conversion can be avoided by using actual **string** literals [(`""s`)](https://en.cppreference.com/w/cpp/string/basic_string/operator%22%22s) instead of C-style strings. – DevSolar Aug 03 '22 at 12:39
  • To call `f(int, std::string)` the first argument is an exact match, but exactly one implicit conversion of the second argument is needed. To call your templated `f(std::string, T &&)`, the second argument is an exact match and exactly one implicit conversion of the first argument is needed. When doing overload resolution, both ways are therefore equally good matches (exactly one conversion of one argument) hence the ambiguity. – Peter Aug 03 '22 at 12:48

2 Answers2

2

The third line does compile, even though it is the same conversion.

Yes, but it's not the only conversion. A string literal like "as" is not of type std::string, and requires a converting constructor. So it's a user defined conversion. The rank of the overload is the worst rank for any conversion of any argument.

Same way 0 to std::string is user defined, because 0 is a null pointer constant. That means both overloads have the same implicit conversion sequence rank, without a tie breaker.

Integers in general are not convertible to std::string; only null pointer constants are convertible (because they can initialise a pointer, which std::string's constructors may accept) . So for the case of 1, the second overload is not a candidate.

As a side note, passing a null pointer to std::string's constructor is undefined, so even if that resolution "succeeds", it's bad code.


Your last two overloads don't have the same implicit conversion rank. One is the identity conversion, while the other is user defined. The identity wins.

StoryTeller - Unslander Monica
  • 165,132
  • 21
  • 377
  • 458
0

To work around the implicit conversion you can do this (SFINAE) In this case f2's first argument has match std::string exactly.

#include <string>
#include <iostream>
#include <type_traits>

void f(int, std::string) { std::cout << "f1\n"; }

template <class T, class U>
auto f(T&&, U&&)->std::enable_if_t<std::is_same_v<T,std::string>,void>
{ 
    std::cout << "f2\n"; 
}

void f(int) { std::cout << "f3\n"; }
void f(std::string) { std::cout << "f4\n"; }

int main() 
{
    f(0, "as"); // call of overloaded 'f(int, const char [3])' is ambiguous
    f(1, "as");
    f(0);
}
Pepijn Kramer
  • 9,356
  • 2
  • 8
  • 19