C++ template —— 智能指针

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++ template —— 智能指针相关的知识,希望对你有一定的参考价值。

在管理动态分配的内存时,一个最棘手的问题就是决定何时释放这些内存,而智能指针就是用来简化内存管理的编程方式。智能指针一般有独占和共享两种所有权模型。
------------------------------------------------------------------------------------------------------------
20.1 holder和trule
本节将介绍两种智能指针类型:holder类型独占一个对象;而trule可以使对象的拥有者从一个holder传递给另一个holder。

20.1.1 安全处理异常
正常情况下,程序只有一个入口一个出口,异常的出现使程序多了其他出口,导致程序可能会提前终止,对异常的不当使用会导致许多问题,特别是内存泄漏问题。即便我们可以通过异常处理机制来解决这种问题,但我们会发现异常执行路径会影响程序正常的执行路径了,并且对象的释放操作不得不在两个不同的地方执行:一个在正常执行路径,一个在异常执行路径。
对于动态分配的内存,只要遵循“谁申请谁释放”的原则,一般都不会导致内存泄漏,但异常的出现令这种内存管理变得更加复杂,智能指针旨在解决这个问题。
同时,通常都应该避免使用会抛出异常的析构函数,因为当一个异常被抛出的时候,析构函数都是被自动调用的;而此时如果再抛出另一个异常,那么将会导致程序立即中止。
智能指针的优点在于:我们可以很方便的管理动态分配的内存(不再需要在析构函数中释放对象),同时,也避免了抛出异常而导致的资源泄漏。

20.1.2 holder
智能指针会在下面两种情况下释放所指向的对象:本身被释放,或者把另一个指针赋值给它。下面我们模拟实现一个智能指针:

// pointers/holder.hpp

template<typename T>
class Holder
{
    private:
        T* ptr;         // 引用它所持有的对象(前提是该对象存在)
    public:
        // 缺省构造函数:让该holder引用一个空对象
        Holder() : ptr(0) { }

        // 针对指针的构造函数:让该holder引用该指针所指向的对象
        // 这里使用explicit,禁止隐式转型(也即禁止了使用赋值语法来初始化Holder对象,如“holderObj = originObj”形式的赋值语法)
        // 但依然可以通过对象构造的形式来给对象初始化,如"Holder holderObj(originObj)",这里是显式转型。 
        explicit Holder (T* p) : ptr(p) {}

        // 析构函数:释放所引用的对象(前提是该对象存在)
        ~Holder() {
            delete ptr;
        }

        // 针对新指针的赋值运算符
        Holder<T>& operator= (T* p){
            delete ptr;
            ptr = p;
            return *this;
        }

        // 指针运算符
        T& operator* () const {
            return *ptr;
        }

        T* operator-> () const {
            return ptr;
        }

        // 获取所引用的对象(前提是该对象存在)
        T* get() const {
            return ptr;
        }

        // 释放对所引用对象的所有权
        void release() {
            ptr = 0;
        }
    
        // 与另一个holder交换所有权
        void exchange_with(Holder<T>& h) {
            swap(ptr, h.ptr);
        }

        // 与其他的指针交换所有权
        void exchange_twith(T*& p) {       // 参数是什么语法?传入指针p的引用?
            swap(ptr, p);
        }
        
    private:
        // 不想外提供拷贝构造函数和拷贝赋值运算符
        // 不允许一个Holder对象A赋值给另一个Holder对象B.
        Holder(Holder<T> const&); 
        Holder<T>& operator= (Holder<T> const&);
};

从语义上讲,该holder独占ptr所引用对象的所有权。而且,这个对象一定要用new操作来创建,因为在销毁holder所拥有对象的时候,需要用到delete。接下来,release()成员函数释放holder对其持有对象的所有权。另外,上面的普通赋值运算符也设计得比较巧妙,它会销毁和释放任何被拥有的对象,因为另一个对象会替代原先的对象被holder所拥有,而且赋值运算符也不会返回原先对象的一个holder或指针(而是返回新对象的一个holder)。最后,我们添加了两个exchange_with()成员函数,从而可以在不销毁原有对象的前提下,方便地替换该holder所拥有的对象。
所以,我们可以如下使用上面的Holder创建两个对象:

void do_two_things()
{
    Holder<Something> first(new Something);
    firsh->perform();

    Holder<Something> second(new Something);
    second->perform();
}

20.1.3 作为成员的holder
我们也可以在类中使用holder来避免资源泄漏。要注意的是,只有那些完成构造之后的对象,它的析构函数才会被调用。因此,如果在构造函数内部产生异常,那么只有那些构造函数已正常执行完毕的成员对象,它的析构函数才会被调用。

// pointers/refmem2.hpp

#include "holder.hpp"

class RefMembers
{
    private:
        Holder<MemType> ptr1;       // 所引用的成员
        Holder<MemType> ptr2;
    
    public:
        // 缺省构造函数
        // - 不可能出现资源泄漏
        RefMembers() : ptr1(new MemType), ptr2(new MemType) { }
    
        // 拷贝构造函数
        // - 不可能出现资源泄漏
        RefMembers (RefMembers const& x) : ptr1(new MemType(*x.ptr1)), ptr2(new MemType(*x.ptr2)) { }

        // 赋值运算符
        const RefMembers& operator= (RefMembers const& x){
            *ptr1 = *x.ptr1;
            *ptr2 = *x.ptr2;
            return *this;
        }

        // 不需要析构函数
        // (缺省的析构函数将会让ptr1和ptr2删除它们所引用的对象)
        ...
};

要注意的是,我们在这里可以省略用户定义的析构函数,但一定要编写拷贝构造函数和赋值运算符

20.1.4 资源获取于初始化

Holder所用到的基本思想是一种称为“资源获取去初始化”或RAII的模式(RAII在博文xxxxx有相关讲解,可供参考)。

20.1.5 hodler局限
包括 20.1.6 和 20.1.7两小节,介绍了holder在参数传递,返回返回值处理时的不足之处,以及复制holder、跨函数调用来复制holder所会产生的问题(这部分内容在博文xxxx中有相关讲解,可供参考)。并引出下一节trule的内容。

20.1.8 trule
为了解决上一小节留下的问题,我们引进了一个专门用于传递holder的辅助类模板,并把它称为trule。在语言中,它是一个术语,来自于transfer capsule的缩写。下面是其定义:

// pointers/trule.hpp

#ifndef TRULE_HPP
#define TRULE_HPP

template <typename T>
class Holder;

template <typename T>
class Trule
{
    private:
        T* ptr;        // trule所引用的对象(如果有的话)
    
    public:
        // 构造函数,确保trule只能作为返回类型,用于将holder从被调用函数传递给调用函数
        // 显式构造函数(会自动屏蔽默认无参构造函数),只能通过Holder构造Trule对象
        Trule (Holder<T>& h){
            ptr = h.get();
            h.release();
        }

        // 拷贝构造函数
        // 这里,trule通常是作为那些想传递holders的函数的返回类型,也就是
      说trule对象总是作为临时对象(rvalues,右值)出现;因此它们的类型也就只能是
      常引用(reference-to-const)类型。
Trule (Trule<T> const& t){ ptr = t.ptr; // 由于Trule不能作为一份拷贝,也不能含有一份拷贝,如果我们希望实现类似于拷贝操作,
        就必须移除原trule的所有权。我们是通过将被封装指针置为空来实现这种移除操作的。而最后
        这个置空操作显然只能针对non-const对象,所以才有了这种把const强制转型为non-const的做法。
// 另外,由于原来的对象实际上并没有被定义为常类型,所以即使这样做有些别扭,但在这种情况下这种转型却能合法地实现。 // 因此,对于最后需要把一个holder转换为trule,并且将其返回的函数,如果要声明这类函数的
          返回类型,我们就必须把它声明为trule<T>类型,而绝对不能声明为trule<T> const,
          这点需特别注意。如下面例子中的函数load_something()
const_cast<Trule<T>&>(t).ptr = 0; // 置空操作 } // 析构函数 ~Trule() { delete ptr; } private: // 对于trule的用法,除了作为传递holder对象的返回类型,我们要防止把它用于其他地方。
      于是,一个接收non-const引用对象的拷贝构造函数和一个类似的拷贝赋值运算符,都被声
      明为私用函数,防止外界直接调用。通过禁止将trule作为左值的方法,因为左值允许取
        址和赋值操作,这种特性容易导致其用于其他地方而没有报错。
Trule(Trule<T>&); Trule<T>& operator= (Trule<T>&); // 禁止拷贝赋值 friend class Holder<T>; }; #endif // TRULE_HPP

还有一点需要注意的是,上面的代码并不完全是把一个holder完全转换为一个trule:如果是这样的话,holder就必须是一个可修改的左值。这也是我们为什么要使用一个单独的类型来实现trule,而不是将它的功能合并到holder类模板中的原因。

最后,对于上面实现的trule,只有被holder模板所辨识并且使用之后,才能算是完整的。如下:

// pointers/holder2.hpp

template <typename T>
class Holder
{
    // 前面已经定义的成员
    ...

    public:
        Holder(Trule<T> const& t){
            ptr = t.ptr;
            const_cast<Trule<T>&>(t).ptr = 0;
        }

        Holder<T>& operator= (Trule<T> const& t) {
            delete ptr;
            ptr = t.ptr;
            const_cast<Trule<T>&>(t).ptr = 0;
            return *this;
        }
};

为了充分演示对holder/trule作了哪些改善,我们可以重写load_something()例子,如下:

// pointers/truletest.cpp

#include "holder2.hpp"
#include "trule.hpp"

class Something
{
};

void read_something(Something* x)
{
}

// 返回类型为Trule<Something>,通过将Holder<Something>转换成返回类型(也即,通过trule传递返回值)
Trule<Something> load_something()
{
    Holder<Something> result(new Something);
    read_something(result.get());
    return result;
}

int main()
{
    // 接收load_something函数返回的Trule<Something>类型的值,并通过Holder内部接收Trule对象的构造函数初始化Holder对象ptr
    Holder<Something> ptr(load_something());
    ....
}

20.2 引用计数
设计一个引用计数的智能指针,基本思想是:对于每个被指向的对象,都保存一个计数,用于代表指向该对象的指针的个数,当计数值减少到0时,就删除此对象。
我们首先面对的问题是:计算器在什么地方?这里可以有两种方式,一种是把计算器放在对象中,但如果对象早期已经设计好,则无法再把计算器放入对象;另一种也是通常会使用的就是使用专用的(内存)分配器。
我们面对的第二个问题是:对象的析构和释放。我们有可能会需要使用非标准方式(比如C的free(),或者delete[]运算符释放对象数组)来释放对象,故而,我们还需要指定一种单独的对象(释放)policy。
对于大多数用CountingPtr计数的对象,我们可以使用下面这个简单的对象policy:

// pointers/stdobjpolicy.hpp
class StandardObjectPolicy
{
    public:
        template<typename T> void dispose(T* object){
            delete object;
        }
};

// pointers/stdarraypolicy.hpp
class StandardArrayPolicy
{
    public:
        template<typename T> void dispose(T* array){
            delete[] array;
        }
};

在考虑了上面两个问题之后,我们现在开始定义我们的CountingPtr模板:

// pointers/countingptr.hpp

template <typename T,
                typename CounterPolicy = SimpleReferenceCount,            // 计算器的policy
                typename ObjectPolicy = StandardObjectPolicy>           // 对象(释放)policy
class CountingPrt : private CounterPolicy, private ObjectPolicy
{
    private:
        // typedef 两个简单的别名
        typedef CountPolicy CP;
        typedef ObjectPolicy OP;

        T* object_pointer_to;         // 所引用的对象
        // 如果没有引用任何对象,则为NULL
    
    public:
        // 缺省构造函数(没有显式初始化,即没有加上explicit关键字)
        CountingPtr(){
            this->object_pointed_to = NULL;
        }

        // 一个针对转型的构造函数(转型自一个内建的指针)
        explicit CountingPtr(T* p) {
            this->init(p);             // 使用普通指针初始化
        }

        // 拷贝构造函数
        CountingPtr(CountingPtr<T, CP, OP> const& cp) 
            : CP((CP const&)cp),             // 拷贝policy
            OP((OP const&)cp){
                this->attach(cp);             // 拷贝指针,并增加计数值
        }
        
        // 析构函数
        ~CountingPtr(){
            this->detach();             // 减少计数值,如果计数值为0,则释放该计数器
        }

        // 针对内建指针的赋值运算符
        CountingPtr<T, CP, OP>& operator= (T* p){
            // 计数指针不能指向*p
            assert(p != this->object_pointed_to);
            this->detach();               // 减少计数值,如果计数值为0,则释放该计数器

            this->init(p);              // 用一个普通指针进行初始化
            return *this;
        }

        // 拷贝赋值运算符(要考虑自己给自己赋值)
        CountingPtr<T, CP, OP>&
        operator= (CountingPtr<T, CP, OP> const& cp){
            if(this->object_pointed_to != cp.object_pointed_to){
                this->detach();               // 减少计数值,如果计数值为0,则释放该计数器
                
                CP::operator=((CP const&)cp);       // 对policy进行赋值
                OP::operator=((OP const&)op);
                this->attach(cp);           // 拷贝指针并增加计数值
            }
            return *this;
        }

        // 使之成为智能指针的运算符
        T* operator->() const {
            return this->object_pointed_to;
        }

        T& operator* () const {
            return *this->object_pointed_to;
        }

        // 以后在这里将可能会增加一些其他的接口
        ....

    private:
        // 辅助函数
        // - 用普通指针进行初始化(前提是普通指针存在)
        void init(T* p){
            if (p != NULL)
            {
                CounterPolicy::init(p);
            }
            this->object_pointed_to = p;
        }

        // - 拷贝指针并且增加计数值(前提是指针存在)
        void attach(CountingPtr<T, CP, OP> const& cp){
            this->object_pointed_to = cp.object_pointed_to;
            if (cp.object_pointed_to != NULL)
            {
                CounterPolicy::increment(cp.object_pointed_to);
            }
        }

        // - 减少计数值(如果计数值为0, 则释放计数器)
        void detach(){
            if (this->object_pointed_to != NULL)
            {
                CounterPolicy::decrement(this->object_pointed_to);
                if (CounterPolicy::is_zero(this->object_pointed_to))
                {
                    // 如果有必要的话,释放计数器
                    CounterPolicy::dispose(this->object_pointed_to);
                    // 使用object policy来释放所指向的对象
                    ObjectPolicy::dispose(this->object_pointed_to);
                }
            }
        }
};

上面代码需要注意:
(1)在拷贝赋值操作中,要判断是否为自赋值;
(2)由于空指针并没有一个可关联的计数器,所以在减少计数值之前,必须先显式地检查空指针的情况;
(3)在前面的代码中,我们使用继承来包含两种policy。这样做确保了在policy类为空的情况下,并不需要占用存储空间(前提是我们的编译器实现了空基类优化);

20.2.5 一个简单的非侵入式计数器
从总体看来,我们已经完成了CountingPtr的设计,下面我们需要为计数policy编写代码。
于是,我们先来看一个针对计数器的policy,它并不把计数器存储于所指向对象的内部,也就是说,它是一种非侵入式的计数器policy(或者称为非插入式的计数器policy)。对于计数器而言,最主要的问题是如何分配存储空间。事实上,同一个计数器需要被多个CountingPtr所共享;因此,它的生命周期必须持续到最后一个智能指针被释放之后。通常而言,我们会使用一种特殊的分配器来完成这种任务,这种分配器专门用于分配大小固定的小对象。

// pointers/simplerefcount.hpp

#include <stddef.h> // 用于size_t的定义
#include "allocator.hpp"

class SimpleReferenceCount
{
    private:
        size_t* counter;        // 已经分配的计数器
    public:
        SimpleReferenceCount(){
            counter = NULL;
        }

        // 缺省的拷贝构造函数和拷贝赋值运算符都是允许的
        // 因为它们只是拷贝这个共享的计数器
    public:
        // 分配计数器,并把它的值初始为1
        template <typename T> void init(T*) {
            Counter = alloc_counter();
            *counter = 1;
        }

        // 释放该计数器
        template <typename T> void dispose(T*) {
            dealloc_counter(counter);    
        }

        // 计数值加1
        template<typename T> void increment(T*){
            ++*counter;
        }

        // 计数值减1
        template<typename T> void decrement(T*){
            --*counter;
        }

        // 检查计数值是否为0
        template<typename T> bool is_zero(T*){
            return *counter == 0;
        }
};

20.2.6 一个简单的侵入式计数器模板
侵入式(或插入式)计数器policy就是将计数器放到被管理对象本身的类型中(或者可能存放到由被管理对象所控制的存储空间中)。显然,这种policy通常需要在设计对象类型的时候就加以考虑;因此这种方案很可能会专用于被管理对象的类型。

// pointers/memberrefcount.hpp

template<typename ObjectT,        // 包含计数器的类型
            typename CountT,                // 计数器的类型
            CountT Object::*CountP>        // 计数器的位置,需要在设计ObjectT对象的时候就考虑到计数器
class MemberReferenceCount
{
    public:
        // 缺省构造函数和析构函数都是允许的

        // 让计数器的值初始化为1
        void init(ObjectT* object){
            object->*CountP = 1;
        }

        // 对于计数器的释放,并不需要显式执行任何操作
        void dispose(ObjectT*){ }
        
        // 计数器加1
        void increment(ObjectT* object){
            ++object->*CountP;
        }

        // 计数器减1
        void increment(ObjectT* object){
            --object->*CountP;
        }

        // 检查计数值是否为0
        template<typename T> bool is_zero(ObjectT* object){
            return object->*CounP == 0;
        }
};

如果使用这种policy的话,那么在类的实现中,就可以很快地写出类的引用计数指针类型。其中类的设计框架大概如下:

class ManagedType
{
    private:
        size_t ref_count;
    public:
        typedef CountingPtr<ManagedType,
                                    MemberReferenceCount
                                    <ManagedType,        // 包含计数器的对象类型
                                        size_t,                // 计数器类型
                                        &ManagedType::ref_count> >
                    Ptr;
        ....
};

有了上面这个定义之后,我们就可以使用ManageeType::Ptr,方面地引用“那些用于访问ManagedType对象的”引用计数指针类型(在此为智能指针类型CountingPtr)。

书中还介绍了关于智能指针的其他一些功能实现,包括常数性相关内容、隐式转型,以及比较等等,有兴趣自行查阅学习,这里不介绍。

以上是关于C++ template —— 智能指针的主要内容,如果未能解决你的问题,请参考以下文章

C++ 自定义智能指针

C++ 智能指针和指针到指针输出 API。模板化的“包装器”

模拟实现c++标准库和boost库中的智能指针

更新:C++ 指针片段

以下代码片段 C++ 的说明

C++ template —— 类型区分