C++通用刪除器設(shè)計(jì)
版本:0.1
最后修改:2010-11-15
撰寫:李現(xiàn)民
概述
很久以前,我寫過一篇短文討論如何在C++項(xiàng)目中避免使用delete的設(shè)想,基本方法是使用域(scope)對象或std::auto_ptr代替。盡管當(dāng)時已經(jīng)討論在所有可能的情況,但后面在實(shí)際項(xiàng)目實(shí)施中發(fā)現(xiàn)效果并不好。原因是方面的,比如在使用std::auto_ptr時會存在以下不得因素:
可能的額外開銷外(其實(shí)很小);
你需要時刻小心對象所有權(quán)的問題。盡管可能只需要稍微注意一下就可以了,但似乎沒有任何程序員喜歡過提心吊膽的日子;
你不能在容器(比如std::vector)中存儲std::auto_ptr對象;
基于以上原因,類似于Text*
pText
=
new
Text;這種直接在堆上申請內(nèi)存的方式還是在代碼得到了大量應(yīng)用。而接下來就是如何安全、有效的回收這些內(nèi)存的問題,這也正是本文所討論的話題。
回收單個堆對象
//
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;
}
這是一個模板函數(shù),它主要有三個作用:
第一個作用是檢查被刪除對象的類型完整性。這通常無法引起人們的重視,但在某些情況下可能會導(dǎo)致未定義行為,比如以下代碼:
Text*
pText =
new
Text;
void*
pData =
pText;
delete
pData;
Text*類對象pText
被轉(zhuǎn)換成了擁有void*對象pData,并對pData
調(diào)用了delete
刪除操作。在這種情況下編譯器的行為是未知的,但至少有一點(diǎn):由于編譯器無法推導(dǎo)pData
的原始類型,因此無法調(diào)用對象的析構(gòu)函數(shù)。
//
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);
這兩句代碼可以檢查被刪除對象的類型完整性。其效果發(fā)生在編譯期,如果對類型不完整的對象調(diào)用delete_null
刪除操作,將引起編譯錯誤。它沒有運(yùn)行期開銷,因此使用delete_null
帶來的安全性實(shí)際上免費(fèi)的。
更加詳細(xì)的解釋可以參考boost庫中的checked_delete.hpp。
delete_null
的第二個作用是回收堆對象,這沒有什么可說的。
delete_null
的第三個作用是將對象指針設(shè)置為NULL,這主要是為了應(yīng)對指針有效性檢查,屬于常規(guī)手段。
另外,注意到delete_null
被設(shè)計(jì)為一個模板函數(shù),在發(fā)布版本(Release)中,它將以內(nèi)聯(lián)代碼(inline)的形式存在,因此不會有運(yùn)用期函數(shù)調(diào)用開銷。
回收容器中的堆對象
//
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
的模板函數(shù)與一個名為deleter的仿函數(shù)。
先來看第一部分,它同樣叫delete_null,與前面介紹的那個版本所不同的是它接受一對迭代器作用輸入條件,其作用是回收[first,
last)
范圍內(nèi)所有堆對象。與std::for_each等很多STL標(biāo)準(zhǔn)算法類似,該函數(shù)可以同時應(yīng)用于普通數(shù)組或存儲單值的標(biāo)準(zhǔn)容器(包括std::vector,
std::list, std::set等,不包含std::map)。
第二部分比較有意思:它是一個仿函數(shù)。它可以在一定程度上代替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
仿函數(shù)?
理由是:并不是所有存儲堆對象的集合都是直接存儲對象指針的。比如可以將指針存儲在std::map中“值”部分,甚至有些自定義集合只提供了遍歷函數(shù)(類似于std::for_each),但并不公開迭代器接口。在這些情況下,我們就可以使用deleter
仿函數(shù)進(jìn)行堆對象回收。