Effective C++ 学习笔记 条款45 运用成员函数模板接受所有兼容类型

智能指针是行为很像指针但提供了指针所不具备之功能的对象。例如,条款13解释了标准库中的 auto_ptr 和 tr1::shared_ptr 如何能在适当的时机自动删除堆上分配的资源。STL 容器的迭代器几乎都是智能指针;你当然不能指望通过"++"将一个内置指针从链表的一个节点移到下一个节点,但对于 list::iterator 来说这却是可行的。

真实指针做得很好的事情之一,是支持隐式转换。派生类指针隐式转换为基类指针,指向非 const 对象的指针转换为指向 const 对象的指针,等等。例如,考虑在一个三层继承体系中可能发生的一些转换:

在用户自定义的智能指针类中模拟这样的转换是棘手的。我们需要让以下代码能够编译:

同一模板的不同实例化之间没有内在关系,所以编译器把 SmartPtr<Middle>SmartPtr<Top> 视为完全不同的类,它们之间的关联并不比 vector<float>Widget 更密切。为了获得我们想要的 SmartPtr 类之间的转换,我们必须显式地对它们进行编程。

在之前的智能指针示例代码中,每条语句都创建了一个新的智能指针对象,所以现在我们重点讨论如何编写行为符合我们预期的智能指针构造函数。一个关键的观察是:我们无法写出所有我们需要的构造函数。在上面的继承体系中,我们可以从 SmartPtr<Middle>SmartPtr<Bottom> 构造 SmartPtr<Top>,但如果未来扩展了继承体系,SmartPtr<Top> 对象还必须能够从其他智能指针类型构造。例如,如果我们后来添加了:

我们就需要支持从 SmartPtr<BelowBottom> 对象创建 SmartPtr<Top> 对象,而我们当然不想为了这个而修改 SmartPtr 模板。

从原则上讲,我们需要的构造函数数量是无限的。既然模板可以被实例化以生成无限数量的函数,看来我们需要的不是 SmartPtr 的一个构造函数,而是一个构造函数模板。这类模板就是成员函数模板(通常简称为成员模板)的例子------即生成类的成员函数的模板:

这表示对于每一个类型 T 和每一个类型 U,都可以从一个 SmartPtr<U> 创建一个 SmartPtr<T>,因为 SmartPtr<T> 有一个接受 SmartPtr<U> 参数的构造函数。像这样的构造函数------从一个类型为同一模板的不同实例化的对象创建另一个对象的构造函数(例如,从 SmartPtr<U> 创建 SmartPtr<T>)------有时被称为泛化拷贝构造函数(generalized copy constructors)。

上面的泛化拷贝构造函数没有声明为 explicit。这是有意为之的。内置指针类型之间的类型转换(例如从派生类指针到基类指针)是隐式的,不需要强制转型,所以智能指针模拟这种行为是合理的。省略模板化构造函数上的 explicit 正好做到了这一点。

按照上述声明,SmartPtr 的泛化拷贝构造函数提供的功能超出了我们想要的。是的,我们希望能够从 SmartPtr<Bottom> 创建 SmartPtr<Top>,但我们不希望能够从 SmartPtr<Top> 创建 SmartPtr<Bottom>,因为那违背了 public 继承的含义(参见条款32)。我们也不希望能够从 SmartPtr<double> 创建 SmartPtr<int>,因为不存在从 double* 到 int* 的相应隐式转换。在某种程度上,我们必须对这个成员模板将要生成的成员函数进行筛选。

假设 SmartPtr 仿照 auto_ptr 和 tr1::shared_ptr 的做法,提供了一个 get 成员函数,该函数返回智能指针对象所持有的内置指针的一份副本(参见条款15),那么我们可以利用构造函数模板的实现来将转换限制为我们想要的类型:

我们使用成员初始化列表来将 SmartPtr<T> 中类型为 T* 的数据成员,用 SmartPtr<U> 所持有的类型为 U* 的指针来初始化。只有当存在从 U* 指针到 T* 指针的隐式转换时,这段代码才能编译,而这正是我们想要的。最终效果是,SmartPtr<T> 现在拥有一个泛化拷贝构造函数,只有当传入一个兼容类型的参数时才会编译通过。

成员函数模板的用途并不仅限于构造函数。另一个常见的角色是支持赋值操作。例如,TR1 的 shared_ptr(同样参见条款13)支持从所有兼容的内置指针、tr1::shared_ptr、auto_ptr 和 tr1::weak_ptr(参见条款54)进行构造,并且还支持从除 tr1::weak_ptr 以外的所有这些类型进行赋值。下面是 TR1 规范中关于 tr1::shared_ptr 的一段摘录,其中还包括它在声明模板参数时偏好使用 class 而非 typename 的习惯(如条款42所解释的,在此上下文中它们含义完全相同):

这些构造函数除了泛化拷贝构造函数之外,都被声明为 explicit。这意味着允许从一种类型的 shared_ptr 到另一种类型的 shared_ptr 的隐式转换,但不允许从内置指针或其他智能指针类型的隐式转换(显式转换------比如通过强制转型------是允许的)。同样有趣的是,传递给 tr1::shared_ptr 构造函数和赋值操作符的 auto_ptr 参数没有被声明为 const,这与 tr1::shared_ptr 和 tr1::weak_ptr 的传递方式形成对比。这是因为 auto_ptr 在被拷贝时会被修改,这一点上它是独一无二的(参见条款13)。

成员函数模板是很棒的东西,但它们并不改变语言的基本规则。条款5解释说,编译器可能生成的四个成员函数中有两个是拷贝构造函数和拷贝赋值操作符。tr1::shared_ptr 声明了一个泛化拷贝构造函数,而且很明显,当类型 T 和 Y 相同时,泛化拷贝构造函数可以被实例化以创建"常规"拷贝构造函数。那么当从一个 tr1::shared_ptr 对象构造另一个同类型的 tr1::shared_ptr 对象时,编译器是生成一个拷贝构造函数,还是实例化泛化拷贝构造函数模板呢?

如我所说,成员模板并不改变语言的规则,而规则规定:如果需要拷贝构造函数而你没有声明一个,编译器会自动为你生成一个。在类中声明一个泛化拷贝构造函数(一个成员模板)并不能阻止编译器生成它们自己的拷贝构造函数(一个非模板)。因此,如果你想控制拷贝构造的所有方面,就必须同时声明泛化拷贝构造函数和"常规"拷贝构造函数。赋值操作也是如此。

下面是一段摘自 tr1::shared_ptr 定义的示例,说明了这一点:

切记:

1.使用成员函数模板来生成接受所有兼容类型的函数。

2.如果你声明了用于泛化拷贝构造或泛化赋值的成员模板,你仍然需要同时声明常规的拷贝构造函数和拷贝赋值操作符。

相关推荐
qq_1998868716 小时前
第6板块·第4节:构建系统与多文件项目
c++·人工智能·gpu算力
知识分享小能手17 小时前
深度学习学习教程,从入门到精通,深度生成模型 —— 知识点详解与代码实现(20)
人工智能·深度学习·学习
汉克老师17 小时前
GESP2026年9月认证C++三级( 第三部分编程题(2、分割字符串))精讲
c++·gesp·小学生·学c++编程
估值探索者17 小时前
【Python量化系统工程化 #08】关了 SSH 就停?systemd 让脚本开机自启 + 异常自动拉起
java·c++·人工智能·分类·数据挖掘
m4Rk_17 小时前
【论文阅读】Agent 记忆机制(74):CompassMem——从相似度检索走向事件图上的记忆导航
论文阅读·人工智能·学习·开源·github
小李不想当小白18 小时前
DMA直接存储器存取(STM32标准库学习笔记)
笔记·stm32·单片机·嵌入式硬件·学习·分享
天若有情67318 小时前
C++花式魔改!手写头文件复刻Java语法,彻底打破编码思维定式
java·c++·语言做语言·jac++
qq_1998868718 小时前
第7板块·第3节:CUTLASS 的 GEMM 实现与优化策略
c++·人工智能·gpu算力·cuda
hetao173383718 小时前
2026-09-17 hetao1733837 的刷题记录
c++·算法
shada19 小时前
Boost.Process v2 在旧内核上报 `assign: Bad file descriptor`
c++