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

Time:05-17

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.

  • Related