清风竹林

ぷ雪飘绛梅映残红
   ぷ花舞霜飞映苍松
     ----- Do more,suffer less

C++通用删除器设计

C++通用删除器设计

版本:0.1

最后修改:2010-11-15

撰写:李现民


概述

很久以前,我写过一篇短文讨论如何在C++项目中避免使用delete的设想,基本方法是使用域(scope)对象或std::auto_ptr代替。尽管当时已经讨论在所有可能的情况,但后面在实际项目实施中发现效果并不好。原因是方面的,比如在使用std::auto_ptr时会存在以下不得因素:

  1. 可能的额外开销外(其实很小);

  2. 你需要时刻小心对象所有权的问题。尽管可能只需要稍微注意一下就可以了,但似乎没有任何程序员喜欢过提心吊胆的日子;

  3. 你不能在容器(比如std::vector)中存储std::auto_ptr对象;


基于以上原因,类似于Text* pText = new Text;这种直接在堆上申请内存的方式还是在代码得到了大量应用。而接下来就是如何安全、有效的回收这些内存的问题,这也正是本文所讨论的话题。

回收单个堆对象

// delete a object pointer and reset it
template<class T> void delete_null(T*& p)
{
// check if T is incomplete type, if it is, the compiler will report an error
typedef char type_must_be_complete[ sizeof(T)? 1: -1 ];
(void) sizeof(type_must_be_complete);
// delete the pointer and reset it
delete p;
p =NULL;
}


这是一个模板函数,它主要有三个作用:

第一个作用是检查被删除对象的类型完整性。这通常无法引起人们的重视,但在某些情况下可能会导致未定义行为,比如以下代码:

Text* pText = new Text;
void* pData = pText;
delete pData;

Text*类对象pText 被转换成了拥有void*对象pData,并对pData 调用了delete 删除操作。在这种情况下编译器的行为是未知的,但至少有一点:由于编译器无法推导pData 的原始类型,因此无法调用对象的析构函数。

// check if T is incomplete type, if it is, the compiler will report an error
typedef char type_must_be_complete[ sizeof(T)? 1: -1 ];
(void) sizeof(type_must_be_complete);

这两句代码可以检查被删除对象的类型完整性。其效果发生在编译期,如果对类型不完整的对象调用delete_null 删除操作,将引起编译错误。它没有运行期开销,因此使用delete_null 带来的安全性实际上免费的。

更加详细的解释可以参考boost库中的checked_delete.hpp

delete_null 的第二个作用是回收堆对象,这没有什么可说的。

delete_null 的第三个作用是将对象指针设置为NULL这主要是为了应对指针有效性检查,属于常规手段。

另外,注意到delete_null 被设计为一个模板函数,在发布版本(Release)中,它将以内联代码(inline)的形式存在,因此不会有运用期函数调用开销


回收容器中的堆对象


// delete container (std::vector, std::list) items and reset them to NULL
template< typename InputIterator > void delete_null(InputIterator first, InputIterator last)
{
while(last != first)
{
delete_null(*first);
++first;
}
}

// delete functor, used for iterative delete
struct deleter
{
template< typename T > void operator()(T*& p)
{
delete_null(p);
}
};


这段代码分为两部分:一个同样叫delete_null 的模板函数与一个名为deleter的仿函数。

先来看第一部分,它同样叫delete_null,与前面介绍的那个版本所不同的是它接受一对迭代器作用输入条件,其作用是回收[first, last) 范围内所有堆对象。与std::for_each等很多STL标准算法类似,该函数可以同时应用于普通数组或存储单值的标准容器(包括std::vector, std::list, std::set等,不包含std::map)。


第二部分比较有意思:它是一个仿函数。它可以在一定程度上代替delete_null(first, last)以下代码展示了分别使用这两种方式回收容器中的堆对象的方法

typedef std::vector<Text*> TextPack;
TextPack uTexts1, uTexts2;
const int datasize = 100;
for (int i= 0; i< datasize; ++i)
{
uTexts1.push_back(new Text);
uTexts2.push_back(new Text);
}

//
使用delete_null
delete_null(uTexts1.begin(), uTexts1.end());
// 使用deleter
std::for_each(uTexts2.begin(), uTexts2.end(), deleter());


可以看到前者稍微简洁一些(包括最终的汇编代码),那么问题来了:为什么还需要代码量更大一些的deleter 仿函数?

理由是:并不是所有存储堆对象的集合都是直接存储对象指针的。比如可以将指针存储在std::map中“值”部分,甚至有些自定义集合只提供了遍历函数(类似于std::for_each),但并不公开迭代器接口。在这些情况下,我们就可以使用deleter 仿函数进行堆对象回收。



posted on 2010-11-15 14:46 李现民 阅读(3225) 评论(16)  编辑 收藏 引用 所属分类: design

评论

# re: C++通用删除器设计 2010-11-15 22:11 ,。。

自作聪明,无语。。。  回复  更多评论   

# re: C++通用删除器设计[未登录] 2010-11-15 23:09 Jeff

博主,有问题请教:
1、为什么不直接使用boost中提供的smart ptr呢?
2、你的实现是否是“异常安全”呢?  回复  更多评论   

# re: C++通用删除器设计 2010-11-16 09:18 李现民

@Jeff
唉, 这批评还真犀利呀!
1. 不直接使用boost中的智能指针最直接的原因是boost不稳定,而且并不是boost中的所有特性都对项目有帮助的。你可能会问为什么我不自己实现一个,一个类似于shared_ptr的东西,除了那一点性能损失外,真正的原因是我还没有发现让我不得不用的理由。而且,项目中有很多地方是不能直接调用析构函数的,而是需要单独写一个Destroy的函数,因为时机。
2. 关于异常安全,我想,如果析构函数本身是异常安全的话,那么我的代码本身应该算异常安全了吧?反之,如果析构函数本身不安全的话,那无论采用什么方法析构都是有问题的。

如果我的想法有什么问题的话,请不吝指正,非常感谢。
  回复  更多评论   

# re: C++通用删除器设计 2010-11-16 10:52 空明流转

@李现民

扯淡,有什么不稳定的。

shared_ptr是一个完全OK的选择。至于性能损失,我还真没遇到过。一个atomic的add,能有多少性能损耗?能和你的res alloc和free比么?

还有,RAII本身就是一种Idiom。你觉得不适用,只是因为你对RAII这个Idiom本身不熟悉而已。  回复  更多评论   

# re: C++通用删除器设计 2010-11-16 11:16 李现民

@空明流转
我所指的“不稳定”是指boost一直在开发,里面有长期积累下来的库,也有新加入的库,只是加入到项目中,新库在未证明其稳定性之前也有可能被使用,而这可能导致一些问题。

另外, 你说得对,相比于资源的分配与回收,shared_ptr的开销是可以忽略的,但我们的系统中现在还没有这样的机制,而且我说过了,真正的原因是我尚未发现让我不得不用它的理由。

最后, 我其实是很赞同使用RAII的,实际上,我们已经在陆续使用它了。

感谢回复!  回复  更多评论   

# re: C++通用删除器设计 2010-11-16 13:40 陈梓瀚(vczh)

@李现民
那你就自己写一个shared_ptr吧,最多发现不稳定的时候你还可以改嘛,改着改着就稳定了。  回复  更多评论   

# re: C++通用删除器设计 2010-11-16 13:41 陈梓瀚(vczh)

@李现民
不得不用的理由有很多的,譬如说我最推崇的一条就是:“不使用它会浪费人类的时间”  回复  更多评论   

# re: C++通用删除器设计[未登录] 2010-11-16 22:37 Jeff

@李现民
恐怕你误解我的意思了。我并没有批评的意思,只是想搞清楚一些问题。
关于boost的“不稳定”,在实际项目的运用时,只选择使用某一个版本的boost使用是就好了。对一个项目而言,boost的版本是稳定的。这样能避免一些配置管理的问题。
另外,使用boost的一个原因就是避免重复造轮子:)当然,一定要选择boost中质量高、稳定性好的功能才能在项目中使用。“取其精华、去其糟粕”,呵呵……
  回复  更多评论   

# re: C++通用删除器设计 2010-11-17 10:36 李现民

@陈梓瀚(vczh)
看了这么多回复, 如果我没想错的话,是不是大家都推崇使用shared_ptr替代原始对象指针?如果多数人都这认为的话,那肯定是有道理的。如你所言,看来有时间我得去仔细研究一下shared_ptr了。

我现在能想到的问题是,假如我自己写了一个shared_ptr加入到了旧的项目中,那么所有其它使用该对象的地方(包括参数)是否都必须带着shared_ptr<T>的声明呢?

感谢回复,不忙的时候研究一下。  回复  更多评论   

# re: C++通用删除器设计 2010-11-17 10:40 李现民

@Jeff
没有, 非常欢迎你的评论。大家的回复让我想了很多,这些都是非常意外而重要的收获。

其实, 最直接的问题是:我没有权利在项目中引入一个像boost 这样大的库,因此这不但限制了应用,也限制了我的眼界。  回复  更多评论   

# re: C++通用删除器设计 2010-11-17 17:34 冬瓜

这个和boost::shared_ptr没关系吧~~~~,
我看了一下,它的做用是对删除void *的指针,会产生一个编译器的错误。以避免一些人员将T *指针转换成void *,再删除,而造成没有执行析构函数的问题。不过,这种情况非常少。

shared_ptr的作用是0引用时删除。  回复  更多评论   

# re: C++通用删除器设计 2010-11-17 17:58 李现民

@冬瓜
是这样的, 大家觉得如果直接用shared_ptr的话就没有必要写一个delete_null函数了
  回复  更多评论   

# re: C++通用删除器设计 2010-11-17 18:24 星绽紫辉

没有绝对的,指针引用计数用在比较复杂的环境(大块的资源使用、队列资源)中非常有用(比如内存池的分配和释放),你可以写自己的AddRef和DelRef函数来管理资源, 但是通常简单类就没必要了,如果连简单的实现类、模式类、UI类都这么搞,你会晕的。。。  回复  更多评论   

# re: C++通用删除器设计 2010-11-17 19:42 李现民

@星绽紫辉
呵呵, 受教了  回复  更多评论   

# re: C++通用删除器设计 2010-11-18 10:09 冬瓜

@李现民
你的delete_null和shared_ptr是两个完全不同的东西。
作用目标都不一样~  回复  更多评论   

# re: C++通用删除器设计 2010-11-18 10:32 陈梓瀚(vczh)

@李现民
shared_ptr只是一个例子哈,当然也可以用来删除东西,因为它支持的转换比起C++的指针转换还是更严格的。

我自己也实现了一次,在ptr<A>和ptr<B>进行转换用的是dynamic_cast,这样如果一个类型没有虚函数就会报错,因此我不会因为错误转换了指针而导致析构的时候发生问题,就非常安全了。  回复  更多评论   


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理