如何让类模板的用户定义转换排序与模板实参的转换排序保持一致?
你提的这个问题非常典型——原始指针的转换会自动优先选择继承层次更近的基类,但用户定义的智能指针转换却做不到,因为编译器不会给用户定义的转换序列默认排序。下面我分两部分解答:先讲用标准std::shared_ptr能不能实现目标,再讲自己写智能指针模板的方案。
先回顾你的问题场景
先把你的代码贴出来方便分析:
#include <memory> struct Base {}; struct Middle : public Base {}; struct Derived : public Middle {}; void foo(Base*); void foo(Middle*); void test_1(Derived* p) { foo(p); // 明确选择foo(Middle*),因为Derived*→Middle*是更优的标准转换 } void bar(std::shared_ptr<Base>); void bar(std::shared_ptr<Middle>); void test_2(std::shared_ptr<Derived> p) { bar(p); // 歧义!两个重载都可行,编译器无法区分优先级 }
核心问题是:std::shared_ptr<Derived>到std::shared_ptr<Middle>/std::shared_ptr<Base>的转换都是用户定义转换(通过std::shared_ptr的模板转换构造函数),而C++标准中,用户定义转换序列之间没有默认优先级——哪怕一个转换对应的继承层次更近,编译器也不会认为它更优。
方案一:用标准std::shared_ptr实现目标
不需要修改std::shared_ptr的源码,我们可以通过**SFINAE+类型特性(traits)**给bar的重载加上约束,让编译器自动选中“更近”的基类版本。
关键思路
- 先实现一个类型特性工具,用来计算
Derived到Base的继承深度(比如Derived→Middle深度为1,Derived→Base深度为2); - 给
bar函数的重载加上std::enable_if约束,只允许深度最小的重载参与匹配。
具体实现
首先,写一个计算继承深度的inheritance_depth特性(用递归判断直接基类的方式):
#include <type_traits> // 辅助:判断U是不是T的直接基类 template <typename T, typename U> struct is_direct_base_of : std::false_type {}; // 利用static_cast特性:如果U是T的直接基类,static_cast<U*>(T*)合法且无后续转换 template <typename T, typename U> auto test_direct_base(int) -> decltype(static_cast<U*>(std::declval<T*>()), std::true_type{}); template <typename T, typename U> std::false_type test_direct_base(...); template <typename T, typename U> struct is_direct_base_of : decltype(test_direct_base<T, U>(0)) {}; // 计算从D到B的继承深度 template <typename D, typename B> struct inheritance_depth { static constexpr int value = std::is_base_of_v<B, D> ? (is_direct_base_of<D, B>::value ? 1 : 1 + inheritance_depth<typename std::remove_cv_t<D>, B>::value) : 0; }; // 特化:当D就是B时,深度为0 template <typename T> struct inheritance_depth<T, T> : std::integral_constant<int, 0> {};
然后修改bar的重载,用std::enable_if约束只允许最浅深度的重载参与匹配:
// 针对Middle的重载:直接处理,优先级最高 void bar(std::shared_ptr<Middle> p) { // 处理Middle的业务逻辑 } // 针对Base的重载:仅当没有更浅的基类重载时才启用 template <typename D> std::enable_if_t<inheritance_depth<D, Base>::value > 1> bar(std::shared_ptr<Base> p) { // 处理Base的业务逻辑 }
这样调用bar(std::shared_ptr<Derived>{})时,编译器会优先匹配bar(std::shared_ptr<Middle>),因为Base的重载会被SFINAE规则禁用,完全避免歧义。
方案二:自己实现Ptr<T>模板,内置转换优先级
如果觉得上面的方法太繁琐,完全可以自己写一个智能指针类,在转换构造函数中内置继承层次的优先级判断,让它的转换行为和原始指针完全对齐。
核心思路
利用C++模板重载的优先级规则:
- 更特化的模板重载 > 更通用的模板重载;
- 非模板函数 > 模板函数。
我们给Ptr<T>的转换构造函数提供两个版本:
- 高优先级版本:当
U是T的直接基类时启用; - 低优先级版本:当
U是T的间接基类时启用。
具体实现
#include <type_traits> #include <cstddef> template <typename T> class Ptr { private: T* m_ptr; size_t* m_ref_count; // 简单的引用计数实现 // 辅助:释放资源 void release() { if (m_ref_count && --*m_ref_count == 0) { delete m_ptr; delete m_ref_count; } } public: // 基本构造函数 explicit Ptr(T* p = nullptr) : m_ptr(p), m_ref_count(p ? new size_t(1) : nullptr) {} // 拷贝构造 Ptr(const Ptr& other) : m_ptr(other.m_ptr), m_ref_count(other.m_ref_count) { if (m_ref_count) ++*m_ref_count; } // 移动构造 Ptr(Ptr&& other) noexcept : m_ptr(other.m_ptr), m_ref_count(other.m_ref_count) { other.m_ptr = nullptr; other.m_ref_count = nullptr; } // 析构函数 ~Ptr() { release(); } // 拷贝赋值 Ptr& operator=(const Ptr& other) { if (this != &other) { release(); m_ptr = other.m_ptr; m_ref_count = other.m_ref_count; if (m_ref_count) ++*m_ref_count; } return *this; } // 移动赋值 Ptr& operator=(Ptr&& other) noexcept { if (this != &other) { release(); m_ptr = other.m_ptr; m_ref_count = other.m_ref_count; other.m_ptr = nullptr; other.m_ref_count = nullptr; } return *this; } // 获取原始指针 T* get() const noexcept { return m_ptr; } // 转换构造函数1:高优先级——U是T的直接基类 template <typename U> Ptr(const Ptr<U>& other, std::enable_if_t<is_direct_base_of<U, T>::value, int> = 0) : m_ptr(other.get()), m_ref_count(other.m_ref_count) { if (m_ref_count) ++*m_ref_count; } // 转换构造函数2:低优先级——U是T的间接基类 template <typename U> Ptr(const Ptr<U>& other, std::enable_if_t<std::is_base_of_v<U, T> && !is_direct_base_of<U, T>::value, int> = 0) : m_ptr(other.get()), m_ref_count(other.m_ref_count) { if (m_ref_count) ++*m_ref_count; } }; // 复用之前的is_direct_base_of特性 template <typename T, typename U> struct is_direct_base_of : std::false_type {}; template <typename T, typename U> auto test_direct_base(int) -> decltype(static_cast<U*>(std::declval<T*>()), std::true_type{}); template <typename T, typename U> std::false_type test_direct_base(...); template <typename T, typename U> struct is_direct_base_of : decltype(test_direct_base<T, U>(0)) {};
现在调用bar(Ptr<Derived>{})时,编译器会优先选择bar(Ptr<Middle>),因为Ptr<Derived>到Ptr<Middle>的转换用了高优先级的构造函数,而到Ptr<Base>的转换是低优先级的,完全和原始指针的行为一致。
总结
- 对于标准
std::shared_ptr:可以通过类型特性+SFINAE约束函数重载的优先级,不需要修改shared_ptr的实现; - 自己实现
Ptr<T>:可以在转换构造函数中内置优先级判断,让转换行为和原始指针完全对齐。
内容来源于stack exchange

