Search code examples
c++polymorphismdefault-parameterstemplate-argument-deduction

Template deduction fails for polymorphic class with default template parameters


Consider this example:

#include <memory>

template<typename T>
class A {};

template<typename T1, typename T2>
class B: public A<T1> {};

template<typename T = int>
void foo(std::shared_ptr< A<T> > test)
{

}

int main()
{
    auto p = std::make_shared<B<int, int>>();
    foo<int>(p);    // Works
    foo<>(p);       // Does not work
    foo(p);         // Does not work
}

I'm trying to get this to compile without explicitly specifying the type T for foo, but it doesn't work. I'm not sure why as if I explicitly the type T, it works just fine, but if I it, it doesn't compile, even though I've told the compiler the what the type T should be if I don't explicitly specify it.

I get why the compiler can't deduce the type T, but why can't it use my default type T when I don't specify it? How do I get around this and what is the "proper" way of doing this?


Solution

  • The problem isn't related to the default template parameter. Instead, the type of p (std::shared_ptr<B<int, int>>) can't be matched against the std::shared_ptr<T> argument of the template foo: During template argument deduction, no conversions are taken into account, and passing a reference to a derived class instance as a base class reference is indeed a conversion.

    You can fix the issue by explicitly upcasting the pointer that std::shared_ptr manages.

    std::shared_ptr<A<int>> p = std::make_shared<B<int, int>>();
    

    Now, these calls will work out as expected:

    foo<>(p); // Ok, argument matches the signature
    foo(p);   // Same...
    

    Note that it makes a big difference whether the template argument type appears in the parameter list of a function template (making deduction possible) or not. Consider these two templates,

    template <class T = int> void foo(T&&)
    {
        std::cout << __PRETTY_FUNCTION__ << "\n";
    }
    
    template <class T = int> void bar(double)
    {
        std::cout << __PRETTY_FUNCTION__ << "\n";
    }
    

    Both have the default type int, but when they are instantiated like this

    foo<>(0.0); // outputs: void foo(T&&) [with T = double]
    bar<>(0.0); // outputs: void bar(double) [with T = int]
    

    the first instantiation uses the function argument to deduce the template parameter (results in double), while the second doesn't deduce anything and defaults to int.