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

相关推荐
Capricorn198817 分钟前
Ox Alpha 1M 上下文科研引用失真怎么排查?知芽 Notebook Skill 技术链路实操
人工智能·笔记·python·深度学习·知识图谱·论文笔记
0_Karl_023 分钟前
永磁爬壁机器人开源资源指南
学习·机器人
爱和冰阔落25 分钟前
【Linux】多线程打印为什么会乱?pthread 创建、等待、退出、取消与分离全实战
linux·运维·c++·redis
luj_176827 分钟前
尾椎藏今生记忆?骨盆对应不确定性
开发语言·网络·c++·经验分享·算法
Bode_200231 分钟前
AI工具/大模型学习
人工智能·学习
断点之下38 分钟前
C++类和对象:六个默认成员函数详解
开发语言·c++
南宫萧幕41 分钟前
计算机四级网络工程师:操作系统原理与计算机网络 · 综合知识点笔记
网络·笔记·计算机网络
小蒜学长1 小时前
springboot党建云课堂学习与管理系统(代码+数据库+LW)
java·数据库·spring boot·后端·学习
weixin_466068111 小时前
《十分钟冥想》精读笔记(下):把正念活成生活方式,附全书10个练习索引
笔记·生活