天天看点

Effective C++条款14:资源管理之(在资源管理类中小心拷贝行为)一、“不希望拷贝”演示案例二、当RAII对象被拷贝时的做法四、引用计数法五、总结

前言

  • 在前面的文章中我们介绍了智能指针类来管理资源,但是对于某些资源,它们不是动态分配的(不是堆上的),因此智能指针就是不适合这种资源的管理了,因此你可能会设计自己的资源管理类

一、“不希望拷贝”演示案例

  • 假设我们使用C的API函数处理类型为Mutex的互斥器对象,其中有两个函数如下:
void lock(Mutex* pm);   //锁定pm所指的互斥器
void unlock(Mutex* pm); //将互斥器解锁
           
  • 现在我们设计一个类,来管理Mutex对象,并且使用了上一篇文章的RAII规则,即“在构造期间获得资源,在析构期间释放资源”,原型如下:
class Lock
{
public:
    //获得资源
    explicit Lock(Mutex* pm) :mutexPtr(pm) {
        lock(mutexPtr);
    }

    //释放资源
    ~Lock() { unlock(mutexPtr); }
private:
    Mutex *mutexPtr;
};
           
  • 如果用下面的函数调用Lock对象是正确的
Mutex m;

void f()
{
    //管理Mutex对象
    Lock ml(&m);
    //...

}//函数结束后,自动调用ml的析构函数解锁
           
  • 但是如果发生下面的拷贝行为,那么程序就会产生意想不到的后果:
    • 因为m2拷贝时会调用lock函数对Mutex对象进行加锁,但是m对象此时处于使用状态,因为其会阻塞等待,等待m1释放互斥量,但是m1此时也无法向下执行,因此这个程序就产生死锁了
    • 所以对于我们设计的Lock这个类,我们不希望其有拷贝行为
Mutex m;

void f()
{
    Lock m1(&m);//管理Mutex对象
    //...
    
    Lock m2(m1); //拷贝

}//函数结束后,自动调用ml的析构函数解锁
           

二、当RAII对象被拷贝时的做法

  • 当RAII对象被复制时,我们通常有以下两种做法:
    • 禁止复制:许多时候,RAII对象被复制时不合理的,就像上面我们那个Mutex一样。因此我们可以根据条款6的做法,将拷贝操作声明为private或者让其继承于Uncopyable这样的基类
    • 对底层资源做出“引用计数法”:我们可以参考shared_ptr类那样的机制,对资源设计“引用计数法”

四、引用计数法

  • 如果我们希望对管理的资源进行引用计数法,那么可以重新设计上面的Lock类:
    • 使用shared_ptr管理Mutex对象
    • 当引用计数为0时,我们希望做的是解锁资源而不是删除资源,但是我们的shared_ptr提供一个“删除器”,删除器可以是一个函数或函数对象,因此下面我们为mutexPtr提供了unlokc函数,当引用计数为0时就会调用unlock函数,将Mutex对象解锁
    • 不再设计析构函数:因为默认的析构函数会调用其非静态成员的析构函数,因此默认的析构函数会在互斥器的引用计数为0时自动调用shared_ptr的删除器(此处为unlock函数)
class Lock
{
public:
    explicit Lock(Mutex* pm) 
        :mutexPtr(pm,unlock) //提供unlock函数为删除器
    {
        lock(mutexPtr.get()); //条款15会介绍get函数
    }
private:
    std::shared_ptr<Mutex> mutexPtr;
};
           

五、总结

  • 赋值RAII对象必须一并复制它所管理的资源,所以资源的拷贝行为决定RAII对象的拷贝行为
  • 普遍而常见的RAII类拷贝行为是:抑制拷贝、试行引用计数法。不过其他行为也都可能被实现

继续阅读