Why cant the c++ template compiler infer types from covariant smart pointer arguments?

Viewed 95

I have the following code

https://godbolt.org/z/7d1arK

#include <memory>

template <typename T>
class A {

};

template <typename T>
class B : public A<T> {};


template <typename T>
void Foo(std::shared_ptr<A<T>> arg){}


int main(){
    auto bptr = std::make_shared<B<int>>();
    std::shared_ptr<A<int>> aptr = bptr;


    // This compiles
    Foo(aptr);

    // This does not compile
    Foo(bptr);

    // This compiles
    Foo<int>(bptr);
}

My question is why can't the compiler handle the line?

Foo(bptr);
3 Answers

Template argument deduction and user-defined conversions do not mix. Because they are related by a user-defined conversion, shared_ptr<A<int>> and shared_ptr<B<int>> are not "covariant" in the same sense as types like A<int>* and B<int>* for purposes of the C++ language.

Though that's enough to make the call illegal, this case is even harder because there's not a simple user-defined converting constructor or conversion function involved here, but a constructor template:

template <class T> class std::shared_ptr {
public:
    template <class Y>
    shared_ptr(const shared_ptr<Y>&);
    // ...
};

So here the compiler would need to deduce both T and Y to determine T. "Obviously" Y will deduce to int, but the general case of this complication is not practical to solve.

And actually in this case, int is not the only possible value of T. The call Foo<const int>(bptr) would also be legal.

The C++ language specifies exactly when template argument deduction will and won't succeed, and with what resulting template arguments, so that we can be certain code working on one conforming compiler won't fail on another, at least not for this reason. At some point the line must be drawn, and one such line is "user-defined conversions are not considered when deducing template arguments".

As @SamVarshavchik and @aschepler mentioned, template deduction takes place without considering user-defined conversions, which would be necessary to convert from std::shared_ptr<B<int>> to std::shared_ptr<A<int>>. Therefore no suitable instantiation of the template is found and the call fails.

One way to enable this would be to use a wider range of templates, with a static assertion to narrow it down to enabling only actual derived classes of A:

#include <memory>
#include <type_traits>

template <typename T>
class A { };

template <typename T>
class B : public A<T> { };

class C { };


template <template<class> class Derived, class T>
void Foo(std::shared_ptr<Derived<T>> arg) {
    static_assert(std::is_base_of_v<A<T>, Derived<T>>);
}


int main() {
    auto bptr = std::make_shared<B<int>>();
    std::shared_ptr<A<int>> aptr = bptr;
    auto cptr = std::make_shared<C>();


    // This compiles
    Foo(aptr);

    // This compiles
    Foo(bptr);

    // This does not compile
    Foo(cptr);
}

In this case, Foo will be able to accept as an argument a std::shared_ptr to anything that inherits from A.

Like @SamVarshavchik said, user-defined implicit conversions are not allowed in template argument deduction. bptr is of type std::shared_ptr<B<int> > which is implicitly convertible to std::shared_ptr<A<int> > since B<int>* is implicitly convertible to A<int>*, but the template expects something like std::shared_ptr<A<T> > directly.

Related