读书笔记 effective c++ Item 45 使用成员函数模板来接受“所有兼容类型”

智能指针的行为像是指针,但是没有提供加的功能。例如,Item 13中解释了如何使用标准auto_ptr和tr1::shared_ptr指针在正确的时间自动删

堆上的资源。STL容器中的迭代器基本上都是智能指针:当然,你不能通过使用“++”来将链表中的指向一个节点的内建指针移到下一个节点上去,但是list::iterator可以这么做。

1. 问题分析——如何实现智能指针的隐式转换

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

1
2
3
4
5
6
7
8
class Top { ... };
class Middle: public Top { ... };
class Bottom: public Middle { ... };
Top *pt1 = new Middle; // convert Middle* ⇒ Top*
Top *pt2 = new Bottom; // convert Bottom* ⇒ Top*
const Top *pct2 = pt1; // convert Top* ⇒ const Top*

在用户自定义的智能指针中模仿这种转换是很微妙的。我们想让下面的代码通过编译:

1
2
3
4
5
6
7
8
9
10
11
12
template<typename T>
class SmartPtr {
public: // smart pointers are typically
explicit SmartPtr(T *realPtr); // initialized by built-in pointers
...
};
SmartPtr<Top> pt1 = // convert SmartPtr<Middle> ⇒
SmartPtr<Middle>(new Middle); // SmartPtr<Top>
SmartPtr<Top> pt2 = // convert SmartPtr<Bottom> ⇒
SmartPtr<Bottom>(new Bottom); // SmartPtr<Top>
SmartPtr<const Top> pct2 = pt1; // convert SmartPtr<Top> ⇒
// SmartPtr<const Top>

同一个模板的不同实例之间没有固有的关系,所以编译器将SmartPtr和SmartPtr视为完全不同的类,它们之间的关系不比vector和Widget来的近。为了实现SmartPtr类之间的转换,我们必须显示的实现。

在上面的智能指针示例代码中,每个语句都创建了一个新的智能指针对象,所以现在我们把焦点放在如何实现出一个行为表现如我们所愿的智能指针构造函数。关键的一点是没有办法实现我们需要的所有构造函数。在上面的继承体系中,我们可以用一个SmartPtr或一个SmartPtr来构造一个SmartPtr,但是如果这个继承体系在未来扩展了,SmartPtr对象必须能够从其他智能指针类型中构造出来。例如,如果我们增加了下面的类:

1
class BelowBottom: public Bottom { ... };

我们将会需要支持用SmartPtr对象来创建SmartPtr对象,我们当然不想通过修改SmartPtr模板来实现它。

2. 使用成员函数模板——泛化拷贝构造函数进行隐式转换

从原则上来说,我们所需要的构造函数的数量是没有限制的。既然模板可以被实例化成为没有限制数量的函数,因此看上去我们不需要一个SmartPtr的构造函数,我们需要的是一个构造函数模板。这样的模板是成员函数模板(member function templates) (也被叫做member templates)的一个例子——也即是为类产生成员函数的模板:

1
2
3
4
5
6
7
8
9
template<typename T>
class SmartPtr {
public:
template<typename U> // member template
SmartPtr(const SmartPtr<U>& other); // for a ”generalized
... // copy constructor”
};

这就是说对于每个类型T和每个类型U,一个SmartPtr能够用SmartPtr创造出来,因为SmartPtr有一个以SmartPtr作为参数的构造函数 。像这样的构造函数——用一个对象来创建另外一个对象,两个对象来自于相同的模板但是它们为不同类型(例如,用SmartPtr来创建SmartPtr),它通常被叫做泛化拷贝构造函数(generalized copy constructors)。

2.1 隐式转换不需要explicit

上面的泛化拷贝构造函数并没有被声明为explicit。这是经过仔细考虑的。内建指针类型之间的类型转换(例如从派生类转换到基类指针)是隐式的,并且不需要cast,因此智能指针模仿这种行为就是合理的。在模板化的构造函数上省略explicit正好做到了这一点。

2.2 将不符合要求的模板实例化函数剔除掉

为SmartPtr实现的泛化拷贝构造函数比我们想要的提供了更多的东西。我们想要用SmartPtr创建SmartPtr,但是我们不想用SmartPtr创建SmartPtr,因为这违背了public继承的含义(Item 32)。我们同样不想用SmartPtr创建SmartPtr,因为没有从double到int之间的隐式转换。因此,我们必须将成员模板生成的这种成员函数集合剔除掉。

假设SmartPtr遵循auto_ptr和tr1::shared_ptr的设计,也提供一个get成员函数来返回智能指针对象所包含的内建类型指针的一份拷贝(Item 15),我们可以使用构造函数模板的实现来对一些转换进行限制:

1
2
3
4
5
6
7
8
9
10
11
template<typename T>
class SmartPtr {
public:
template<typename U>
SmartPtr(const SmartPtr<U>& other) // initialize this held ptr
: heldPtr(other.get()) { ... } // with other’s held ptr
T* get() const { return heldPtr; }
...
private: // built-in pointer held
T *heldPtr; // by the SmartPtr
}

我们在成员初始化列表中用SmartPtr中包含的类型为U的指针来初始化SmartPtr中的类型为T的数据成员。这只有在能够从U指针到T指针进行隐式转换的情况下才能通过编译,这也正是我们所需要的。实际结果是现在SmartPtr有了一个泛化拷贝构造函数,只有传递的参数为兼容类型时才能够通过编译。

3. 成员函数模板对赋值的支持

成员函数模板的使用不仅仅限定在构造函数上。它们的另外一个普通的角色是对赋值的支持。例如,tr1的shared_ptr(Item 13)支持用所有兼容的内建指针来对其进行构造,可以用tr1::shared_ptr,auto_ptr和tr1::weak_ptr(Item 54)来进行构造,对赋值也同样使用,但是tr1::weak_ptr例外。下面是从tr1的说明中摘录下来的tr1::shared_ptr的实现,可以看到在声明模板参数的时候它倾向于使用class而不是typename。(Item 42中描述的,在这个上下文中它们的意义相同。)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
template<class T> class shared_ptr {
public:
template<class Y> // construct from
explicit shared_ptr(Y * p); // any compatible
template<class Y> // built-in pointer,
shared_ptr(shared_ptr<Y> const& r); // shared_ptr,
template<class Y> // weak_ptr, or
explicit shared_ptr(weak_ptr<Y> const& r); // auto_ptr
template<class Y>
explicit shared_ptr(auto_ptr<Y>& r);
template<class Y> // assign from
shared_ptr& operator=(shared_ptr<Y> const& r); // any compatible
template<class Y> // shared_ptr or
shared_ptr& operator=(auto_ptr<Y>& r); // auto_ptr
...
};

所有的这些构造函数都是explicit的,除了泛化拷贝构造函数。这就意味着从shared_ptr的一种类型隐式转换到shared_ptr的另一种类型是允许的,但是内建类型指针和其他的智能指针类型到shared_ptr的隐式转换是禁止的。(显示的转换是可以的(例如通过使用cast))。同样有趣的是传递给tr1::shared_ptr构造函数和赋值运算符的auto_ptr没有被声明为const,但是tr1::shared_ptr和tr1::weak_ptr的传递却声明为const了。这是因为auto_ptr被拷贝的时候已经被修改了(Item 13)。

4. 成员函数模板会生成默认拷贝构造函数

成员函数模板是美好的东西,但是它们没有修改语言的基本规则。Item 5解释了编译器会自动生成的4个成员函数中的两个函数为拷贝构造函数和拷贝赋值运算符。Tr1::shared_ptr声明了一个泛化拷贝构造函数,很清楚的是如果类型T和类型Y是相同的,泛化拷贝构造函数就会被实例化成一个“普通”的拷贝构造函数。那么编译器会为tr1::shared_ptr生成一个拷贝构造函数么?或者说用相同类型的tr1::shared_ptr构造另外一个tr1::shared_ptr的时候,编译器会实例化泛化拷贝构造函数么?

正如我所说的,成员模板没有修改语言的规则。“如果你需要一个拷贝构造函数而你没有自己声明,编译器会为你生成一个”这条规则也是其中之一。在一个类中声明一个泛化拷贝构造函数(一个member template)不会阻止编译器生成它们自己的拷贝构造函数(non-template),所以如果你想控制拷贝构造函数的所有方面,你必须同时声明一个泛化拷贝构造函数和“普通的”构造函数。对于赋值同样适用。下面是tr1::shared_ptr的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
template<class T> class shared_ptr {
public:
shared_ptr(shared_ptr const& r); // copy constructor
template<class Y> // generalized
shared_ptr(shared_ptr<Y> const& r); // copy constructor
shared_ptr& operator=(shared_ptr const& r); // copy assignment
template<class Y> // generalized
shared_ptr& operator=(shared_ptr<Y> const& r); // copy assignment
...
};

5. 总结

  • 使用成员函数模板来生成接受所有兼容类型的函数。
  • 如果你为泛化拷贝构造函数和泛化赋值运算符声明成员模板,你同样需要声明普通的拷贝构造函数和拷贝赋值运算符。