C++箴言:谨慎考虑资源管理类的拷贝行为

2016-02-19 12:17 11 1 收藏

只要你有一台电脑或者手机,都能关注图老师为大家精心推荐的C++箴言:谨慎考虑资源管理类的拷贝行为,手机电脑控们准备好了吗?一起看过来吧!

【 tulaoshi.com - 编程语言 】

在上一篇文章中介绍了作为资源管理类支柱的 Resource Acquisition Is Initialization (RAII) 原则,并描述了 auto_ptr 和 tr1::shared_ptr 在基于堆的资源上运用这一原则的表现。并非所有的资源都是基于堆的,然而,对于这样的资源,像 auto_ptr 和 tr1::shared_ptr 这样的智能指针通常就不像 resource handlers(资源管理者)那样合适。在这种情况下,有时,你可能要根据你自己的需要去创建你自己的资源管理类。

  例如,假设你使用 C API 提供的 lock 和 unlock 函数去操纵 Mutex 类型的互斥体对象:

void lock(Mutex *pm); // lock mutex pointed to by pm

void unlock(Mutex *pm); // unlock the mutex

  为了确保你从不会忘记解锁一个被你加了锁的 Mutex,你希望创建一个类来管理锁。RAII 原则规定了这样一个类的基本结构,通过构造函数获取资源并通过析构函数释放它:

class Lock {
  public:
   explicit Lock(Mutex *pm)
   : mutexPtr(pm)
   { lock(mutexPtr); } // acquire resource
 
   ~Lock() { unlock(mutexPtr); } // release resource

  private:
   Mutex *mutexPtr;
};

  客户按照 RAII 风格的惯例来使用 Lock:

Mutex m; // define the mutex you need to use
...
{ // create block to define critical section
  Lock ml(&m); // lock the mutex
  ... // perform critical section operations

} // automatically unlock mutex at end
// of block

  这没什么问题,但是如果一个 Lock 对象被拷贝应该发生什么?

Lock ml1(&m); // lock m

Lock ml2(ml1); // copy ml1 to ml2-what should
// happen here?

  这是一个更一般问题的特定实例,每一个 RAII 类的作者都要面临这样的问题:当一个 RAII 对象被拷贝的时候应该发生什么?大多数情况下,你可以从下面各种可能性中挑选一个:

  禁止拷贝。在很多情况下,允许 RAII 被拷贝是没有意义的。这对于像 Lock 这样类很可能是正确的,因为同步的基本要素的“副本”很少有什么意义。当拷贝对一个 RAII 类没有什么意义的时候,你应该禁止它。Item 6 解释了如何做到这一点。声明拷贝操作为私有。对于 Lock,看起来也许像这样:

class Lock: private Uncopyable { // prohibit copying - see
public: // Item 6
... // as before
};

  对底层的资源引用计数。有时人们需要的是保持一个资源直到最后一个使用它的对象被销毁。在这种情况下,拷贝一个 RAII 对象应该增加引用这一资源的对象的数目。这也就是使用 tr1::shared_ptr 时“拷贝”的含意。

  通常,RAII 类只需要包含一个 tr1::shared_ptr 数据成员就能够实现引用计数的拷贝行为。例如,如果 Lock 要使用引用计数,他可能要将 mutexPtr 的类型从 Mutex* 改变为 tr1::shared_ptrMutex。不幸的是,tr1::shared_ptr 的缺省行为是当它所指向的东西的引用计数变为 0 的时候将它删除,但这不是我们要的。当我们使用 Mutex 完毕后,我们想要将它解锁,而不是将它删除。

  幸运的是,tr1::shared_ptr 允许一个 "deleter" 规范——当引用计数变为 0 时调用的一个函数或者函数对象。(这一功能是 auto_ptr 所没有的,auto_ptr 总是删除它的指针。)deleter 是 tr1::shared_ptr 的构造函数的可选的第二个参数,所以,代码看起来就像这样:

class Lock {
public:
  explicit Lock(Mutex *pm) // init shared_ptr with the Mutex
  : mutexPtr(pm, unlock) // to point to and the unlock func
  { // as the deleter

   lock(mutexPtr.get()); // see Item 15 for info on "get"
  }
private:
  std::tr1::shared_ptrMutex mutexPtr; // use shared_ptr
}; // instead of raw pointer

  在这个例子中,注意 Lock 类是如何不再声明一个析构函数的。那是因为它不再需要。Item 5 解释了一个类的析构函数(无论它是编译器生成还是用户定义)会自动调用这个类的非静态(non-static)数据成员的析构函数。在本例中,就是 mutexPtr。但是,当互斥体的引用计数变为 0 时,mutexPtr 的析构函数会自动调用的是 tr1::shared_ptr 的 deleter ——在此就是 unlock。(看过这个类的源代码的人多半意识到需要增加一条注释表明你并非忘记了析构,而只是依赖编译器生成的缺省行为。)

  拷贝底层的资源。有时就像你所希望的你可以拥有一个资源的多个副本,唯一的前提是你需要一个资源管理类确保当你使用完它之后,每一副本都会被释放。在这种情况下,拷贝一个资源管理对象也要同时拷贝被它隐藏的资源。也就是说,拷贝一个资源管理类需要完成一次“深层拷贝”。

  某些标准 string 类型的实现是由堆内存的指针组成,堆内存中存储着组成那个 string 的字符。这样的字符串对象包含指向堆内存的指针。当一个 string 对象被拷贝,这个副本应该由那个指针和它所指向的内存组成。这样的 strings 表现为深层拷贝。

  传递底层资源的所有权。在某些特殊场合,你可能希望确保只有一个 RAII 对象引用一个未加工的资源,而当这个 RAII 对象被拷贝的时候,资源的所有权从被拷贝的对象传递到拷贝对象。就像上一篇文章所说明的,这就是使用 auto_ptr 时“拷贝”的含意。

  拷贝函数(copying functions)(拷贝构造函数和拷贝赋值运算符)可能是由编译器生成的,所以除非编译器生成的版本所做的事正是你所要的,你应该自己编写它们。在某些情况下,你也要支持这些函数的泛型化版本。

  Things to Remember

  ·拷贝一个 RAII 对象必须拷贝它所管理的资源,所以资源的拷贝行为决定了 RAII 对象的拷贝行为。

  ·普通的 RAII 类的拷贝行为不接受拷贝和进行引用计数,但是其它行为是有可能的。

来源:http://www.tulaoshi.com/n/20160219/1600489.html

延伸阅读
这是一个所有程序员都应该了解的小型的 C++ 词汇表。下面的条目都足够重要,值得我们对它们的含义务必取得完全一致。 声明(declaration)告诉编译器关于某物的名字和类型,但它省略了某些细节。以下这些都是声明: extern int x; // object declaration std::size_t numDigits(int number); // function declaration class Widget; /...
关于 const 的一件美妙的事情是它允许你指定一种语义上的约束:一个特定的对象不应该被修改。而编译器将执行这一约束。它允许你通知编译器和其他程序员,某个值应该保持不变。如果确实如此,你就应该明确地表示出来,因为这样一来,你就可以谋取编译器的帮助,确定这个值不会被改变。 关键字 const 非常多才多艺。在类的外部,你可以将它用...
C++ 的规则设计为保证不会发生类型错误。在理论上,假如你的程序想顺利地通过编译,你就不应该试图对任何对象做任何不安全的或无意义的操作。这是一个非常有价值的保证,你不应该轻易地放弃它。 !-- frame contents -- !-- /frame contents -- 不幸的是,强制转型破坏了类型系统。它会引起各种各样的麻烦,其中一些...
在面向对象的程序设计中,有经常接触类、对象等专业名词;到底什么是类、什么是对象呢?在程序又是怎样运用呢?类是面向对象程序设计的核心,它实际是一种新的数据类型,也是实现抽象类型的工具,因为类是通过抽象数据类型的方法来实现的一种数据类型。类是对某一类对象的抽象;而对象是某一种类的实例,因此,类和对象是密切相关的。没有...
假设你正在一个包含矩形的应用程序上工作。每一个矩形都可以用它的左上角和右下角表示出来。为了将一个 Rectangle 对象保持在较小状态,你可能决定那些点的定义的域不应该包含在 Rectangle 本身之中,更合适的做法是放在一个由 Rectangle 指向的辅助的结构体中: class Point { // class for representing points public: Point(...

经验教程

510

收藏

31
微博分享 QQ分享 QQ空间 手机页面 收藏网站 回到头部