C++通用刪除器設計
版本:0.1
最后修改:2010-11-15
撰寫:李現民
概述
很久以前,我寫過一篇短文討論如何在C++項目中避免使用delete的設想,基本方法是使用域(scope)對象或std::auto_ptr代替。盡管當時已經討論在所有可能的情況,但后面在實際項目實施中發現效果并不好。原因是方面的,比如在使用std::auto_ptr時會存在以下不得因素:
可能的額外開銷外(其實很小);
你需要時刻小心對象所有權的問題。盡管可能只需要稍微注意一下就可以了,但似乎沒有任何程序員喜歡過提心吊膽的日子;
你不能在容器(比如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
仿函數進行堆對象回收。