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.