Search code examples
c++castingoperator-overloadingstandardsoverload-resolution

Implicit conversion operator priority


In the following piece of code (live on coliru):

#include <iostream>
#include <string>

int main()
{
    struct S {
        operator bool        () const { return false; }
        operator std::string () const { return "false"; }
    } s;
    std::cout << s << "\n"; // outputs 0
}

How does the compiler choose to pick the implicit conversion to bool over std::string?

My hypothesis is that in this case, it might be purely the order of declaration of the different flavours of std::basic_ostream::operator<<, but is it all? Does the standard say something about picking a specific implicit conversion?


Solution

  • Recall that std::string is not a standalone type, it's really a class template specialization - std::basic_string<char>. The very important detail is that the potential overload for streaming a std::string does not take a std::string const& argument, it is a function template that deduces a std::basic_string const&:

    template <class CharT, class Traits, class Allocator>
    std::basic_ostream<CharT, Traits>& 
        operator<<(std::basic_ostream<CharT, Traits>& os, 
                   const std::basic_string<CharT, Traits, Allocator>& str);
    

    Template deduction never considers conversions. Name lookup will find this function template, and then discard at as being non-viable due to deduction failure. S is not a basic_string<CharT, Traits, Allocator> for any such types, so we're done. The only viable stream operators would be all the integral ones, of which bool is the best match.

    If there specifically was a function with signature:

    std::ostream& operator<<(std::ostream&, std::string const& );    
    

    Then the call would be ambiguous - you'd get two user-defined conversions that would be equivalently ranked.


    This is easy to verify by using our own functions instead of the million overloads for operator<<:

    void foo(bool ); // #1
    void foo(std::string ); // #2
    
    void bar(bool );  // #3
    template <class C, class T, class A>
    void bar(std::basic_string<C,T,A> ); // #4
    
    foo(S{}); // error: ambiguous
    bar(S{}); // calls #3