Overload Resolution, Implicit Conversion Sequences

The overloading story of std::string and bool for "Sweet Melon" and How C++’s Implicit Conversion Sequences played out….


Every now and then we expose overloaded API’s in C++. But we seldom do consider How overload resolution works. The rules are more complex than you think, even the ISO C++ standard dedicated several pages of technical text describing it’s rules. However, in this article we will only consider The interaction of overload resolution with Implicit Conversion Sequences ..

Consider the following snippet:

void foo(int){ 
    std::cout << "foo(int)" << '\n';
}

void foo(bool){
    std::cout << "foo(bool)" << '\n';
}

int main(){
    foo(NULL);
    foo(nullptr);
}

What do you think of its output?

foo(int)
foo(bool)

This is probably no brainer if you have programmed in C++ using C++03 standard and you’ve migrated to post C++11. For a simple remainder, NULL was simply inherited from C, which is typically a MACRO defined as #define NULL 0.

So, basically the call in the main() boils down to (after Preprocessor pass):

int main(){
    foo(0);
    foo(nullptr);
}

The above, 0 as with any other “value” arabic numeral or integer is known as an integer literal. whose type is deduced by another set of rules as described here


The Overloading story of std::string and bool

Let’s get into the main point, consider the code below:

void foo(std::string){
    std::cout << "foo(std::string)" << '\n';
}

void foo(bool){
    std::cout << "foo(bool)" << '\n';
}

int main(){
    std::string str = "Sweet Melon!";
    
    foo("Sweet Melon!");
    foo(nullptr);
    foo(str);
}

What do you think of it’s output?:

foo(bool)
foo(bool)
foo(std::string)

….WTH! Not intuitive right? It turns out that the “string” "Sweet Melon" is not a std::string object, but rather a string literal of type const char[12]. It’s type is deduced by the compiler as a const array of 12 characters. (remember, string literals are always implicitly “Null \0” terminated by the compiler).

The problem we have now is ranking the type const char[12] against viable overloads of:

  • std::string (Has many Constructor overload, User Defined Type)
  • bool ( built-in type, Non-class type, but special.)

Unfortunately, there is none that is an exact match. So the compiler considers an implicit conversion sequence (ICS) which also includes exploration of converting constructors of the class types we are up against…

It also turns out that there are three ordered ranks of Implicit Conversion Sequences:

And these rank in the order they appear. Note the “s” in the “sequences” above.

When comparing the basic forms of implicit conversion sequences (as defined in [over.best.ics])

  • a standard conversion sequence is a better conversion sequence than a user-defined conversion sequence or an ellipsis conversion sequence, and
  • a user-defined conversion sequence is a better conversion sequence than an ellipsis conversion sequence.

The compiler’s overload resolution subsystem considers ICS and tries to apply viable permutations of them, then rank the sequences.

It turns out that the only possible ICS for const char[12] is “Array-to-pointer” conversion which is a Standard Conversion Sequence. “Array to pointer” conversion is typically known as array “decay”.


So, having performed the “decay”, the compiler begins a new quest of finding a matching overload for const char* among:

  • std::string
  • bool

… Unfortunately, it still can’t produce a best viable function.


If after one Standard Conversion Sequence, and a best viable function among the overload set hasn’t been produced, the compiler is required to:

  1. perform a User-Defined Conversion Sequence, or
  2. perform additional conversions, one from each of the categories proceeding the current category of Standard Conversion Sequence in order.
1. Perform a User-Defined Conversion Sequence:

std::string has a constructor (simplified):

namespace std{
    template<typename CharT,
             typename Traits = std::char_traits<CharT>,
             typename Allocator = std::allocator<CharT>>
    class basic_string{
    public:
        basic_string(const CharT*);   //Our constructor of interest
        ....
    };
    using string = basic_string<char>;
} 

In this parital procedure, we can then say that, for a const char* type, we have viable paths of:

  • std::string from std::string(const char*)
  • bool from ??
2. Perform Additional Standard Conversion from proceeding Categories:

Any pointer, regardless of it’s qualification can be converted to a bool type.

We can then say that, for a const char* type, we have viable paths of:

  • std::string from std::string(const char*) (User defined conversion)
  • bool from const char* (standard conversion)

Implicit conversion of a pointer type to a bool is a Standard Conversion Sequence and it ranks higher than std::string’s converting constructor which is a User-defined Conversion Sequence. (Remember, anything not defined by the core language, is assumed to be user defined in C++’s pureview, even an STL type!)


Feedbacks, corrections to my mail below \

Written on May 5, 2017