一、添加成员函数模板
以一个例子引出,何时设计成员函数模板
- 第一步:我们知道指针的一个特点就是:支持隐式转换。例如“指向non-const对象的指针可以转换为指向const对象”,“派生类指针可以隐式转换为基类指针”等等。代码如下:
class Top {};
class Middle :public Top {};
class Bottom :public Middle {};
Top* pt1 = new Middle; //将Miffle*转换为Top*
Top* pt2 = new Bottom; //将Bottom*转换为Top*
const Top* pct2 = pt1; //将Top*转换为const Top*
- 第二步:假设现在我们设计一个模板,用来模仿智能指针类,并且希望智能指针能像普通指针一样进行类型转换。例如:
class Top {};
class Middle :public Top {};
class Bottom :public Middle {};
//自己设计的智能指针类
template<typename T>
class SmartPtr
{
public:
explicit SmartPtr(T* realPtr);
};
int main()
{
//下面是我们希望能完成的,但是还没有实现
SmartPtr<Top> pt1 = SmartPtr<Middle>(new Middle);
SmartPtr<Top> pt2 = SmartPtr<Bottom>(new Bottom);
SmartPtr<const Top> pct2 = pt1;
return 0;
}
- 第三步:根据上面的需求,我们希望让自己的智能指针类能像普通指针一样进行类型转换,那么我们可以为SmartPtr设计拷贝构造函数或拷贝赋值运算符,那么上面的功能就能实现了。
- 一种低效的做法是:在SmartPtr模板中针对于每一个Top派生类定义一个拷贝构造函数和拷贝赋值运算符。但是这种做法十分低效,因为针对每一个派生类设计相对应的拷贝构造函数和拷贝赋值运算符会使class膨胀,并且如果将来加入新的派生类,那么还需要继续添加新的成员函数
- 第四步:另一种做法是:为SmartPtr模板添加一个成员函数模板,例如:
- 根据下面的拷贝构造函数,我们可以对任何类型T和任何类型U,将一个SmartPtr<U>转换为SmartPtr<T>
- 下面的拷贝构造函数并未声明为explicit:因为原始指针类型之间的转换是隐式转换,如果我们的模板类型为原始指针,那么要支持这种隐式转换,因为我们并未声明explicit
template<typename T>
class SmartPtr
{
public:
//拷贝构造函数,是一个成员函数模板
typename<typename U>
SmartPtr(const SmartPtr<U>& other);
};
- 根据上面的介绍我们可以知道,为类模板设计一个成员函数模板是为了进行统一性与间接性,避免冗余操作
二、约束成员函数模板的行为
- 在“一”中,我们为智能指针类设计了拷贝构造函数,这样就可以根据类型进行类型转换了
- 但是还有一些问题没有解决:
- 那就是,对于类继承来说,派生类指针可以转换为基类指针,但是基类指针不能转换为派生类指针
- 类似的,对于普通类型来说,我们不能将int*转换为double*
- 因此,即使我们设计了成员函数模板,那么还需要考虑一些转换的特殊情况(上面列出的)
解决方法
- 我们可以为自己的智能指针类提供一个类似于shared_ptr的get()成员函数,这个函数返回智能指针锁封装的那个原始指针
- 设计的代码如下:
template<typename T>
class SmartPtr
{
public:
typename<typename U>
SmartPtr(const SmartPtr<U>& other)
:heldPtr(other.get())
T get()const {
return heldPtr;
}
private:
T* heldPtr;
};
- 此处设计的原理:
- get()成员函数返回原始指针
- 在拷贝构造函数中,我们使用了成员初始化列表来进行初始化智能指针封装的原始指针
- 因此,在拷贝构造函数的构造过程中,是根据原始指针进行转换的,因此如果原始指针会自己判断这种转换行为:如果可以转换,那么拷贝构造函数就正确执行;如果不能转换,那么拷贝构造函数出错
三、设计赋值成员函数模板
- 我们上面设计的智能指针模板不限于构造函数,而且还可以自己设计赋值操作
演示说明
- 例如shared_ptr:
- 支持所有“来自内置指针、shared_ptr、auto_ptr、weak_ptr”的构造函数
- 支持上面所有(除去weak_ptr)的赋值操作
- 例如下面是shared_ptr的源码摘录:(其中template参数强烈倾向使用class而不是typename)
template<class T>
class shared_ptr
{
public:
//下面都是拷贝构造函数(列出了一部分)
template<class Y>
explicit shared_ptr(Y* p);
template<class Y>
shared_ptr(shared_ptr<Y> const& r);
template<class Y>
explicit shared_ptr(weak_ptr<Y> const& r);
template<class Y>
explicit shared_ptr(auto_ptr<Y>& r);
//下面都是赋值操作(列出了一部分)
template<class Y>
shared_ptr& operator=(shared_ptr<Y> const& r);
template<class Y>
shared_ptr& operator=(auto_ptr<Y>& r);
};
- 代码说明:
- 构造函数:都是explicit,除了“泛化copy构造函数”除外。因为从某个shared_ptr类型隐式转换为另一个shared_ptr是被允许的,但是从某个内置指针或从其他智能指针进行隐式转换为shared_ptr是不被允许的(除了使用cast进行强制类型转换)
- auto_ptr:参数为auto_ptr的拷贝构造函数和赋值运算符,其参数都不是const的(条款13说过,当你赋值一个auto_ptr时,我们希望其所管理的对象被移动改动)
四、与默认函数的区别
- 我们曾说过,一个类如果没有提供构造函数、拷贝构造函数、拷贝赋值运算符,那么编译器会自动为类提供合成/默认的版本,这一规则同样适用于模板类
- 因此,例如我们上面为自己的类添加了成员函数模板(拷贝构造函数),那么当我们使用拷贝构造函数的时候是调用哪一个版本呢?答案为:根据实际调用情况选择
- 因此,如果我们自己设计成员函数模板还需要拷贝类为我们自己提供的合成版本,在必要时自己设计非成员函数模板
- 例如,下面是shared_ptr的源码摘录:
template<class T>
class shared_ptr
{
public:
//拷贝构造函数
shared_ptr (shared_ptr const& r); //非泛化版本
template<class Y>
shared_ptr(shared_ptr<Y> const& r); //泛化版本
//拷贝赋值运算符
shared_ptr& operator=(shared_ptr const& r); //非泛化版本
template<class Y>
shared_ptr& operator=(shared_ptr<Y> const& r); //泛化版本
};
五、总结
- 请使用member function templates(成员函数模板)生成“可接受所有兼容类型”的函数
- 如果你声明member templates用于“泛化copy构造”或“泛化assignment操作”,你还是需要声明正常的copy构造函数和copy assignment操作符