Search code examples
c++pointerstemplatesreferencetype-deduction

Function overload resolution for template type parameters with respect to references and pointers


I've been working on some code as an exercise.

template <typename T> const T& larger(const T& a, const T& b) {
    std::cout << "Calling larger(const T&, const T&)" << std::endl;
    return a > b ? a : b;
}

template <typename T> const T* larger(const T* a, const T* b) {

    std::cout << "Calling larger(const T*, const T*)" << std::endl;

    return *a > *b ? a : b;
}

int main() {


    int a { 1 }, b { 2 };
    std::cout << larger(a, b) << std::endl;

    int *c { new int { 5 } }, *d { new int { 4 } };

    std::cout << *larger<int>(c, d) << std::endl;

    return 0;

}

There are a few things I don't understand. Primarily, why does the compiler resolve both calls to the larger() function in main to the template that accepts reference parameters. In the second call to larger shouldn't the deduced types be int* and therefore, call the function that accepts pointer parameters instead?

I do know that I can "fix" my issue by calling the second function with an explicit type parameter larger<int>(c, d) (it works, but I don't know why). However, I am more interested in understanding the type deduction rules for this scenario?


Solution

  • Primarily, why does the compiler resolve both calls to the larger() function in main to the template that accepts reference parameters?

    In larger(a, b), it's obvious: you cannot deduce const T* from the given ints; they're not pointers, so only the first overload is viable.

    In the actual larger<int>(c, d), the first overload would have const int& parameters after substituting int, and you cannot bind const int& to pointers, so only the second overload (with int* parameters) is viable.

    In a hypothetical larger(c, d), the parameters in the first overload are int * const & (reference to const pointer to int) and this type is reference-compatible with the given int*s. Therefore, the reference binds directly according to [dcl.init.ref] p5.1.1 without any implicit conversions. On the other hand, converting int* to const int* for the second overload would require a qualification conversion, which makes this overload worse. In overload resolution, the overload with the better conversion sequence wins, and here, an Identity is better than Qualification Adjustment (see [tab:over.ics.scs]).

    Workaround

    There are multiple ways around this dilemma:

    • Have only one function template containing if constexpr (std::is_pointer_v<T>) ... which handles both cases.
    • Constrain the first overload with requires !std::is_pointer_v<T>.
    • ...