c++templatesoverload-resolutiontemplate-argument-deductionfunction-templates

Overload resolution and template argument deduction - why is 0 special?


In the following example, 0 behaves in a special way: it chooses a different overload than one would expect for one example function call. I would like to know why. My understanding is also below.

#include <iostream>

template<typename T>
void f(T a) {
    std::cout << "first" << std::endl;
}

template<typename T>
void f(T* a) {
    std::cout << "second" << std::endl;
}

int main()
{
    f(0);
    f<size_t>(0);
    f<size_t>(0UL);
    
    f(1);
    f<size_t>(1);
}

Output:

first
second
first
first
first

My understanding:

f(0) - template argument deduction, integer literal 0 is int type, hence first f is chosen with T=int

f<size_t>(0) - explicit template instantiation with integer promotion, chosen type is T=size_t, first function is chosen and 0 is promoted from int to size_t (I AM WRONG HERE)

f<size_t>(0UL) - same as above but with no promotion (0 is already type size_t)

f(1) - same as 1.

f<size_t>(1) - same as 2. (I am right here for some reason??)

NOTE:

I know that 0 is implicitly convertible to a null pointer:

A null pointer constant is an integer literal (5.13.2) with value zero or a prvalue of type std::nullptr_t

However, I also know from the standard that promotion has higher priority than conversion:

Each type of standard conversion sequence is assigned one of three ranks:

  1. Exact match: no conversion required, lvalue-to-rvalue conversion, qualification conversion, function pointer conversion,(since C++17) user-defined conversion of class type to the same class
  2. Promotion: integral promotion, floating-point promotion
  3. Conversion: integral conversion, floating-point conversion, floating-integral conversion, pointer conversion, pointer-to-member conversion, boolean conversion, user-defined conversion of a derived class to its base

Solution

  • f<size_t>(0) - explicit template instantiation with integer promotion, chosen type is T=size_t, first function is chosen and 0 is promoted from int to size_t

    The reason f<size_t>(0) calls/uses the second overload f(T*) is because of partial ordering rules. In particular, the second overload f(T* a) is more specialized than the first overload f(T a).

    Note that if we were to have ordinary(non-template) functions say void f(std::size_t*) and void f(std::size_t) then the call f(0) would've been ambiguous but with function templates(as in your case) the call f(0) prefers/selects the void f(T*) version because you've overloaded function templates and the void f(T*) version is more specialized than void f(T).


    why f(0) calls first overload. I know that 0 is implicitly convertible to a null pointer:

    Because implicit conversions are not considered during template argument deduction. This means that for the call f(0) only the first overload void f(T a) is viable. In other words, the second overload void f(T* a) is not even viable for the call f(0) .You can verify this in this demo:

    template<typename T>
    void f(T* a) {
        std::cout << "second" << std::endl;
    }
    int main()
    {
        f(0);//FAILS because implicit conversion are not considered during deduction   
    }