auto_ptr到底能不能作为容器的元素?
【摘要】C++语言本身来说,它并不在乎用户把什么类型的对象作为STL容器的元素,因为模板类型参数在理论上可以为任何类型。比如说STL容器仅支持“值”语义而不支持“引用(&)”语义,并非因为模板类型参数不能为引用,而是因为如果容器元素为引用类型,就会出现“引用的引用”、“引用的指针”等C++语言不支持的语法和语义。智能指针是一种模拟原始指针行为的对象,因此理论上也可以作为容器的元素,就象原始指针可以作为容器元素一样。但是智能指针毕竟是一种特殊的对象,它们在原始指针共享实值对象的基础能力上增加了自动销毁实值对象的能力,如果将它作为容器的元素,可能导致容器之间共享元素对象实值,这不仅不符合STL容器的概念和“值”语义,也会存在安全隐患,同时也会存在许多应用上的限制,特别是象STL中的auto_ptr这样的智能指针。本文深入地阐述了auto_ptr这种较简单的智能指针“可以”或者“不可以”作为容器元素的根本原因,以及它作为容器元素会存在的限制和带来的问题,最后说明auto_ptr存在的真正意义、正确的使用方法以及它的替代品——带有引用计数能力的智能指针,当容器之间需要共享元素对象时,或者程序中存在大量的指针传递而担心资源泄漏时,这样的智能指针就特别有用。
【关键字】auto_ptr  容器  智能指针
一、引言
Scott Meyers在《More Effective C++》[3]一书中对智能指针及其相关问题(构造、析构、复制、提领、测试以及类型转换等)作了深入的分析,其中也提到“STL的auto_ptr这种在复制时会把对实值对象的拥有权转交出去的智能指针不宜作为STL容器的元素”,而且在他的《Effective STL》[4]Item 8中明确指出了这一点。Nicolai M.Josuttis的《The C++ Standard Library》[5]中有一节专门针对auto_ptr的阐述也指出“auto_ptr不满足STL标准容器对元素的最基本要求”。但是他们都是从容器的需求、语义以及应用的安全性来阐述,而没有从语言的静态类型安全性和auto_ptr的实现方案角度深入地分析其原因,因此有些读者看了之后可能仍然不明就里:它是如何不满足容器需求的?它是如何违反C++的静态类型安全性从而避免误用的?
我们知道,可以作为STL容器的元素的数据类型一般来说需要满足下列条件:
(1)可默认构造的(Default Constructible),也即具有public的default constructor,不论是用户显式定义的还是编译器自动合成的。但是用户定义的带参数的constructor(包括copy
constructor)会抑制编译器合成default constructor。实际上并非任何情况下任何一种容器都强制要求其元素类型满足这一要求,特别是关联式容器,因为只有序列式容器的某些成员函数才可能明确地或隐含地使用元素类型的默认构造函数,如果你不使用这样的成员函数,编译器就不需要元素类型的默认构造函数;
(2)可拷贝构造(Copy Constructible)和拷贝赋值(Copy Assignable)的,即具有public的copy constructor和copy assignment operator,不论是编译器自动合成的还是用户显式定义的。其它版本的operator=()重载并不会抑制编译器合成copy assignment operator,如果你没有显式定义它的话。这个条件可归结为:元素必须是可拷贝的(Copyable),但实际上拷贝赋值的要求也不是强制的,原因和默认构造函数类似;
(3)具有public的destructor,不论是编译器自动合成的还是用户显式定义的;
(4)对于关联式容器,要求其元素必须是可比的(Comparable)。
auto_ptr满足上述条件吗?至少满足前三条,因此至少可以作为序列式容器的元素;如果为auto_ptr定义了比较运算符的话,应该还可以把它作为关联式容器的元素。
但是auto_ptr的特点是接管和转移拥有权,而不是像原始指针那样可以共享实值对象,即:auto_ptr在初始化时接管实值对象和拥有权,而在拷贝时(拷贝构造和拷贝赋值)会交出实值对象及其拥有权。因此,auto_ptr对象和它的拷贝绝对不会共享实值对象,任何两个auto_ptr也不应该共享同一个实值对象。这就是说,auto_ptr对象和它的拷贝并不相同。然而根据STL容器“值” 语义的要求,可拷贝构造意味着一个对象必须和它的拷贝相同(标准中的正式定义比这稍复杂一些)。同样,可赋值意味着把一个对象赋值给另一个同类型对象将产生两个相同的对象。显然,auto_ptr不能满足这一要求,似乎与上面的结论矛盾!
那么问题究竟出在哪里呢?
二、copy constructorcopy assignment operator的形式
在揭开auto_ptr的神秘面纱之前需要了解copy constructor和copy assignment operator的几种合法形式。任何一个类都允许两种形式的copy constructor(C代表任何一个类):
C(const C& copy);
C(C& copy);
同样,copy assignment operator也允许类似的两种形式(返回值类型视实际需要可改变):
C& operator=(const C& copy);
C& operator=(C& copy);
实际上,由于copy assignment operator为普通的运算符重载成员函数,因此还可以为下列形式:
C& operator=(C copy);
这两个函数具体是什么形式,取决于用户的定义或者该类的成员对象及其基类具有什么样的copy constructor和copy assignment operator。比如,如果基类的copy constructor为第一种形式,那么编译器自动为派生类合成的copy constructor也为第一种形式;相反为第二种形式。Copy assignment operator亦类似。具体细节可参考[8]。
这两种形式的区别就在于参数有无修饰符const:如果有const修饰,则该函数体不能修改实
参对象(即拷贝源),也不能调用其non-const成员函数;如果没有const修饰,则该函数可以修改实参对象,也可以调用其non-const成员函数。
从语言的角度讲,任何对象都可以放到容器中(只要不是引用,因为STL容器不支持“引用”语义),只是某些类型的对象会存在安全隐患或者其容器会受到很大的应用限制。如果要防止用户把一些不适宜的对象放入容器中,就要求对象的设计和实现者使用一些语言支持的但不常用的特征。也就是说,要能够在编译阶段就阻止这种具有潜在危险性的行为。常用的方法就是迫使其违反C++静态类型安全规则。
下面我们来看一看auto_ptr到底是如何通过迫使其违反C++静态类型安全规则而在编译时阻止将其作为容器元素的。
三、auto_ptr源码分析
其实auto_ptr的拥有权管理非常简单。根据上一节的阐述,可以使用两种方案来实现auto_ptr。下面是拷贝构造函数和拷贝赋值函数采用non-const参数的一个实现版本:
template<class T>
class auto_ptr
{
private:
T      *m_ptr;            // 原始指针
public:
explicit auto_ptr(T *p = 0) throw()        // explicit constructor
: m_ptr(p){ }                          // *p必须是运行时创建的对象
auto_ptr(auto_ptr& other) throw()        // 非常规copy constructor
: m_lease()){ }          // 转让拥有权,修改了实参对象
#ifdef _SUPPORT_MEMBER_TEMPLATES_
template<class U>
auto_ptr(auto_ptr<U>& other) throw()
: m_lease()){ }          // 转让拥有权,修改了实参对象
#endif
   
auto_ptr& operator=(auto_ptr& other) throw() // 非常规assignment
{
if (&other != this) {
delete m_ptr;                    // 释放实值对象
m_ptr = lease();          // 交出拥有权,修改了实参对象
}
return (*this);
}
   
#ifdef _SUPPORT_MEMBER_TEMPLATES_
template<class U>
auto_ptr& operator=(auto_ptr<U>& other) throw()
{
if (() != this->get()) {
delete m_ptr;                  // 释放实值对象
m_ptr = lease();        // 交出拥有权,修改了实参对象
}
return (*this);
}
#endif
       
            // 从析构函数看,m_ptr必须指向动态创建的对象
~auto_ptr(){ delete m_ptr; }            // destructor,“delete 0”没有任何问题!
T& operator*() const throw(){ return *m_ptr; }
T* operator->() const throw(){ return m_ptr; }
T* get() const throw(){ return m_ptr; }
T* release() throw(){
T *temp = m_ptr;
m_ptr = 0;          // 必要!修改成员,释放拥有权
return temp;
}
void reset(T *p = 0) throw(){
if (p != m_ptr) {
delete m_ptr;
m_ptr = p;
}
}
bool owns() const{ return (m_ptr != 0); }
      // 这里省略了一些无关紧要的东西
};
如你所见,该auto_ptr实现版本的copy constructor和copy assignment operator的参数类型都是non-const的,因为这两个函数都会修改实参对象的数据成员,即调用其release方法(non-const方法)释放其对实值对象的拥有权,并把实值对象的指针置为0。如果参数类型为const的,那么这种修改就不可能直接进行。所以,一旦用一个auto_ptr对象去构造另一个auto_ptr对象,或者把一个auto_ptr对象赋值给另一个auto_ptr对象,你就不能再使用原来的那个auto_ptr对象了,因为反引用NULL指针会导致运行时异常,除非你让它重新接管一个新的实值对象。
这个版本的auto_ptr就不能作为任何容器的元素,如果你这样做了,在编译阶段就会检查出错误,即违反了C++的静态类型安全规则。比如:
std::list< std::auto_ptr<int> >  la;    // auto_ptr列表
std::auto_ptr<int> p1(new int(1));
std::auto_ptr<int> p2(new int(2));
std::auto_ptr<int> p3(new int(3));
la.push_back(p1);                        // compiling-error!
la.push_back(p2);                        // compiling-error!
la.push_back(p3);                        // compiling-error!
set<auto_ptr<int> > sa;                  // auto_ptr集合:假设为auto_ptr定义了operator<
sa.insert(p1);                          // compiling-error!
sa.insert(p2);                          // compiling-error!
sa.insert(p3);                          // compiling-error!
析构方法
STL容器管理元素的方法是动态创建元素的拷贝,并负责管理这些动态分配的资源,即值的深拷贝语义(deep copy),具体由一个可定制的memory allocator来负责,不过这不是我们讨论的重点,因此忽略。可以想象std::list<T>::push_back方法的实际动作如下: