我眼中的C++难点(转)---smart pointer

1、smart pointer是何方神圣?
smart pointer,嗯,一个好东东,不过它的中文名到底叫什么至今还是很混乱,有人叫“聪明指针”,有人则称之为“灵巧指针”,还有人说成“智能指针”,都不太准确。不过话又说回来就连smart pointer也不是名副其实,因为他并不是一个指针,而是一个特殊的类,不过他能够模仿C++中的指针,所以大家也就稀里糊涂地称之为指针了。这里,我们就不翻译了,还叫:smart pointer。

2、smart pointer如何使自己变成一个指针样子的?
要使smart pointer伪装成一个指针,有两个条件必不可少:
1、smart pointer必须是高度类型化的,模板恰好提供了这个功能;
2、smart pointer还要模仿指针主要的两个运算符->和*,这就要用到运算符重载。
template
class smart_pointer
{
public:
smart_pointer(T* p = 0); //空构造
smart_pointer(const smart_pointer& p);//拷贝构造
~smart_pointer(); //析构,完成自动撤销
smart_pointer& operator =(smart_pointer& p);//赋值
T& operator*() const {return *the_p;} //模拟*
T* operator->() const {return the_p;} //模拟->
private:
T *the_p; //受保护的指针
};
这里有一个规则:任何时候,只要你提供了析构函数、拷贝构造函数或赋值运算符中的一个,你通常需要三个都提供。
当然,实际的东西远比这个复杂,这只是一个大概的样子,你可以根据具体的应用环境而作相应的变动。

3、smart pointer有那几种?
到目前为止,C++标准里面只有一个smart pointer,那就是大家熟知的std::auto_ptr。

灵巧指针是一种外观和行为都被设计成与内建指针相类似的对象,不过它能提供更多的功能。它们有许多应用的领域,包括资源管理和重复代码任务的自动化。
当你使用灵巧指针替代C++的内建指针(也就是dumb pointer),你就能控制下面这些方面的指针的行为:
l 构造和析构。你可以决定建立灵巧指针时应该怎么做。通常赋给灵巧指针缺省值0,避免出现令人头疼的未初始化的指针。当指向某一对象的最后一个灵巧指针被释放时,某些灵巧指针被设计成负责删除它们指向的对象。这样做对防止资源泄漏很有帮助。
l 拷贝和赋值。你能对拷贝灵巧指针或有灵巧指针参与的赋值操作进行控制。对于某些类型的灵巧指针来说,期望的行为是自动拷贝它们所指向的对象或用对这些对象进行赋值操作,也就是进行deep copy(深层拷贝)。对于其它的一些灵巧指针来说,仅仅拷贝指针本身或对指针进行赋值操作。还有一部分类型的灵巧指针根本就不允许这些操作。无论你认为应该如何去做,灵巧指针始终受你的控制。
l Dereferencing(取出指针所指东西的内容)。当用户引用被灵巧指针所指的对象,会发生什么事情呢?你可以自行决定。例如你可以用灵巧指针实现条款M17提到的lazy fetching 方法。
灵巧指针从模板中生成,因为要与内建指针类似,必须是strongly typed(强类型)的;模板参数确定指向对象的类型。大多数灵巧指针模板看起来都象这样:template //灵巧指针对象模板
class SmartPtr {
public:
SmartPtr(T* realPtr = 0); // 建立一个灵巧指针
// 指向dumb pointer所指的
// 对象。未初始化的指针
// 缺省值为0(null)
SmartPtr(const SmartPtr& rhs); // 拷贝一个灵巧指针
~SmartPtr(); // 释放灵巧指针
// make an assignment to a smart ptr
SmartPtr& operator=(const SmartPtr& rhs);
T* operator->() const; // dereference一个灵巧指针
// 以访问所指对象的成员
T& operator*() const; // dereference 灵巧指针
private:
T *pointee; // 灵巧指针所指的对象
};
拷贝构造函数和赋值操作符都被展现在这里。对于灵巧指针类来说,不能允许进行拷贝和赋值操作,它们应该被声明为private。两个dereference操作符被声明为const,是因为dereference一个灵巧指针时不会对其自身进行修改(尽管可以修改指针所指的对象)(这样,const型的灵巧指针才可以使用这两个操作符。)。最后,每个指向T对象的灵巧指针包含一个指向T的dumb pointer。这个dumb pointer指向的对象才是灵巧指针指向的真正对象。
进入灵巧指针实现的细节之前,应该研究一下用户如何使用灵巧指针。考虑一下,存在一个分布式系统(即其上的对象一些在本地,一些在远程)。相对于访问远程对象,访问本地对象通常总是又简单而又速度快,因为远程访问需要远程过程调用(RPC),或其它一些联系远距离计算机的方法。对于编写程序代码的用户来说,采用不同的方法分别处理本地对象与远程对象是一件很烦人的事情。让所有的对象看起来都位于一个地方会更方便。灵巧指针可以让程序库实现这样的梦想。
template // 指向位于分布式 DB(数据库)
class DBPtr { // 中对象的灵巧指针模板
public: //
DBPtr(T *realPtr = 0); // 建立灵巧指针,指向
// 由一个本地dumb pointer
// 给出的DB 对象
DBPtr(DataBaseID id); // 建立灵巧指针,
// 指向一个DB对象,
// 具有惟一的DB识别符
... // 其它灵巧指针函数
}; //同上
class Tuple { // 数据库元组类
public:
...
void displayEditDialog(); // 显示一个图形对话框,
// 允许用户编辑元组。
// user to edit the tuple
bool isValid() const; // 返回*this是否通过了
}; // 合法性验证
// 这个类模板用于在修改T对象时进行日志登记。
// 有关细节参见下面的叙述:
template
class LogEntry {
public:
LogEntry(const T& objectToBeModified);
~LogEntry();
};
void editTuple(DBPtr& pt)
{
LogEntry entry(*pt); // 为这个编辑操作登记日志
// 有关细节参见下面的叙述
// 重复显示编辑对话框,直到提供了合法的数值。
do {
pt->displayEditDialog();
} while (pt->isValid() == false);
}
在editTuple中被编辑的元组物理上可以位于本地也可以位于远程,但是编写editTuple的程序员不用关心这些事情。灵巧指针类隐藏了系统的这些方面。就程序员所关心的方面而言,通过灵巧指针对象进行访问元组,除了如何声明它们不同外,其行为就像一个内建指针。
注意在editTuple中LogEntry对象的用法。一种更传统的设计是在调用displayEditDialog前开始日志记录,调用后结束日志记录。在这里使用的方法是让LogEntry的构造函数启动日志记录,析构函数结束日志记录。正如条款M9所解释的,当面对异常时,让对象自己开始和结束日志记录比显示地调用函数可以使得程序更健壮。而且建立一个LogEntry对象比每次都调用开始记录和结束记录函数更容易。
正如你所看到的,使用灵巧指针与使用dump pointer没有很大的差别。这表明了封装是非常有效的。灵巧指针的用户可以象使用dumb pointer一样使用灵巧指针。正如我们将看到的,有时这种替代会更透明化。
l 灵巧指针的构造、赋值和析构
灵巧指针的的构造通常很简单:找到指向的对象(一般由灵巧指针构造函数的参数给出),让灵巧指针的内部成员dumb pointer指向它。如果没有找到对象,把内部指针设为0或发出一个错误信号(可以是抛出一个异常)。
灵巧指针拷贝构造函数、赋值操作符函数和析构函数的实现由于(所指对象的)所有权的问题所以有些复杂。如果一个灵巧指针拥有它指向的对象,当它被释放时必须负责删除这个对象。这里假设灵巧指针指向的对象是动态分配的。这种假设在灵巧指针中是常见的(有关确定这种假设是真实的方法,参见条款M27)。
看一下标准C++类库中auto_ptr模板。这如条款M9所解释的,一个auto_ptr对象是一个指向堆对象的灵巧指针,直到auto_ptr被释放。auto_ptr的析构函数删除其指向的对象时,会发生什么事情呢?auto_ptr模板的实现如下:
template
class auto_ptr {
public:
auto_ptr(T *ptr = 0): pointee(ptr) {}
~auto_ptr() { delete pointee; }
...
private:
T *pointee;
};
假如auto_ptr拥有对象时,它可以正常运行。但是当auto_ptr被拷贝或被赋值时,会发生什么情况呢?
auto_ptr ptn1(new TreeNode);
auto_ptr ptn2 = ptn1; // 调用拷贝构造函数
//会发生什么情况?
auto_ptr ptn3;
ptn3 = ptn2; // 调用 operator=;
// 会发生什么情况?
如果我们只拷贝内部的dumb pointer,会导致两个auto_ptr指向一个相同的对象。这是一个灾难,因为当释放quto_ptr时每个auto_ptr都会删除它们所指的对象。这意味着一个对象会被我们删除两次。这种两次删除的结果将是不可预测的(通常是灾难性的)。
另一种方法是通过调用new,建立一个所指对象的新拷贝。这确保了不会有许多指向同一个对象的auto_ptr,但是建立(以后还得释放)新对象会造成不可接受的性能损耗。并且我们不知道要建立什么类型的对象,因为auto_ptr对象不用必须指向类型为T的对象,它也可以指向T的派生类型对象。虚拟构造函数(参见条款M25)可能帮助我们解决这个问题,但是好象不能把它们用在auto_ptr这样的通用类中。
如果quto_ptr禁止拷贝和赋值,就可以消除这个问题,但是采用“当auto_ptr被拷贝和赋值时,对象所有权随之被传递”的方法,是一个更具灵活性的解决方案:
template
class auto_ptr {
public:
...
auto_ptr(auto_ptr& rhs); // 拷贝构造函数
auto_ptr& // 赋值
operator=(auto_ptr& rhs); // 操作符
...
};
template
auto_ptr::auto_ptr(auto_ptr& rhs)
{
pointee = rhs.pointee; // 把*pointee的所有权
// 传递到 *this
rhs.pointee = 0; // rhs不再拥有
} // 任何东西
template
auto_ptr& auto_ptr::operator=(auto_ptr& rhs)
{
if (this == &rhs) // 如果这个对象自我赋值
return *this; // 什么也不要做
delete pointee; // 删除现在拥有的对象
pointee = rhs.pointee; // 把*pointee的所有权
rhs.pointee = 0; // 从 rhs 传递到 *this
return *this;
}
注意赋值操作符在接受新对象的所有权以前必须删除原来拥有的对象。如果不这样做,原来拥有的对象将永远不会被删除。记住,除了auto_ptr对象,没有人拥有auto_ptr指向的对象。
因为当调用auto_ptr的拷贝构造函数时,对象的所有权被传递出去,所以通过传值方式传递auto_ptr对象是一个很糟糕的方法。因为:
// 这个函数通常会导致灾难发生
void printTreeNode(ostream& s, auto_ptr p)
{ s << *p; }
int main()
{
auto_ptr ptn(new TreeNode);
...
printTreeNode(cout, ptn); //通过传值方式传递auto_ptr
...
}
当printTreeNode的参数p被初始化时(调用auto_ptr的拷贝构造函数),ptn指向对象的所有权被传递到给了p。当printTreeNode结束执行后,p离开了作用域,它的析构函数删除它指向的对象(就是原来ptr指向的对象)。然而ptr已不再指向任何对象(它的dumb pointer是null),所以调用printTreeNode以后任何试图使用它的操作都将产生未定义的行为。只有在你确实想把对象的所有权传递给一个临时的函数参数时,才能通过传值方式传递auto_ptr。这种情况很少见。
这不是说你不能把auto_ptr做为参数传递,这只意味着不能使用传值的方法。通过const引用传递(Pass-by-reference-to-const)的方法是这样的:
// 这个函数的行为更直观一些
void printTreeNode(ostream& s,
const auto_ptr& p)
{ s << *p; }
在函数里,p是一个引用,而不是一个对象,所以不会调用拷贝构造函数初始化p。当ptn被传递到上面这个printTreeNode时,它还保留着所指对象的所有权,调用printTreeNode以后还可以安全地使用ptn。所以通过const引用传递auto_ptr可以避免传值所产生的风险。(“引用传递”替代“传值”的其他原因参见Effective C++条款22)。
在拷贝和赋值中,把对象的所有权从一个灵巧指针传递到另一个中去,这种思想很有趣,而且你可能已经注意到拷贝构造函数和赋值操作符不同寻常的声明方法同样也很有趣。这些函数的参数通常会带有const,但是上面这些函数则没有。实际上在拷贝和赋值中上述这些函数的代码修改了这些参数。也就是说,如果auto_ptr对象被拷贝或做为赋值操作的数据源,就会修改auto_ptr对象!
是的,就是这样。C++是如此灵活能让你这样去做,真是太好了。如果语言要求拷贝构造函数和赋值操作符必须带有const参数,你必须去掉参数的const属性(参见Effective C++条款21)或用其他方法实现所有权的转移。准确地说:当拷贝一个对象或这个对象做为赋值的数据源,就会修改该对象。这可能有些不直观,但是它是简单的、直接的,在这种情况下也是准确的。
如果你发现研究这些auto_ptr成员函数很有趣,你可能希望看看完整的实现。在291页至294页上有(指原书页码),在那里你也能看到,在标准C++库中auto_ptr模板有比这里所描述的更灵活的拷贝构造函数和赋值操作符。在标准C++库中,这些函数是成员函数模板,而不只是成员函数。(在本条款的后面会讲述成员函数模板。也可以阅读Effective C++条款25)。
灵巧指针的析构函数通常是这样的:
template
SmartPtr::~SmartPtr()
{
if (*this owns *pointee) {
delete pointee;
}
}
有时删除前不需要进行测试,例如在一个auto_ptr总是拥有它指向的对象时。而在另一些时候,测试会更为复杂:一个使用了引用计数(参见条款M29)灵巧指针必须在判断是否有权删除所指对象前调整引用计数值。当然还有一些灵巧指针象dumb pointer一样,当它们被删除时,对所指对象没有任何影响。
l 实现Dereference 操作符
让我们把注意力转向灵巧指针的核心部分,operator*和operator-> 函数。前者返回所指的对象。理论上,这很简单:
template
T& SmartPtr::operator*() const
{
perform "smart pointer" processing;
return *pointee;
}
首先,无论函数做什么,必须先初始化指针或使pointee合法。例如,如果使用lazy fetch(参见条款M17),函数必须为pointee建立一个新对象。一旦pointee合法了,operator*函数就返回其所指对象的一个引用。
注意返回类型是一个引用。如果返回对象,尽管编译器允许这么做,却可能导致灾难性后果。必须时刻牢记:pointee不用必须指向T类型对象;它也可以指向T的派生类对象。如果在这种情况下operator*函数返回的是T类型对象而不是派生类对象的引用,你的函数实际上返回的是一个错误类型的对象!(这是一个slicing(切割)问题,参见Effective C++条款22和本书条款13。)在返回的这种对象上调用虚拟函数,不会触发与(原先)所指对象的动态类型相符的函数。实际上就是说你的灵巧指针将不能支持虚拟函数,象这样的指针再灵巧也没有用。而返回一个引用还能够具有更高的效率(不需要构造一个临时对象,参见条款M19)。能够兼顾正确性与效率当然是一件好事。
如果你是一个急性子的人,你可能会想如果一些人在null灵巧指针上调用operator*,也就是说灵巧指针的dumb pointer是null。放松。随便做什么都行。dereference一个空指针的结果是未定义的,所以随你怎么实现都不算错。想抛一个异常么?可以,抛出吧。想调用abort函数(可能被assert在失败时调用)?好的,调用吧。想遍历内存把每个字节都设成你生日与256模数么?当然也可以。虽说这样做没有什么好处,但是就语言本身而言,你完全是自由的。
operator->的情况与operator*是相同的,但是在分析operator->之前,让我们先回忆一下这个函数调用的与众不同的含义。再考虑editTuple函数,其使用一个指向Tuple对象的灵巧指针:
void editTuple(DBPtr& pt)
{
LogEntry entry(*pt);
do {
pt->displayEditDialog();
} while (pt->isValid() == false);
}
语句
pt->displayEditDialog();
被编译器解释为:
(pt.operator->())->displayEditDialog();
这意味着不论operator->返回什么,它必须在返回结果上使用member-selection operator(成员选择操作符)(->)。因此operator->仅能返回两种东西:一个指向某对象的dumb pointer或另一个灵巧指针。多数情况下,你想返回一个普通dumb pointer。在此情况下,你这样实现operator-> :
template
T* SmartPtr::operator->() const
{
perform "smart pointer" processing;
return pointee;
}
这样做运行良好。因为该函数返回一个指针,通过operator->调用虚拟函数,其行为也是正确的。
对于很多程序来说,这就是你需要了解灵巧指针的全部东西。条款M29的引用计数代码并没有比这里更多的功能。但是如果你想更深入地了解灵巧指针,你必须知道更多的有关dumb pointer的知识和灵巧指针如何能或不能进行模拟dumb pointer。如果你的座右铭是“Most people stop at the Z-but not me(多数人浅尝而止,但我不能够这样) ”,下面讲述的内容正适合你
l 测试灵巧指针是否为NULL
目前为止我们讨论的函数能让我们建立、释放、拷贝、赋值、dereference灵巧指针。但是有一件我们做不到的事情是“发现灵巧指针为NULL”:
SmartPtr ptn;
...
if (ptn == 0) ... // error!
if (ptn) ... // error!
if (!ptn) ... // error!
这是一个严重的限制。
在灵巧指针类里加入一个isNull成员函数是一件很容易的事,但是没有解决当测试NULL时灵巧指针的行为与dumb pointer不相似的问题。另一种方法是提供隐式类型转换操作符,允许编译上述的测试。一般应用于这种目的的类型转换是void* :
template
class SmartPtr {
public:
...
operator void*(); // 如果灵巧指针为null,
... // 返回0, 否则返回
}; // 非0。
SmartPtr ptn;
...
if (ptn == 0) ... // 现在正确
if (ptn) ... // 也正确
if (!ptn) ... // 正确
这与iostream类中提供的类型转换相同,所以可以这样编写代码:
ifstream inputFile("datafile.dat");
if (inputFile) ... // 测试inputFile是否已经被
// 成功地打开。
象所有的类型转换函数一样,它有一个缺点:在一些情况下虽然大多数程序员希望它调用失败,但是函数确实能够成功地被调用(参见条款M5)。特别是它允许灵巧指针与完全不同的类型之间进行比较:
SmartPtr pa;
SmartPtr po;
...
if (pa == po) ... // 这能够被成功编译!
即使在SmartPtr 和 SmartPtr之间没有operator= 函数,也能够编译,因为灵巧指针被隐式地转换为void*指针,而对于内建指针类型有内建的比较函数。这种进行隐式类型转换的行为特性很危险。(再回看一下条款M5,必须反反复复地阅读,做到耳熟能详。)
在void*类型转换方面,也有一些变化。有些设计者采用到const void*的类型转换,还有一些采取转换到bool的方法。这些变化都没有消除混合类型比较的问题。
有一种两全之策可以提供合理的测试null值的语法形式,同时把不同类型的灵巧指针之间进行比较的可能性降到最低。这就是在灵巧指针类中重载operator!,当且仅当灵巧指针是一个空指针时,operator!返回true:
template
class SmartPtr {
public:
...
bool operator!() const; // 当且仅当灵巧指针是
... // 空值,返回true。
};
用户程序如下所示:
SmartPtr ptn;
...
if (!ptn) { // 正确
... // ptn 是空值
}
else {
... // ptn不是空值
}
但是这样就不正确了:
if (ptn == 0) ... // 仍然错误
if (ptn) ... // 也是错误的
仅在这种情况下会存在不同类型之间进行比较:
SmartPtr pa;
SmartPtr po;
...
if (!pa == !po) ... // 能够编译
幸好程序员不会经常这样编写代码。有趣的是,iostream库的实现除了提供void*隐式的类型转换,也有operator!函数,不过这两个函数通常测试的流状态有些不同。(在C++类库标准中(参见Effective C++ 条款49和本书条款M35),void*隐式的类型转换已经被bool类型的转换所替代,operator bool总是返回与operator!相反的值。)
l 把灵巧指针转变成dumb指针
有时你要在一个程序里或已经使用dumb指针的程序库中添加灵巧指针。例如,你的分布式数据库系统原来不是分布式的,所以可能有一些老式的库函数没有使用灵巧指针:
class Tuple { ... }; // 同上
void normalize(Tuple *pt); // 把*pt 放入
// 范式中; 注意使用的
// 是dumb指针
考虑一下,如果你试图用指向Tuple的灵巧指针作参数调用normalize,会出现什么情况:
DBPtr pt;
...
normalize(pt); // 错误!
这种调用不能够编译,因为不能把DBPtr转换成Tuple*。你可以这样做,从而使该该函数正常运行:
normalize(&*pt); // 繁琐, 但合法
不过我觉得你会讨厌这种调用方式。
在灵巧指针模板中增加指向T的dumb指针的隐式类型转换操作符,可以让以上函数调用成功运行:
template // 同上
class DBPtr {
public:
...
operator T*() { return pointee; }
...
};
DBPtr pt;
...
normalize(pt); // 能够运行
并且这个函数也消除了测试空值的问题:
if (pt == 0) ... // 正确, 把pt转变成
// Tuple*
if (pt) ... // 同上
if (!pt) ... // 同上 (reprise)
然而,它也有类型转换函数所具有的缺点(几乎总是这样,看条款M5)。它使得用户能够很容易地直接访问dumb指针,绕过了“类指针(pointer-like)”对象所提供的“灵巧”特性:
void processTuple(DBPtr& pt)
{
Tuple *rawTuplePtr = pt; // 把DBPtr 转变成
// Tuple*
使用raw TuplePtr 修改 tuple;
}
通常,灵巧指针提供的“灵巧”行为特性是设计中的主要组成部分,所以允许用户使用dumb指针会导致灾难性的后果。例如,如果DBPtr实现了条款M29中引用计数的功能,允许客户端直接对dumb指针进行操作很可能破坏“引用计数”数据结构,而导致引用计数错误。
甚至即使你提供一个从灵巧指针到dumb指针的隐式转换操作符,灵巧指针也不能真正地做到与dumb指针互换。因为从灵巧指针到dumb指针的转换是“用户定义类型转换”,在同一时间编译器进行这种转换的次数不能超过一次。例如假设有一个表示所有能够访问某一元组的用户的类:
class TupleAccessors {
public:
TupleAccessors(const Tuple *pt); // pt identifies the
... // tuple whose accessors
}; // we care about
通常,TupleAccessors的单参数构造函数也可以作为从Tuple*到TupleAccessors的类型转换操作符(参见条款M5)。现在考虑一下用于合并两个TupleAccessors对象内信息的函数:
TupleAccessors merge(const TupleAccessors& ta1,
const TupleAccessors& ta2);
因为一个Tuple*可以被隐式地转换为TupleAccessors,用两个dumb Tuple*调用merge函数,可以正常运行:
Tuple *pt1, *pt2;
...
merge(pt1, pt2); // 正确, 两个指针被转换为
// TupleAccessors objects
如果用灵巧指针DBPtr进行调用,编译就会失败:
DBPtr pt1, pt2;
...
merge(pt1, pt2); // 错误!不能把 pt1 和
// pt2转换称TupleAccessors对象
因为从DBPtr到TupleAccessors的转换要调用两次用户定义类型转换(一次从DBPtr到Tuple*,一次从Tuple*到TupleAccessors),编译器不会进行这种序列的转换。
提供到dumb指针的隐式类型转换的灵巧指针类也暴露了一个非常有害的bug。考虑这个代码:
DBPtr pt = new Tuple;
...
delete pt;
这段代码应该不能被编译,pt不是指针,它是一个对象,你不能删除一个对象。只有指针才能被删除,对么?
当然对了。但是回想一下条款M5:编译器使用隐式类型转换来尽可能使函数调用成功;再回想一下条款M8:使用delete会调用析构函数和operator delete,两者都是函数。编译器为了使在delete pt语句里的两个函数成功调用,就把pt隐式转换为Tuple*,然后删除它。(本来是你写错了代码,而现在却编译过了,)这样做必然会破坏你的程序。
如果pt拥有它指向的对象,对象就会被删除两次,一次在调用delete时,第二次在pt的析构函数被调用时。当pt不拥有对象,而是其它人拥有时,如果拥有者同时负责删除pt的则情况还好,但是如果拥有者不是负责删除pt的人,可以预料它以后还会再次删除该对象。不论是最前者所述的情况还是最后者的情况都会导致一个对象被删除两次,这样做会产生不能预料的后果。
这个bug极为有害,因为隐藏在灵巧指针后面的全部思想就是让它们不论是在外观上还是在使用感觉上都与dumb指针尽可能地相似。你越接近这种思想,你的用户就越可能忘记正在使用灵巧指针。如果他们忘记了正在使用灵巧指针,肯定会在调用new之后调用delete,以防止资源泄漏,谁又能责备他们这样做不对呢?
底线很简单:除非有一个让人非常信服的原因去这样做,否则绝对不要提供转换到dumb指针的隐式类型转换操作符。
l 灵巧指针和继承类到基类的类型转换
假设我们有一个public继承层次结构,以模型化音乐商店的商品:

class MusicProduct {
public:
MusicProduct(const string& title);
virtual void play() const = 0;
virtual void displayTitle() const = 0;
...
};
class Cassette: public MusicProduct {
public:
Cassette(const string& title);
virtual void play() const;
virtual void displayTitle() const;
...
};
class CD: public MusicProduct {
public:
CD(const string& title);
virtual void play() const;
virtual void displayTitle() const;
...
};
再接着假设,我们有一个函数,给它一个MusicProduct对象,它能显示产品名,并播放它:
void displayAndPlay(const MusicProduct* pmp, int numTimes)
{
for (int i = 1; i <= numTimes; ++i) {
pmp->displayTitle();
pmp->play();
}
}
这个函数能够这样使用:
Cassette *funMusic = new Cassette("Alapalooza");
CD *nightmareMusic = new CD("Disco Hits of the 70s");
displayAndPlay(funMusic, 10);
displayAndPlay(nightmareMusic, 0);
这并没有什么值得惊讶的东西,但是当我们用灵巧指针替代dumb指针,会发生什么呢:
void displayAndPlay(const SmartPtr& pmp,
int numTimes);
SmartPtr funMusic(new Cassette("Alapalooza"));
SmartPtr nightmareMusic(new CD("Disco Hits of the 70s"));
displayAndPlay(funMusic, 10); // 错误!
displayAndPlay(nightmareMusic, 0); // 错误!
既然灵巧指针这么聪明,为什么不能编译这些代码呢?
不能进行编译的原因是不能把SmartPtr或SmartPtr转换成SmartPtr。从编译器的观点来看,这些类之间没有任何关系。为什么编译器的会这样认为呢?毕竟SmartPtr 或 SmartPtr不是从SmartPtr继承过来的,这些类之间没有继承关系,我们不可能要求编译器把一种对象转换成(完全不同的)另一种类型的对象。
幸运的是,有办法避开这种限制,这种方法的核心思想(不是实际操作)很简单:对于可以进行隐式转换的每个灵巧指针类型都提供一个隐式类型转换操作符(参见条款M5)。例如在music类层次内,在Cassette和CD的灵巧指针类内你可以加入operator SmartPtr函数:
class SmartPtr {
public:
operator SmartPtr()
{ return SmartPtr(pointee); }
...
private:
Cassette *pointee;
};
class SmartPtr {
public:
operator SmartPtr()
{ return SmartPtr(pointee); }
...
private:
CD *pointee;
};
这种方法有两个缺点。第一,你必须人为地特化(specialize)SmartPtr类,所以你加入隐式类型转换操作符也就破坏了模板的通用性。第二,你可能必须添加许多类型转换符,因为你指向的对象可以位于继承层次中很深的位置,你必须为直接或间接继承的每一个基类提供一个类型转换符。(如果你认为你能够克服这个缺点,方法是仅仅为转换到直接基类而提供一个隐式类型转换符,那么请再想想这样做行么?因为编译器在同一时间调用用户定义类型转换函数的次数不能超过一次,它们不能把指向T的灵巧指针转换为指向T的间接基类的灵巧指针,除非只要一步就能完成。)
如果你能让编译器为你编写所有的类型转换函数,这会节省很多时间。感谢最近的语言扩展,让你能够做到,这个扩展能声明(非虚)成员函数模板(通常就叫成员模板(member template)),你能使用它来生成灵巧指针类型转换函数,如下:
template // 模板类,指向T的
class SmartPtr { // 灵巧指针
public:
SmartPtr(T* realPtr = 0);
T* operator->() const;
T& operator*() const;
template // 模板成员函数
operator SmartPtr() // 为了实现隐式类型转换.
{
return SmartPtr(pointee);
}
...
};
现在请你注意,这可不是魔术——不过也很接近于魔术。它的原理如下所示。(如果下面的内容让你感到既冗长又令你费解,请不要失望,一会儿我会给出一个例子。我保证你看完例子后,就能够更深入地理解这段内容了。)假设编译器有一个指向T对象的灵巧指针,它要把这个对象转换成指向“T的基类”的灵巧指针。编译器首先检查SmartPtr的类定义,看其有没有声明明确的类型转换符,但是它没有声明。(这不是指:在上面的模板没有声明类型转换符。)编译器然后检查是否存在一个成员函数模板,并可以被实例化成它所期望的类型转换。它发现了一个这样的模板(带有形式类型参数newType),所以它把newType绑定成T的基类类型来实例化模板。这时,惟一的问题是实例化的成员函数代码能否被编译:传递(dumb)指针pointee到指向“T的基类”的灵巧指针的构造函数,必须合法的。指针pointee是指向T类型的,把它转变成指向其基类(public 或 protected)对象的指针必然是合法的,因此类型转换操作符能够被编译,可以成功地把指向T的灵巧指针隐式地类型转换为指向“T的基类”的灵巧指针。
举一个例子会有所帮助。让我们回到CDs、cassettes、music产品的继承层次上来。我们先前已经知道下面这段代码不能被编译,因为编译器不能把指向CD的灵巧指针转换为指向music产品的灵巧指针:
void displayAndPlay(const SmartPtr& pmp,
int howMany);
SmartPtr funMusic(new Cassette("Alapalooza"));
SmartPtr nightmareMusic(new CD("Disco Hits of the 70s"));
displayAndPlay(funMusic, 10); // 以前是一个错误
displayAndPlay(nightmareMusic, 0); // 以前是一个错误
修改了灵巧指针类,包含了隐式类型转换操作符的成员函数模板以后,这个代码就可以成功运行了。拿如下调用举例,看看为什么能够成功运行:
displayAndPlay(funMusic, 10);
funMusic对象的类型是SmartPtr。函数displayAndPlay期望的参数是SmartPtr地对象。编译器侦测到类型不匹配,于是寻找把funMusic转换成SmartPtr对象的方法。它在SmartPtr类里寻找带有SmartPtr类型参数的单参数构造函数(参见条款M5),但是没有找到。然后它们又寻找成员函数模板,以实例化产生这样的函数。它们在SmartPtr发现了模板,把newType绑定到MusicProduct上,生成了所需的函数。实例化函数,生成这样的代码:
SmartPtr:: operator SmartPtr()
{
return SmartPtr(pointee);
}
能编译这行代码么?实际上这段代码就是用pointee做为参数调用SmartPtr的构造函数,所以真正的问题是能否用一个Cassette*指针构造一个SmartPtr对象。现在我们对dumb指针类型之间的转换已经很熟悉了,Cassette*能够被传递给需要MusicProduct*指针的地方。因此SmartPtr构造函数可以成功调用,同样SmartPtr到 SmartPtr之间的类型转换也能成功进行。太棒了,实现了灵巧指针之间的类型转换,还有什么比这更简单么?
此外,还有其它功能么?不要被这个例子误导,而认为这种方法只能用于把指针在继承层次中向上进行类型转换。这种方法可以成功地用于任何合法的指针类型转换。如果你有dumb指针T1*和另一种dumb指针T2*,当且仅当你能隐式地把T1*转换为T2*时,你就能够隐式地把指向T1的灵巧指针类型转换为指向T2的灵巧指针类型。
这种技术能给我们几乎所有想要的行为特性。假设我们用一个新类CasSingle来扩充MusicProduct类层次,用来表示cassette singles。修改后的类层次看起来象这样:

现在考虑这段代码:
template // 同上, 包括作为类型
class SmartPtr { ... }; // 转换操作符的成员模板
void displayAndPlay(const SmartPtr& pmp,
int howMany);
void displayAndPlay(const SmartPtr& pc,
int howMany);
SmartPtr dumbMusic(new CasSingle("Achy Breaky Heart"));
displayAndPlay(dumbMusic, 1); // 错误!
在这个例子里,displayAndPlay被重载,一个函数带有SmartPtr 对象参数,其它函数的参数为SmartPtr,我们期望调用SmartPtr,因为CasSingle是直接从Cassette上继承下来的,而间接继承自MusicProduct。当然这是dumb指针时的工作方法。我们的灵巧指针不会这么灵巧,它们的转换操作符是成员函数,对C++编译器而言,所有类型转换操作符是同等地位的。因此displayAndPlay的调用具有二义性,因为从SmartPtr 到SmartPtr的类型转换并不比到SmartPtr的类型转换优先。
通过成员模板来实现灵巧指针的类型转换还有两个缺点。第一,支持成员模板的编译器较少,所以这种技术不具有可移植性。以后情况会有所改观,但是没有人知道这会等到什么时候。第二,这种方法的工作原理不很明了,要理解它必须先要深入理解函数调用时的参数匹配,隐式类型转换函数,模板函数隐式实例化和成员函数模板。有些程序员以前从来没有看到过这种技巧,而却被要求维护使用了这种技巧的代码,我真是很可怜他们。这种技巧确实很巧妙,这自然是肯定,但是过于的巧妙可是一件危险的事情。
不要再拐弯抹角了,直接了当地说,我们想要知道的是在继承类向基类进行类型转换方面,我们如何能够让灵巧指针的行为与dumb指针一样呢?答案很简单:不可能。正如Daniel Edelson所说,灵巧指针固然灵巧,但不是指针。最好的方法是使用成员模板生成类型转换函数,在会产生二义性结果的地方使用casts(类型转换,参见条款M2)。这不是一个完美的方法,不过已经很不错了,在一些情况下需去除二义性,所付出的代价与灵巧指针提供复杂的功能相比还是值得的。
l 灵巧指针和const
对于dumb指针来说,const既可以针对指针所指向的东西,也可以针对于指针本身,或者兼有两者的含义(参见Effective C++条款21):
CD goodCD("Flood");
const CD *p; // p 是一个non-const 指针
//指向 const CD 对象
CD * const p = &goodCD; // p 是一个const 指针
// 指向non-const CD 对象;
// 因为 p 是const, 它
// 必须被初始化
const CD * const p = &goodCD; // p 是一个const 指针
// 指向一个 const CD 对象
我们自然想要让灵巧指针具有同样的灵活性。不幸的是只能在一个地方放置const,并只能对指针本身起作用,而不能针对于所指对象:
const SmartPtr p = // p 是一个const 灵巧指针
&goodCD; // 指向 non-const CD 对象
好像有一个简单的补救方法,就是建立一个指向cosnt CD的灵巧指针:
SmartPtr p = // p 是一个 non-const 灵巧指针
&goodCD; // 指向const CD 对象
现在我们可以建立const和non-const对象和指针的四种不同组合:
SmartPtr p; // non-const 对象
// non-const 指针
SmartPtr p; // const 对象,
// non-const 指针
const SmartPtr p = &goodCD; // non-const 对象
// const指针
const SmartPtr p = &goodCD; // const 对象
// const 指针
但是美中不足的是,使用dumb指针我们能够用non-const指针初始化const指针,我们也能用指向non-cosnt对象的指针初始化指向const对象的指针;就像进行赋值一样。例如:
CD *pCD = new CD("Famous Movie Themes");
const CD * pConstCD = pCD; // 正确
但是如果我们试图把这种方法用在灵巧指针上,情况会怎么样呢?
SmartPtr pCD = new CD("Famous Movie Themes");
SmartPtr pConstCD = pCD; // 正确么?
SmartPtr 与SmartPtr是完全不同的类型。在编译器看来,它们是毫不相关的,所以没有理由相信它们是赋值兼容的。到目前为止这是一个老问题了,把它们变成赋值兼容的唯一方法是你必须提供函数,用来把SmartPtr类型的对象转换成SmartPtr类型。如果你使用的编译器支持成员模板,就可以利用前面所说的技巧自动生成你需要的隐式类型转换操作。(我前面说过,只要对应的dumb指针能进行类型转换,灵巧指针也就能进行类型转换,我没有欺骗你们。带const的类型转换也没有问题。)如果你没有这样的编译器,你必须克服更大的困难。
带const的类型转换是单向的:从non-const到const的转换是安全的,但是从const到non-const则不是安全的。而且用const指针能做的事情,用non-const指针也能做,但是用non-const指针还能做其它一些事情(例如,赋值操作)。同样,用指向const对象的指针能做的任何事情,用指向non-const对象的指针也能做到,但是用指向non-const对象的指针能够完成一些指向const对象的指针所不能完成的事情(例如,赋值操作)。
这些规则看起来与public继承的规则相类似(Effective C++ 条款35)。你能够把一个派生类对象转换成基类对象,但是反之则不是这样,你对基类所做的任何事情对派生类也能做,但是还能对派生类做另外一些事情。我们能够利用这一点来实现灵巧指针,就是说可以让每个指向T的灵巧指针类public派生自一个对应的指向const-T的灵巧指针类:

template // 指向const对象的
class SmartPtrToConst { // 灵巧指针
... // 灵巧指针通常的
// 成员函数
protected:
union {
const T* constPointee; // 让 SmartPtrToConst 访问
T* pointee; // 让 SmartPtr 访问
};
};
template // 指向non-const对象
class SmartPtr: // 的灵巧指针
public SmartPtrToConst {
... // 没有数据成员
};
使用这种设计方法,指向non-const-T对象的灵巧指针包含一个指向const-T的dumb指针,指向const-T的灵巧指针需要包含一个指向cosnt-T的dumb指针。最方便的方法是把指向const-T的dumb指针放在基类里,把指向non-const-T的dumb指针放在派生类里,然而这样做有些浪费,因为SmartPtr对象包含两个dumb指针:一个是从SmartPtrToConst继承来的,一个是SmartPtr自己的。
一种在C世界里的老式武器可以解决这个问题,这就是union,它在C++中同样有用。Union在protected中,所以两个类都可以访问它,它包含两个必须的dumb指针类型,SmartPtrToConst对象使用constPointee指针,SmartPtr对象使用pointee指针。因此我们可以在不分配额外空间的情况下,使用两个不同的指针(参见Effective C++条款10中另外一个例子)这就是union美丽的地方。当然两个类的成员函数必须约束它们自己仅仅使用适合的指针。这是使用union所冒的风险。
利用这种新设计,我们能够获得所要的行为特性:
SmartPtr pCD = new CD("Famous Movie Themes");
SmartPtrToConst pConstCD = pCD; // 正确
l 评价
有关灵巧指针的讨论该结束了,在我们离开这个话题之前,应该问这样一个问题:灵巧指针如此繁琐麻烦,是否值得使用,特别是如果你的编译器缺乏支持成员函数模板时。
通常是值得的。例如通过使用灵巧指针极大地简化了条款M29中的引用计数代码。而且正如该例子所显示的,灵巧指针的使用在一些领域受到极大的限制,例如测试空值、转换到dumb指针、继承类向基类转换和对指向const的指针的支持。同时灵巧指针的实现、理解和维护需要大量的技巧。调试使用灵巧指针的代码也比调试使用dumb指针的代码困难。无论如何你也不可能设计出一种通用目的的灵巧指针,能够替代dumb指针。
达到同样的代码效果,使用灵巧指针更方便。灵巧指针应该谨慎使用, 不过每个C++程序员最终都会发现它们是有用的。

比如C++标准库里的std::auto_ptr就是应用很广的一个例子。它的实现在不同版本的STL中虽有不同,但原理都是一样,大概是下面这个样子:
template class auto_ptr
{
public:
typedef X element_type;
explicit auto_ptr(X* p = 0) throw()
: the_p(p) {}
auto_ptr(auto_ptr& a) throw()
: the_p(a.release()) {}
auto_ptr& operator =(auto_ptr& rhs) throw()
{
reset(rhs.release());
return *this;
}
~auto_ptr() throw() {delete the_p;}
X& operator* () const throw() {return *the_p;}
X* operator-> () const throw() {return the_p;}
X* get () const throw() {return the_p;}
X* release() throw()
{
X* tmp = the_p;
the_p = 0;
return tmp;
}
void reset(X* p = 0) throw()
{
if (the_p!=p)
{
delete the_p;
the_p = p;
}
}
private:
X* the_p;
};
关于auto_ptr的使用我不想多说,这不是我们今天的主要话题。它的主要优点是不用delete,可以自动回收已经被分配的空间,由此可以避免资源泄露的问题。很多Java的拥护者经常不分黑白的污蔑C++没有垃圾回收机制,其实不过是贻笑大方而已。抛开在网上许许多多的商业化和非商业化的C++垃圾回收库不提,auto_ptr就足以有效地解决这一问题。并且即使在产生异常的情况下,auto_ptr也能正确地回收资源。这对于写出异常安全(exception-safe)的代码具有重要的意义。
那在使用smart pointer的过程中,是否有什么值得注意的问题呢?
这个问题就太泛泛了,针对不同的smart pointer,有不同的注意事项。比如auto_ptr,您就不能把它用在标准容器里,因为它只在内存中保留一份实例。不过我相信把握我前面说的两个原则:smart pointer是类而不是指针,是模仿指针,那么一切问题都好办。比如,smart pointer作为一个类,那么以下的做法就可能有问题。
SmartPtr p;
if(p==0)
if(!p)
if(p)
很显然,p不是一个真正的指针,这么做可能出错。而SmartPtr的设计也是很重要的因素。您可以加上一个bool SmartPtr::null() const来进行判断。如果坚持非要用上面的形式,那也未尝不可。我们就加上operator void* ()试试:
template class SmartPtr {
public:
...
operator void*() const {return the_p;}
...
private:
T* the_p;
};
这招在basic_ios中就使用过了。这里也可以更灵活地处理,比如类本身需要operator void*()这样地操作,那么上面这招就不灵了。那我们还有重载operator !()等等方法。不怕做不到,只怕想不到。


您能总结一下smart pointer的实质吗?
smart pointer的实质就是一个外壳,一层包装。正是多了这层包装,我们可以做出许多普通指针无法完成的事,比如前面资源自动回收,或者自动进行引用记数,比如ATL中CComPtr和CComQIPtr这两个COM接口指针类。然而事事都是一把双刃剑,正由于多了这些功能,又会使smart pointer丧失一些功能。一定切记,画虎画皮难画骨,smart pointer毕竟和真正的指针是大大不同的。

[@more@]

来自 “ ITPUB博客 ” ,链接:http://blog.itpub.net/430210/viewspace-911553/,如需转载,请注明出处,否则将追究法律责任。

转载于:http://blog.itpub.net/430210/viewspace-911553/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值