Search code examples
c++variadic-templatesoverload-resolutionfunction-templates

How does overload resolution work with variadic template arguments and a non template argument derived type


I'm trying to create a overload for a function with variadic template arguments for a type and all its possible derivates.

Naively i tried with just one overload and the base class as the first parameter but this doesn't work for derived types. A third overload with a concept or an enable_if can make it work for derived types (and then the first overload wouldn't be needed anymore) but i don't understand why the overload with the base type (the first overload) isn't considered in the overload resolution when called with a derived type.

Can someone explain this?

Here's the code (live demo):

#include <iostream>

struct A {};
struct B : public A {};

template<class... Types>
void func(Types... args)
{
    std::cout << "1";
}
template<class... Types>
void func(const A& a, Types... args)
{
    std::cout << "2";
}
// why is this needed to cover the derived class?
// with this function enabled, the output is "3" and without "1"
#if 1
template<typename dA, class... Types>
requires std::derived_from<dA, A>
void func(const dA& a, Types... args)
{
    std::cout << "3";
}
#endif

int main()
{
    func(B{});
}

To maybe make the question clearer; why does this print 1 and not 3 like this does?


Solution

  • When calling func(Derived{}) without the last overload, 1 gets printed instead of 2 because the implicit conversion sequence when calling it is better. Implicit conversion sequences are more important than which function template is more specialized.

    When adding the overload with the std::derived_from constraint, 1 and 3 have equally good implicit conversion sequences, and 3 is chosen because it is more specialized.

    1 is chosen based on implicit conversion sequences

    #include <iostream>
    
    struct Base {};
    struct Derived : public Base {};
    
    void func(auto) { std::cout << "1"; }
    
    void func(const Base& a) { std::cout << "2"; }
    
    int main() {
        func(Derived{}); // prints 1
    }
    

    Intuitively, func(const Base&) is a better match because it isn't a template, or because it's intuitively "more specialized" in some other way.

    However, func(auto) is selected. The way overload resolution (see [over.match.general]) works is that

    1. all candidates are tested for viability, which disqualifies some
    2. the best candidate is chosen based on various criteria ([over.match.best.general])

    A prvalue of type Derived is converted to const Base& through a derived-to-base conversion ([over.ics.ref] p1, [over.best.ics.general] p7), which has the rank Conversion, whereas Derived{} -> auto is an Exact Match. Thefore, it's better to call func(auto).

    3 is chosen based on most specialized function templates

    template<typename dA, class... Types>
    requires std::derived_from<dA, A>
    void func(const dA& a, Types... args)
    

    This case is different. Since a binds to any const dA, it binds to Derived directly without any derived-to-base conversion. There is an Exact Match conversion for 1 and 3.

    3 is more specialized because it has a constraint std::derived_from and is chosen as the best candidate ([over.match.best.general] p2.5).