shuhalo
shuhalo

Reputation: 6462

Ambiguity due to implicit conversion when using a template but not with normal function

I am trying to write a custom string class MyString with implicit conversion from const char*.

In addition to that, I want to output MyString via the shift operators into any std::ostream. Even more, I want it to output into a custom stream class that also supports the shift operators. I am trying to resolve that situation by using a template that works for MyStream just the same as with std::ostream.

However, this leads easily to ambiguity in overload resolution. The gist of the problem is summarized in the following example.

#include <iostream>
#include <sstream>

struct MyString
{
    // implicit constructors
    MyString( const char* cstr ) {};
    // some dummy c_str()
    const char* c_str() const { return nullptr; };
};

// // This works just fine, as expected, ...
// inline std::ostream& operator<<( std::ostream& out, const MyString& str )
// {
//    std::cout << str.c_str();
//    return out;
// }

// ... but this one causes ambiguity !!!
template<typename S>
inline S& operator<<( S& out, const MyString& str )
{
    std::cout << str.c_str();
    return out;
}


int main()
{
    std::cout << "1";

    std::stringstream ss;
    ss << "2";

    std::cout << ss.str().c_str();

    return 0;
}

The code above works just fine. However, if instead I comment out the std::ostream operator overload and try to use the template instead, then the operator calls are suddenly ambiguous.

The shift into std::cout works but the shift into std::stringstream ss has become ambigious.

Question: what is the reason for the lack of ambiguity in the first case and its the sudden ambiguity in the second case? What possibilities are there to resolve this issue?

I want to retain the implicit conversion facility for MyString, and I want to shift such objects into anything that behaves like the standard stream, using the shift operators.

EDIT: Whether it compiles or not depends on the compiler's strictness. On https://www.onlinegdb.com/online_c++_compiler I get

main.cpp: In function ‘int main()’:
main.cpp:33:11: warning: ISO C++ says that these are ambiguous, even though the worst conversion for the first is better than the worst conversion for the second:
   33 |     ss << "2";
      |           ^~~
In file included from /usr/include/c++/11/iostream:39,
                 from main.cpp:1:
/usr/include/c++/11/ostream:611:5: note: candidate 1: ‘std::basic_ostream<char, _Traits>& std::operator<<(std::basic_ostream<char, _Traits>&, const char*) [with _Traits = std::char_traits<char>]’
  611 |     operator<<(basic_ostream<char, _Traits>& __out, const char* __s)
      |     ^~~~~~~~
main.cpp:21:11: note: candidate 2: ‘S& operator<<(S&, const MyString&) [with S = std::__cxx11::basic_stringstream<char>]’
   21 | inline S& operator<<( S& out, const MyString& str )
      |           ^~~~~~~~
12

Upvotes: 0

Views: 107

Answers (1)

panik
panik

Reputation: 256

std::ostream is a base type of std::stringstream defined as std::basic_stringstream<char> (see the inheritance diagram). The instantiated function operator<<(std::stringstream& out, const MyString& str) conflicts with the function operator<<(std::ostream& out, const char*s) because the compiler can't decide whether to convert the const char* argument to MyString or to upcast std::stringstream to std::ostream.

We may explicitly convert the string argument to remove the ambiguity, i.e. ss << MyString("2");

Update: Note that the first function called with cout argument is the standard function and not the expected one, because the standard function represents the exact match, https://godbolt.org/z/P46sxejr4.

I suggest to not rely on implicit conversions for the listed in the documentation types because there is always will be a better exact standard match. And I suggest to stick to the recommended way of defining the overload to make your code less error-prone.

Upvotes: 1

Related Questions