8. 定制new和delete(Templatesand Generic Programming)
条款49:了解new-handler的行为(Understandthe behavior of the new-handler)
当operatornew无法满足某一内存分配需求时,它会调用一个客户指定的错误处理函数,及new-handler,new-handler可能会为operatornew找到足够的内存或者其他怎样以处理内存不足的情况,如果new-handler为空,operatornew抛出异常。
通过set_new_handler来设置内存不足时调用的操作,它是声明于<new>的一个标准程序库函数:
1. namespace std{
2. typedef void (*new_handler) ();
3. new_handler set_new_handler( new_handler p ) throw(); //承诺不抛出异常
4. };
5. //这样使用set_new_handler
6. void outOfMem(){
7. std::err<<"Unable to satisfy request for memory\n";
8. std::abort();
9. }
10.
11.int main(){
12. std::set_new_handler( outOfMem );
13. int* pBigDataArray = new int[10000000L];
14. ...
15.}
当operatornew无法满足内存申请时,它会不断调用new-handler函数,直到找到足够内存,或者最后抛出异常。因此,一个设计良好的new-handler函数必须做以下事情:
(1)让更多内存可被使用。实现此策略的做法是,程序一开始执行就分配一大块内存,而后当new-handler第一次被调用,将它们释还给程序。
(2)安装另一个new-handler。如果目前这个new-handler无法取得更多可用内存,或许它知道另外哪个new-handler有此能力,因为可以通过set_new_handler安装一个新的new-handler。
(3)卸载new-handler,也就是将null指针传给set_new_handler。一旦没有安装任何new-handler,operatornew会在内存分配不成功时抛出异常。
(4)抛出bad_alloc(或派生自bad_alloc)的异常。这样的异常不会被operatornew捕捉,因此会被传播到内存索求处。
(5)不返回,通常调用abort或exit。
假如你希望以不同方式处理内存分配失败情况,具体视class而定,也就是提供class之专属new-handler,你可以自己实现出这种行为,只需要令每一个class提供自己的set_new_handler和operatornew即可,其中set_new_handler使客户得以指定class专属的new-handler,而operatornew则确保在分配class对象内存的过程中以class专属之new-handler替换globalnew-handler。
1. class Widget{
2. public:
3. static std::new_handler set_new_handler( std::new_handler p )throw();
4. static void* operator new( std::size_t size ) throw( std::bad_alloc );
5. private:
6. static std::new_handler currentHandler;
7. };
8.
9. std::new_handler Widget::currentHandler = 0;
10.std::new_handler Widget::set_new_handler( std::new_handler p )throw(){
11. std::new_handler oldHandler = currentHandler;
12. currentHandler = p;
13. return oldHandler;
14.};
Widget专属的operatornew做的事情就是在调用globaloperator new之前将globalnew handler安装成Widget专属的new-handler,并在成功分配内存或抛出异常前将原先的globalnew-handler安装回去。为了达到这一点,可以使用资源管理类:
1. class NewHandlerHolder{
2. public:
3. explicit NewHandlerHolder( std::new_handler nh ):handler(nh){}
4. ~NewHandlerHolder(){
5. std::set_new_handler(handler); }
6. private:
7. std::new_handler handler;
8. //阻止copying
9. NewHandlerHolder( const NewHandlerHolder& );
10. NewHandlerHolder& operator=( const NewHandlerHolder& );
11.};
12.//于是Widget::operator new的实现就相当简单了:
13.void* Widget::operator new( std::size_t size ) throw ( std::bad_alloc ){
14. NewHandlerHolder h(std::set_new_handler(currentHandler));
15. return ::operator new(size);
16.}
事实上,所有希望实现专属set_new_handler的类在这方面的实现基本没有差异,因此我们其实可以把它总结成一个基类:
1. template<typename T>
2. class NewHandlerSupport {
3. public:
4. static std::new_handler set_new_handler( std::new_handler p )throw();
5. static void* operator new( std::size_t size ) throw( std::bad_alloc );
6. ...
7. private:
8. static std::new_handler currentHandler;
9. };
10.template<typename T>
11.std::new_handler
12.NewHandlerSupport<T>::set_new_handler( std::new_handler p )throw(){
13. std::new_handler oldHandler = currentHandler;
14. currentHandler = p;
15. return oldHandler;
16.}
17.
18.template<typename T>
19.void*
20.NewHandlerSupport<T>::operator*( std::size_t size ) throw( std::bad_alloc ){
21. NewHandlerHolder h(std::set_new_handler(currentHandler));
22. return ::operator new(size);
23.}
这里的templateT完全没有被使用,但是,它确保了每一个derivedclass获得一个实体互异的currentHandler。
最后,可以使用
1. Widget* pw2 = new (std::nothrow) Widget;
来获得返回0而不是抛出异常的operator new,但是它只保证operator new不抛出异常,并不保证后续的构造函数不抛出异常,要知道,operator new分配完内存后,还会使用构造函数初始化这片内存。而nothrow只能保证在分配内存时不抛出异常。
条款50: 了解new和delete的合理替换时机
为什么会想要替换编译器提供的operator new或operator delete呢?下面是三个常见的理由:
(1)用来检测运用上的错误。如果将“new所得内存delete”却不幸失败,会导致内存泄漏。如果在“new所得内存”身上多次delete则会导致不确定行为。如果让operator new持有一串动态分配所得的地址,而operator delete将地址从中移动,便可以很容易地检测出上述错误用法。另外可以在operator new时超额分配内存,以额外空间放置特定的byte pattern,operator delete便得以检查上述签名是否原封不动,若否的话说明在分配区的某个生命时间发生了overrun或underrun。可以通过operator delete将出错的指针载入日志。
(2)为了强化效能。编译器所带的operator new和operator delete主要用于一般目的,它处理的内存请求有时很大,有时很小,它必须处理大数量短命对象的持续分配和归还。它们必须考虑碎片问题。定制版的operator new和operator delete通常在性能上用过缺省版本,它们运行得比较快,需要的内存比较少。
(3)为了收集使用上的统计数据。在定制特定new和delete之前,我们应该收集软件如何使用其动态内存:分配区块的大小分布如何?寿命分布如何?它们倾向于使用FIFO还是LIFO还是随机次序来分配和归还?它们的运用形态是否随时间改变?等等。
(4)为了弥补缺省分配器中的非最佳齐位。
(5)为了将相关对象成簇集中。
(6)为了获得非传统的行为。
条款51: 编写new和delete时需固守常规
operator new必须遵守的规矩:返回正确的值,内存不足时必得调用new-handling函数,必须有对付零内存的准备,还需避免不慎掩盖正常形式的new。C++规定,即使客户要求0 bytes,operator new也得返回一个合法指针。下面是个non-member operator new的伪码:
1. void* operator new( std::size_t, size ) throw (std::bad_alloc ){
2. using namespace std;
3. if( size == 0 ) //处理0 byte申请
4. size = 1;
5. while( true ){
6. //尝试分配size bytes
7. void* pMem = malloc(size);
8. if( pMem ) //成功分配
9. return pMem;
10. //分配失败
11. //获取当前的new_handler
12. new_handler globalHandler = set_new_handler(0);
13. set_new_handler( globalHandler );
14.
15. if( globalHandler ) (*globalHandler)();
16. else throw std::bad_alloc();
17. }
18.}
对于class专属的operator new,还必须注意到它很可能被其派生类继承。而写出定制型内存管理器的一个最常见理由是为针对某特定class的对象分配行为提供最优化,却不是为了该class的任何派生类。也就是说,针对class X而设计的operator new,其行为很典型地只为大小刚好为sizeof(X)对象而设计。然而一旦被继承下去,有可能基类的operator new被调用用以分配派生类对象:
1. class Base{
2. public:
3. static void* operator new(std::size_t size) throw( std::bad_alloc );
4. ...
5. };
6. class Derieved : public Base{
7. ...
8. };
9.
10.Derieved* p = new Derieved; //这里调用的是Base::operator new
11.//因此,假如operator new是被专门设计用以返回Base对象大小的内存
12.//有必要为此情况做出一点处理:
13.void* Base::operator new( std::size_t size ) throw ( std::bad_alloc ){
14. if( size != sizeof(Base) )
15. //注意,sizeof(Base)一定不为0,所以size为0的情况将交给::operator new
16. return ::operator new(size);
17. ...
18.}
如果你打算控制class专属之arrays内存分配行为,那么便需要实现operator new的array兄弟版:operator new[],这时你唯一需要做的一件事就是分配一块未加工内存。
至于operator delete,唯一需要记住的是:C++保证“删除null指针永远安全”。另外,如果你的class专属operator new将大小有误的分配行为转交::operator new执行,那么也必须将大小有误的删除行为转交::operator delete:
1. class Base{
2. public:
3. static void* operator new( std::size_t size ) throw( std::bad_alloc );
4. static void operator delete( void* rawMemory, std::size_t size ) throw();
5. ...
6. };
7.
8. void Base::operator delete( void* rawMemory, std::size_t size ) throw(){
9. if( rawMemory == 0 )
10. return;
11. if( size != sizeof(Base){
12. ::operator delete(rawMemory);
13. return;
14. }
15. return;
16.}
条款52: 写了placement new也要与placement delete
所谓的placement new,是相对于正常的operator new而言的,或者从广义上说,应该是“被重载的operator new”(我个人是这么感觉的)。
正常的operator new及其相应的operator delete版本如下:
1. void* operator new( std::size_t ) throw ( std::bad_alloc );
2. void operator delete( void* rawMemory ) throw ();
3. //class作用域中的delete,包含一个大小
4. void operator delete( void* rawMemory, std::size_t size )throw();
new表达式:
1. Widget* pw = new Widget;
实际调用了两个函数,一个是用以分配内存的operator new,一个是Widget的default构造函数。
假设其中第一个函数调用成功,即内存已经被分配,第二个函数却抛出异常。此时,pw尚未被赋值,被内存已经被分配,但是我们没有得到指向该内存的指针,因此我们没有能力释放被分配的内存。因此,释放内存的责任就落在了C++运行期系统上。运行期系统将会找到与刚才调用的operator new相配对的operator delete来执行这一任务。
假设我们为Widget写了一个专属的operator new,要求接受一个ostream用以记录分配信息:
1. class Widget{
2. public:
3. static void* operator new( std::size_t size, std::ostream& log )
4. throw( std::bad_alloc );
5. ...
6. };
“如果operator new接受的参数除了一定会有的那个size_t之外还有其他,这便是个所谓的placement new”。
最早的placement new版本是一个“接受一个指针指向对象被构造之处”的operator new,其长相如下:
1. void* operator new( std::size_t, void* pMemory ) throw();
它的用途之一是负责在vector的未使用空间上创建对象。
如果在调用
1. Widget* pw = new (std::err) Widget;
时Widget构造函数抛出异常,C++运行期系统将自动调用与operatornew相对应的版本来释放内存。所谓的“相对应的版本”,是指“参数个数和类型都与operator new相同”的某个operator delete,因此,当你定义了一个placementnew时,你必须定义一个对应的placement delete,否则系统找不到对应的delete,无法执行内存回收工作,那么内存就泄漏了。
另外,operatornew函数的名字匹配规则同一般的成员函数一样,如果你只在基类里声明了一个placement new,关于基类的new调用将无法使用正常版本;然后如果你又在继承自该基类的派生类里重新声明正常形式的new,基类的placement new也将不能默认作用在该派生类上。总之就是,operator new的名字匹配规则跟一般的成员函数是一样的。
placement delete只有在“伴随placement new调用而触发的构造函数”出现异常时才会被调用。