49:了解new-handler的行为
当operator new无法满足某一内存分配需求时,它会抛出异常(以前会返回一个null)。在抛出异常之前,它会调用一个客户指定的错误处理函数,也就是所谓的new-handler。
客户通过调用set_new_handler来设置new-handler:
namespace std { typedef void (*new_handler)(); new_handler set_new_handler(new_handler p) throw(); }
set_new_handler返回之前设置的new_handler。
当operator new无法满足内存申请时,它会不断调用new-handler函数,直到找到足够内存。因此,一个设计良好的new-handler必须做以下事:
a:让更多内存可被使用,以便使operator new下一次分配内存能够成功。实现方法之一就是程序一开始就分配一大块内存,而后当new-handler第一次被调用时,将它们还给程序使用;
b:安装另一个new-handler:如果目前的new-handler无法获得更多内存,并且它直到另外哪个new-handler有此能力,则当前的new-handler可以安装那个new-handler以替换自己,下次当operator new调用new-handler时,就是调用最新的那个。
c:卸载new-handler,一旦没有设置new-handler,则operator new就会在无法分配内存时抛异常;
d:抛出bad_alloc异常;
e:不返回,直接调用abort或exit。
有时希望根据不同的class有不同的方式处理内存分配的情况,但是C++并不支持class专属之new-handler,但是C++支持class专属operator new,所以可以利用这点来实现。例子如下:
class Widget { public: static std::new_handler set_new_handler(std::new_handler p) throw(); static void * operator new(std::size_t size) throw(std::bad_alloc); private: static std::new_handler currentHandler; }; std::new_handler Widget::currentHandler = 0; std::new_handler Widget::set_new_handler(std::new_handler p) throw() { std::new_handler oldHandler = currentHandler; currentHandler = p; return oldHandler; }
使用Widget的用户首先调用Widget::set_new_handler设置其专属new-handler。然后,在Widget的专属operator new中,调用std::set_new_handler,设置全局new-handler为Widget的专属new-handler,然后调用全局operator new,执行内存分配。分配失败时,全局operator new会调用Widget的专属new-handler。
如果全局operator new最终无法分配足够内存,会抛出一个异常,或者分配成功后,此时,必须恢复全局new-handler为原来的设置。为了实现这一点,可以使用资源管理对象的方法:
class NewHandlerHolder { public: explicit NewHandlerHolder(std::new_handler nh):handler(nh) {} ~NewHandlerHolder() { std::set_new_handler(handler); } private: std::new_handler handler; // remember it NewHandlerHolder(const NewHandlerHolder&); // prevent copying NewHandlerHolder& operator=(const NewHandlerHolder&); };
有了资源管理类之后,Widget::operator new的代码如下:
void * Widget::operator new(std::size_t size) throw(std::bad_alloc) { //安装Widget专属new-handler,并使用NewHandlerHolder管理原有全局new-handler NewHandlerHolder h(std::set_new_handler(currentHandler)); //不管分配成功还是抛出异常,NewHandlerHolder的析构函数中会恢复全局new-handler return ::operator new(size); }
使用Widget的客户代码如下:
void outOfMem(); Widget::set_new_handler(outOfMem); // 设置Widget的专属new-handler Widget *pw1 = new Widget; //如果分配失败,调用outOfMem std::string *ps = new std::string; //如果分配失败,调用全局new-handler Widget::set_new_handler(0); Widget *pw2 = new Widget; //分配失败,直接抛出异常
实现class专属new-handler不会因class不同而不同,所以可以建立起一个mixin风格的base class。但是因为涉及到static成员,为了使derived class获得不同的base class属性,这里可以使用template:
template<typename T> class NewHandlerSupport{ public: static std::new_handler set_new_handler(std::new_handler p) throw(); static void * operator new(std::size_t size) throw(std::bad_alloc); private: static std::new_handler currentHandler; }; template<typename T> std::new_handler NewHandlerSupport<T>::set_new_handler(std::new_handler p) throw() { std::new_handler oldHandler = currentHandler; currentHandler = p; return oldHandler; } template<typename T> void* NewHandlerSupport<T>::operator new(std::size_t size) throw(std::bad_alloc) { NewHandlerHolder h(std::set_new_handler(currentHandler)); return ::operator new(size); } // this initializes each currentHandler to null template<typename T> std::new_handler NewHandlerSupport<T>::currentHandler = 0;
有了这个模板之后,Widget可以直接继承它:
class Widget: public NewHandlerSupport<Widget> { ... };
这样看起来会有些奇怪,而且NewHandlerSupport模板从未使用类型参数T。这种技巧的作用,只是为了保证:继承自NewHandlerSupport的每一个class,都具有不同的NewHandlerSupport属性,也就是static成员变量currentHandler。这种技术还有自己的名字:“怪异的循环模板模式”(curiously recurring template pattern, CRTP)。
直到1993年,C++还要求operator new在分配失败时返回null,新一代的operator new则应该抛出bad_alloc异常,但是许多C++程序是在编译器支持新规范之前写出来的,因此C++提供了另一形式的operator new,他会在分配失败时返回null:
class Widget { ... }; Widget *pw1 = new Widget; //分配失败抛出bad_alloc异常 if (pw1 == 0) ... // 这个测试一定失败 Widget *pw2 = new (std::nothrow) Widget; // 分配失败返回NULL if (pw2 == 0) ... // 这个测试可能成功
虽然nothrow版的operator new不抛异常,但是接下来Widget的构造函数调用时,内部可能又会new一些内存,而这次不一定会在使用nothrow new。所以,使用nothrow new只能保证operator new不抛异常,不保证像new (std::nothrow) Widget这样的表达式不抛异常,因此其实没有使用nothrow new的需要。
50:了解new和delete的合理替换时机
以下是替换编译器提供的operator new或operator delete的几个最常见的理由:
a:检测运用错误:内存相关的错误包括内存泄漏、多次delete等,使operator new持有一串地址,而operator delete将地址从中移走,可以很容易检测出上述错误用法。另外,编程错误可能导致数据写入点在分配区块之后(overruns)或之前(underrun),可以自定义一个operator new,超额分配内存,在额外空间放置特定的签名,operator delete可以检查签名是否正确来判断是否发生了overrun或underrun。
b:为了强化性能:编译器提供的operator new和operator delete主要用于一般目的,它们的工作对所有人都适度的好,但某些情况下,定制版的operator new和operator delete性能可以胜过缺省版本。要么是速度快,要么是更省内存。
c:为了收集使用数据;
d:为了弥补缺省分配器中的非最佳齐位,比如如果double是8-byte齐位的则访问速度最快,但编译器自带的operator new并不一定保证这一点,此时可以替换operator new为一个8-byte齐位的版本,使得程序效率大大提升;
e:为了将相关对象成簇几种:如果某种数据结构往往一起使用,你希望在处理这些数据时将内存页错误的频率降至最低,则new和delete的placement版本有可能完成这样的任务;
f:为了获得非传统的行为;
51:编写new和delete时需固守常规
编写operator new时需要注意:必得返回正确的值,内存不足时需要调用new-handling函数,必须有对付零内存需求的准备,还需避免不慎掩盖正常形式的new;operator new的返回值十分单纯。如果它有能力供应客户申请的内存,就返回一个指针指向那块内存。如果没有那个能力,则抛出一个bad alloc异常;operator new实际上不只一次尝试分配内存,并在每次失败后调用new-handling函数。这里假设new-handling函数也许能够做某些动作将某些内存释放出来。只有当指向new-handling函数的指针是null,operator new才会抛出异常。
下面是一个非成员函数的operator new伪码:
void * operator new(std::size_t size) throw(std::bad_alloc) { //你的operator new可能有更多的参数 using namespace std; if (size == 0) { //处理0内存需求,将其视为1Byte申请 size = 1; } while (true) { attempt to allocate size bytes; if (the allocation was successful) return (a pointer to the memory); // 申请失败,找到当前的new-handler new_handler globalHandler = set_new_handler(0); set_new_handler(globalHandler); if (globalHandler) (*globalHandler)(); else throw std::bad_alloc(); } }
因为没有办法可以直接取得当前的new-handler,所以先调用set_new_handler将其找出来。
对于operator new的成员函数版本,因为该函数会被derived classes继承,所以需要考虑的更周全些。因为写出定制型内存管理器的一个最常见理由是为针对某特定class的对象分配行为提供最优化,却不是为了该class的任何derived classes。也就是说,针对class X而设计的operator new,其行为很典型地只为大小刚好为sizeof(X)的对象而设计。然而一旦被继承下去,有可能base class的operator new被调用用以分配derived class对象。处理这种情况的最佳做法是将内存申请量错误的调用行为改为标准operator new:
void * Base::operator new(std::size_t size) throw(std::bad_alloc) { if (size != sizeof(Base)) return ::operator new(size); ... }
上面的代码并没有检测size等于0的情况,是因为C++保证所有独立式对象必须具有非0大小,所以sizeof(Base)不会等于0。
如果你打算控制class专属之“arrays内存分配行为”,那么你需要实现operator new [ ],唯一需要做的一件事就是分配一块未加工内存,因为你无法对array之内迄今尚未存在的元素对象做任何事情。实际上你甚至无法计算这个array将含多少个元素对象。首先你不知道每个对象多大,毕竟base class的operator new [ ]有可能经由继承被调用,将内存分配给“元素为derived class对象”的array使用。此外,传递给operator new[]的size t参数,其值有可能比“将被填以对象”的内存数量更多,因为条款16说过,动态分配的arrays可能包含额外空间用来存放元素个数。
operator delete情况更简单,需要记住的唯一事情就是C++保证“删除null指针永远安全”,下面是伪码:
void operator delete(void *rawMemory) throw() { if (rawMemory == 0) return; deallocate the memory pointed to by rawMemory; }
这个函数的member版本也很简单,只需要多加一个动作检查删除数量。万一你的class专属的operator new将大小有误的分配行为转交::operator new执行,你也必须将大小有误的删除行为转交::operator delete执行:
class Base { public: static void * operator new(std::size_t size) throw(std::bad_alloc); static void operator delete(void *rawMemory, std::size_t size) throw(); ... }; void Base::operator delete(void *rawMemory, std::size_t size) throw() { if (rawMemory == 0) return; if (size != sizeof(Base)) { ::operator delete(rawMemory); return; } deallocate the memory pointed to by rawMemory; return; }
有趣的是,如果即将被删除的对象派生自某个base class而后者欠缺virtual析构函数,那么C++传给operator delete的size_t可能不正确。
52:写了placement new也要写placement delete
当你写一个new表达式: Widget* pw=new Widget; 这种情况下共有两个函数被调用,一个是用以分配内存的operator new,一个是Widget的default构造函数。
假设operator new调用成功,构造函数却抛出异常。这种情况下,内存分配所得必须取消并恢复旧观,否则会造成内存泄漏。回收内存的责任是由C++运行期系统完成的。
运行期系统会调用operator new的相应operator delete版本,前提是它必须知道哪一个(因为可能有许多个)operator delete该被调用。如果目前面对的是拥有正常签名式的new和delete,这并不是问题,因为正常的operator new对应于正常的operator delete:
void* operator new(std::size_t) throw(std::bad_alloc); void operator delete(void *rawMemory) throw(); // 全局域 void operator delete(void *rawMemory, std::size_t size) throw(); // 类专属
因此,当只使用正常形式的new和delete,运行期系统毫无问题可以找出那个“知道如何取消new所作所为并恢复旧观”的delete。然而当你开始声明非正常形式的operator new,也就是带有附加参数的operator new,“究竟哪一个delete伴随这个new”的问题便浮现了。
如果operatornew接受的参数除了size_t之外还有其他,这便是个所谓的placement new。众多placement new版本中特别有用的一个是“接受一个指针指向对象该被构造之处”,那样的operator new长相如下:
void* operator new(std::size_t, void *pMemory) throw();
这个版本的new已被纳入C++标准程序库,只要#include <new>就可以取用它。这个new的用途之一是负责在vector的未使用空间上创建对象。它同时也是最早的placement new版本。实际上它正是这个函数的命名根据:一个特定位置上的new。以上说明意味术语placement new有多重定义。当人们谈到placement new,大多数时候他们谈的是此一特定版本,少数时候才是指接受任意额外实参之operator new。因此一般性术语“placement new”意味带任意额外参数的new,因为另一个术语“placement delete”直接派生自它,operator delete如果接受额外参数,便称为placement deletes。
假设写一个class专属的operator new,要求接受一个ostream,用来记录相关分配信息,同时又写了一个正常形式的class专属operator delete:
class Widget { public: ... static void* operator new(std::size_t size, std::ostream& logStream) throw(std::bad_alloc); static void operator delete(void *pMemory, std::size_t size) throw(); ... }; Widget *pw = new (std::cerr) Widget;
如果内存分配成功,而Widget构造函数抛出异常,运行期系统需要取消operator new的分配并恢复旧观。然而运行期系统无法知道真正被调用的那个operator new如何运作,因此运行期系统寻找“参数个数和类型都与operator new相同”的某个operator delete。所以对应的operator delete就应该是:
void operator delete(void *, std::ostream&) throw();
现在,既然Widget没有声明placement版本的operator delete,所以运行期系统不知道如何取消并恢复原先对placement new的调用。于是什么也不做,这就造成了内存泄漏。
规则很简单:如果一个带额外参数的operator new没有“带相同额外参数”的对应版operator delete,那么当new的内存分配动作需要取消并恢复旧观时就没有任何operator delete会被调用。
因此,Widget有必要声明一个placement delete,对应于那个有志记功能的placement new:
class Widget { public: ... static void* operator new(std::size_t size, std::ostream& logStream) throw(std::bad_alloc); static void operator delete(void *pMemory) throw(); static void operator delete(void *pMemory, std::ostream& logStream) throw(); ... };
这种情况下,如果Widget *pw = new(std::cerr) Widget中构造函数抛出异常,则对应的placement delete就会被调用。
如果手动delete:delete pw; 这种情况下,调用的是正常形式的operator delete,而非其placement版本。placement delete只有在“伴随placement new调用而触发的构造函数”出现异常时才会调用,而delete一个指针永远不会调用placement delete。这就表示必须提供一个正常的operator delete,以及一个对应placement new的placement delete版本。
另外,由于成员函数的名称会掩盖外围作用域中的相同名称,因此需要避免让class专属的new掩盖客户期望的其他new,比如:
class Base { public: ... static void* operator new(std::size_t size, std::ostream& logStream) throw(std::bad_alloc); ... }; Base *pb = new Base; // 错误,正常的new已经被掩盖了 Base *pb = new (std::cerr) Base; // 正确,调用Base的placement new
同样的,derived classes中的operator new会掩盖global版本和继承而来的operator new:
class Derived: public Base { public: ... static void* operator new(std::size_t size) // 重新定义正常的operator new throw(std::bad_alloc); ... }; Derived *pd = new (std::clog) Derived; // 错误,Base的placement new被掩盖了 Derived *pd = new Derived; // 正确,调用Derived的正常operator new
缺省情况下,C++在global作用域内提供以下形式的operator new:
void* operator new(std::size_t) throw(std::bad_alloc); // normal new void* operator new(std::size_t, void*) throw(); // placement new void* operator new(std::size_t, const std::nothrow_t&) throw(); // see Item 49
如果在class内声明任何operator new,它都会遮掩这些标准形式。可以建立一个base class,内含所有正常形式的new和delete,在需要时,可以继承并覆盖:
class StandardNewDeleteForms { public: // normal new/delete static void* operator new(std::size_t size) throw(std::bad_alloc) { return ::operator new(size); } static void operator delete(void *pMemory) throw() { ::operator delete(pMemory); } // placement new/delete static void* operator new(std::size_t size, void *ptr) throw() { return ::operator new(size, ptr); } static void operator delete(void *pMemory, void *ptr) throw() { return ::operator delete(pMemory, ptr); } // nothrow new/delete static void* operator new(std::size_t size, const std::nothrow_t& nt) throw() { return ::operator new(size, nt); } static void operator delete(void *pMemory, const std::nothrow_t&) throw() { ::operator delete(pMemory); } };
当想自定义operator new和delete时,可以利用继承和using:
class Widget: public StandardNewDeleteForms { // inherit std forms public: using StandardNewDeleteForms::operator new; // make those using StandardNewDeleteForms::operator delete; // forms visible static void* operator new(std::size_t size, std::ostream& logStream) throw(std::bad_alloc); static void operator delete(void *pMemory, std::ostream& logStream) throw(); ... };