'Template arguments can't be deduced for shared_ptr of class derived from templated base

I'm running into a case where I thought that the compiler would obviously be able to do template argument deduction, but apparently can't. I'd like to know why I have to give explicit template args in this case. Here's a simplified version of what's going on.

I have an inheritance hierarchy where the base class is templated and derived classes are concrete implementations of the base:

template <typename T> class Base {};

class Derived : public Base<int> {};

Then I have a templated function which accepts a shared pointer of the base class:

template <typename T> void DoSomething(const std::shared_ptr<Base<T>> &ptr);

When I try to call this method, I have to explicitly provide the template arguments:

std::shared_ptr<Derived> d = std::make_shared<Derived>();
DoSomething(d);      // doesn't compile!
DoSomething<int>(d); // works just fine

In particular, I get this error message if I don't use explicit template arguments:

main.cpp:23:18: error: no matching function for call to ‘DoSomething(std::shared_ptr&)’
   23 |     DoSomething(d);
      |                  ^
main.cpp:10:28: note: candidate: ‘template void DoSomething(const std::shared_ptr >&)’
   10 | template <typename T> void DoSomething(const shared_ptr<Base<T>> &ptr)
      |                            ^~~~~~~~~~~
main.cpp:10:28: note:   template argument deduction/substitution failed:
main.cpp:23:18: note:   mismatched types ‘Base’ and ‘Derived’
   23 |     DoSomething(d);
      |                  ^

What's even more confusing to me is that template arguments can be deduced if I don't use shared_ptr:

template <typename T> void DoSomethingElse(const Base<T> &b);

Derived d;
DoSomethingElse(d); // doesn't need explicit template args!

So obviously I need to specify the template arguments for DoSomething. But my question is, why? Why can't the compiler deduce the template in this case? Derived implements Base<int> and the type can be deduced in DoSomethingElse, so why does sticking it in a shared_ptr change the compiler's ability to figure out that T should be int?


Full example code which reproduces the issue:

#include <iostream>
#include <memory>

template <typename T> class Base {};

class Derived : public Base<int> {};

template <typename T> void DoSomething(const std::shared_ptr<Base<T>> &ptr)
{
    std::cout << "doing something" << std::endl;
}

template <typename T> void DoSomethingElse(const Base<T> &b)
{
    std::cout << "doing something else" << std::endl;
}

int main()
{
    std::shared_ptr<Derived> d = std::make_shared<Derived>();
    // DoSomething(d);   // doesn't compile!
    DoSomething<int>(d); // works just fine
    
    Derived d2;
    DoSomethingElse(d2); // doesn't need explicit template args!
    
    return 0;
}


Solution 1:[1]

Derived is a derived class of Base<int>, but std::shared_ptr<Derived> isn't a derived class of std::shared_ptr<Base<int>>.

So if you have a function of the form

template <typename T> void f(const Base<T>&);

and you pass a Derived value to it, the compiler will first notice that it can't match up Base<T> against Derived, and then try to match up Base<T> against a base class of Derived. This then succeeds since Base<int> is one of the base classes.

If you have a function of the form

template <typename T> void f(const std::shared_ptr<Base<T>>&);

and you pass a std::shared_ptr<Derived>, then the compiler will fail to match that against std::shared_ptr<Base<T>> and then try with base classes of std::shared_ptr<Derived>. If the latter has any base classes at all, they are internal to the standard library, and not related to std::shared_ptr<Base<T>>, so deduction ultimately fails.

What you are asking the compiler to do here is to say: "aha! std::shared_ptr<Derived> can be converted into std::shared_ptr<Base<int>>, which matches the function parameter!" But the compiler won't do that, because in general, there is no algorithm that the compiler can use in order to make a list of all types that a given type can be converted to.

Instead, you must help the compiler by telling it explicitly what to convert to. This can be done like so:

template <typename T>
Base<T> get_base(const Base<T>*);  // doesn't need definition

template <typename T> void DoSomething(const std::shared_ptr<Base<T>> &ptr);

template <typename D, typename B = decltype(get_base((D*)nullptr))>
void DoSomething(const std::shared_ptr<D>& ptr) {
    DoSomething(static_cast<std::shared_ptr<B>>(ptr));
}

Here, when the second DoSomething overload is called with an argument of type std::shared_ptr<D>, the get_base helper function will be used to determine the base class of D itself that has the form Base<T>. Then, the std::shared_ptr<D> will be explicitly converted to std::shared_ptr<Base<T>> so that the first overload can be called. Finally, note that if D isn't a derived class of any Base<T>, the second overload will be removed from the overload set, potentially enabling some other overload to handle the argument type.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Brian Bi