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?
When calling
func(Derived{})without the last overload,1gets printed instead of2because 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_fromconstraint,1and3have equally good implicit conversion sequences, and3is chosen because it is more specialized.1is chosen based on implicit conversion sequencesIntuitively,
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 thatA prvalue of type
Derivedis converted toconst Base&through a derived-to-base conversion ([over.ics.ref] p1, [over.best.ics.general] p7), which has the rank Conversion, whereasDerived{} -> autois an Exact Match. Thefore, it's better to callfunc(auto).3is chosen based on most specialized function templatesThis case is different. Since
abinds to anyconst dA, it binds toDeriveddirectly without any derived-to-base conversion. There is an Exact Match conversion for1and3.3is more specialized because it has a constraintstd::derived_fromand is chosen as the best candidate ([over.match.best.general] p2.5).