无法为派生自模板化基础的 class 中的 shared_ptr 推导出模板参数

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

我 运行 我认为编译器显然能够进行模板参数推导,但显然不能。我想知道为什么在这种情况下我必须给出明确的模板参数。这是正在发生的事情的简化版本。

我有一个继承层次结构,其中基础 class 是模板化和派生的 classes 是基础的具体实现:

template <typename T> class Base {};

class Derived : public Base<int> {};

然后我有一个模板化函数,它接受一个共享基指针 class:

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

当我尝试调用此方法时,我必须明确提供模板参数:

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

特别是,如果我不使用显式模板参数,我会收到此错误消息:

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);
      |                  ^

更让我困惑的是,如果我不使用 shared_ptr:

,模板参数 可以 被推导出来
template <typename T> void DoSomethingElse(const Base<T> &b);

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

所以显然我需要为 DoSomething 指定模板参数。但我的问题是,为什么?为什么编译器不能在这种情况下推导出模板? Derived 实现 Base<int> 并且可以在 DoSomethingElse 中推导出类型,那么为什么将其粘贴在 shared_ptr 中会改变编译器计算出 T 应该的能力是 int?


重现问题的完整示例代码:

#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;
}

DerivedBase<int> 的派生 class,但 std::shared_ptr<Derived> 不是 std::shared_ptr<Base<int>> 的派生 class。

因此,如果您具有以下形式的函数

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

并且你传递一个 Derived 值给它,编译器会首先注意到它不能匹配 Base<T>Derived,然后尝试匹配 Base<T>Derived 的基数 class。然后这就成功了,因为 Base<int> 是基础 classes 之一。

如果你有一个函数

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

并且你传递了一个 std::shared_ptr<Derived>,那么编译器将无法将其与 std::shared_ptr<Base<T>> 匹配,然后尝试使用 std::shared_ptr<Derived> 的基数 classes。如果后者有任何基础 classes,它们是标准库内部的,并且 std::shared_ptr<Base<T>> 相关,因此推导最终失败。

你在这里要求编译器做的是说:“啊哈!std::shared_ptr<Derived>可以转换std::shared_ptr<Base<int>>,这与函数匹配范围!”但是编译器不会那样做,因为通常情况下,编译器无法使用任何算法来列出给定类型可以转换为的所有类型。

相反,您必须通过明确告诉编译器要转换成什么来帮助编译器。可以这样做:

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));
}

这里,当第二个 DoSomething 重载被调用时带有类型 std::shared_ptr<D> 的参数,get_base 辅助函数将用于确定 class 的基数D 本身具有 Base<T> 的形式。然后,std::shared_ptr<D> 将显式转换为 std::shared_ptr<Base<T>>,以便可以调用第一个重载。最后,请注意,如果 D 不是任何 Base<T> 的派生 class,则第二个重载将从重载集中删除,可能会启用其他一些重载来处理参数类型。