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;
}
CodePudding user response:
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.
