条款49:了解new-handle的行为
Understand the behavior of the new-handler.
operator new:分配例程;
operator delete:归还例程。
new-handler:operator new无法满足客户的内存需求时所调用的函数。
由于heap是一个可写的全局资源,多线程环境下可能会出现多线程争用情况。因此,多线程环境下,需要适当的同步控制(synchronization),加锁等手段防止并发访问(concurrent access)内存。
operator new,operator delete适合分配单一对象。Array所有的内存需要用operator[] new分配,由operator[] delete归还。
STL容器使用的heap内存是由容器所拥有的分配器对象(allocator object)管理,不是被new和delete直接管理。
new-handler错误处理函数
当operator new无法满足某一内存分配需求时,会抛出异常。以前返回null指针,现在,在抛出异常前,会先调用一个客户指定的错误处理函数,即所谓new-handler。
客户调用set_new_handler指定这个错误处理函数,其原型:
// Effective C++描述
namespace std {
typedef void (*new_handler)();
new_handler set_new_handler(new_handler p) throw();
}
// MSVC 2017中的声明
// handler for operator new failures
typedef void (__CLRCALL_PURE_OR_CDECL * new_handler) ();
// FUNCTION AND OBJECT DECLARATIONS
_CRTIMP2 new_handler __cdecl set_new_handler(_In_opt_ new_handler) noexcept;
new_handler是一个由typedef定义的函数指针类型,没有返回值也不返回任何东西。
set_new_handler 是获得一个new_handler类型参数p并返回一个new_handler函数。尾端throw()是一份异常说明,表明该函数不抛出任何异常。
可以这样使用set_new_handler:
// 当operator new无法分配足够内存时,该函数被调用
void outOfMem()
{
std::cerr << "Unable to satisfy request for memory" << endl;
std::abort();
}
// 客户端测试代码
int main()
{
set_new_handler(outOfMem); //设置 new无法满足客户内存分配申请需求时,调用的错误处理函数
int **p = new int*[1000];
for (int i = 0; i < 1000; i++) {
p[i] = new int[100000000L];
}
cout << "return from main" << endl;
return 0;
}
在24GB内存机器上,报错:"Unable to satisfy request for memory"。
new-handler函数的规范
当operator new无法满足内存申请时,会不断调用new-handler函数,直到找到足够内存。一个设计良好的new-handler函数必须做以下事情:
-
让更多内存可被使用
以便operator new的下一次内存分配动作可能成功。一个实现策略:程序一开始执行分配一大块内存:而后当new-handler第一次被调用,就释放以归还给程序用。 -
安装另一个new-handler
如果当前new-handler没有取的更多可用内存能力,但知道哪个new-handler有这个能力,可以安装另一个new-handler替换自己(调用set_new_handler即可)。 -
卸除new-handler
传递null指针给set_new_handler,operator new内存分配不成功时,不会抛出任何异常。 -
抛出bad__alloc(或派生自bad_alloc)的异常
该异常不会被operator new捕捉,因此会被传播到内存申请处。 -
不返回
通常调用abort或exit。
new-handler的使用
有时,希望根据不同class以不同的方式处理内存分配失败情况
class X {
public:
static void outOfMemory();
...
};
class Y {
public:
static void outOfMemory();
...
};
X *p1 = new X; // 如果分配不成功,调用X::outOfMemory
Y *p2 = new Y; // 如果分配不成功,调用Y::outOfMemory
C++不支持class专属new-handler,只支持global new-handler。如果需要,就需要自行实现。方法是:令每个class提供自己的set_new_handler和operator new的static函数即可。
例如, class Widget使用operator new分配内存失败时,利用辅助类NewHandlerHolder的析构函数帮助恢复原来的new-handler。
// RAII对象管理new_handler, 对象创建时保存原来的global new_handler到handler, 析构时还原global new_handler
class NewHandlerHolder {
public:
explicit NewHandlerHolder(std::new_handler nh) : handler(nh) {}
~NewHandlerHolder() { std::set_new_handler(handler); }
private:
std::new_handler handler;
NewHandlerHolder(const NewHandlerHolder&); // 阻止copying constructor
NewHandlerHolder& operator=(const NewHandlerHolder&); // 阻止copying assignment
};
// 假设要处理Widget class内存分配失败情况
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; // currentHandler用来保存当前要传入的错误处理函数, 是在对象生成之前就有的, 所以是static
};
std::new_handler Widget::currentHandler = nullptr;
std::new_handler Widget::set_new_handler(std::new_handler p) throw()
{
std::new_handler oldHandler = currentHandler;
currentHandler = p;
return oldHandler;
}
void* Widget::operator new(std::size_t size) throw(std::bad_alloc)
{
// 安装Widget的new-handler, 分配内存或抛出异常就恢复global new-handler
NewHandlerHolder h(std::set_new_handler(currentHandler)); // 创建局部变量, 退出local作用域时自动析构, i.e. 用户自定义new-handler只有operator new申请分配内存期间有效
return ::operator new(size);
}
// 客户端测试代码
void outOfMem()
{
cerr << "out of memory" << endl;
std::abort();
}
int main()
{
Widget::set_new_handler(outOfMem);
// 会导致out of memory
for (size_t i = 0; i < LLONG_MAX; i++) {
Widget *pwl = new Widget;
}
std::string* ps = new std::string;
Widget::set_new_handler(0);
Widget *pw2 = new Widget;
cout << "return from main" << endl;
return 0;
}
上面代码巧妙之处,就是利用RAII方式,恢复Widget用operator new申请内存发生时的错误处理函数。
operator new中的临时对象 NewHandlerHolder h会在调用全局operator new之后,自动恢复global new-handler(不论成功与否)。
奇特的循环模板模式 CRTP
上面代码只适用于具体的class,然而每个要这样处理operator new异常的class都会这样写。于是,我们改写成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);
~NewHandlerSupport() { std::set_new_handler(olderHandler); }
private:
static std::new_handler currentHandler;
static std::new_handler olderHandler;
};
template<typename T>
std::new_handler NewHandlerSupport<T>::currentHandler;
template<typename T>
std::new_handler NewHandlerSupport<T>::set_new_handler(std::new_handler p) throw()
{
olderHandler = currentHandler;
currentHandler = p;
return olderHandler;
}
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);
}
class Widget : public NewHandlerSupport<Widget> {
// 已经拥有了NewHandlerSupport<T> 那部分成员
// ... 和先前一样, 但不必声明
};
为什么使用template?
我们并没有使用NewHandlerSupport template中的参数T,只是希望继承自NewHandlerSupport的每个class,都拥有不同的NewHandlerSupport复件,明确说,是其static成员currentHandler,参数T只是用来区分不同derived class。Template机制会为每个T生成一份currenHandler。
循环模板模式 CRTP - Do It For Me
Widget继承自一个模板化的templated base class,而后者又以Widget作为类型参数。这种技术被称为 奇特的循环模板模式(curiously recurring template pattern;CRTP)。有了NewHandlerSupport这样的template,为任何class添加一个专属new-handler成为易事。模板化的NewHandlerSupport
1993年以前,旧的operator new在无法分配足够内存时,返回null。新operator new,则应该抛出bad_alloc异常。由于要兼容新规范以前的程序,C++提供另一种形式operator new:负责供应传统的“分配失败便返回null”,称为“nothrow”形式 -- 因为在new的使用场合用了nothrow对象(头文件
class Widget { ... };
Widget* pw1 = new Widget; // 如果分配失败,抛出bad_alloc
if (pw1 == 0) ... // 该测试一定失败,因为pw1不会为null
Widget* pw2 = new (std::nothrow) Widget; // 如果分配失败,则返回0
if (pw2 == 0) ... // 该测试可能成功
nothrow new对异常强制保证性并不高。表达式“new(std::nothrow) Widget”发生两件事:1)nothrow 版的operator new被调用,用来分配足够内存给Widget对象。2)如果分配失败,返回null;如果分配成功,接下来调用Widget构造函数。nothrow new只能保证operator new不抛出异常,无法保证构造函数的调用不抛出异常。
小结
1)set_new_handler允许客户指定一个函数,在内存分配无法获得满足时被调用;
2)Nothrow new是有很多局限的工具,因为它只适用于内存分配,后继的构造函数调用还是可能抛出异常;
[======]
条款50:了解new和delete的合理替换时机
Understand when it makes sense to replace new and delete.
为什么会需要替换编译器提供的operator new或operator delete?
常见三个理由:
-
用来检测运用上的错误。
如果new所得内存,delete失败(或者没有delete),会导致内存泄漏。
如果new所得内存,多次调用delete,会导致不确定行为。
如果程序很可能导致数据“overruns”(写入点在分配区块尾端之后)或“underruns”(写入点在分配区块之前)。
重写operator new可超额分配内存,提供额外空间用于签名,重写operator delete变可以检查是否有越界操作。如果有,operator delete可以log发生问题的指针。 -
为了强化效能
编译器提供的operator new和operator delete主要用于一般目的,但对于特定问题,定制版本修改内存的分配和回收策略,可能更有效。 -
为了收集使用上的统计数据
在定制new和delete前,如何得知动态内存的使用情况?比如,分配区块大小分布,FIFO or LIFO or 随机分配和归还?自定义operator new和operator delete能轻松收集到这些信息。
定制operator new示例
例,定制简单operator new,协助检测“overruns”和“underruns”。
static const int signature = 0xDEADBEEF; // 签名
typedef unsigned char Byte;
// 定制operator new
// 这段代码还有若干小错误
void* operator new(std::size_t size) throw(std::bad_alloc)
{
using namespace std;
size_t realSize = size + 2 * sizeof(int);
void *pMem = malloc(realSize);
if (!pMem) throw bad_alloc();
// 将signature写入内存的最前段落和最后段落
*(static_cast<int*>(pMem)) = signature;
*(reinterpret_cast<int*>(static_cast<Byte*>(pMem)
+ realSize - sizeof(int))) = signature;
// 返回指针,指向第一个signature之后的内存位置
return static_cast<Byte*>(pMem) + sizeof(int);
}
// 定制配套operator delete
void operator delete(void* p) throw()
{
void *start = (static_cast<Byte*>(p) - sizeof(int));
free(start);
}
// 客户端测试
int main()
{
int* p = new int;
*p = 1;
cout << *p << endl;
delete p;
return 0;
}
该定制版operator new缺点:
1)没有遵循条款51,未内含一个无穷循环并在其中尝试分配内存,调用new-handler。也没有处理0byte申请。
2)没有考虑对齐问题。
3)还有可移植性,线程安全等问题。
对齐问题
这里,我们主要探讨对齐(alignment):有些计算机体系结构要求特定类型必须放在特定内存地址上。如指针地址必须是4倍数(four-byte aligned)或double地址必须是8倍数(eight-byte aligned)。如果没有遵循这个约束条件,可能导致运行期硬件异常。而有些并没有这么严格要求,对于如double,只要是byte对齐即可,但如果是8byte对齐,则访问速度会快很多。
malloc返回的指针是安全的,但我们在程序里面对其偏移了一个int大小的位置,而int是固定4byte,如果我们申请8byte的double,就可能导致对齐问题。
何时替换new/delete
何时在“全局性的”或者“class专属的”基础上,合理替换缺省的new和delete?
- 为了检测运用错误;
- 为了收集动态分配内存之使用统计信息;
- 为了增加分配和归还的速度
针对特定类型定制new和delete的速度,往往快于编译器提供的缺省new和delete - 为了降低缺省内存管理器带来的空间额外开销
泛用型内存管理器往往比定制性慢,还使用更多内存,因为它们常常在每个分配区块上招引某些额外开销。针对小型对象开发的分配器(如Boost的Pool程序库)本质上消除了这样的额外开销。 - 为了弥补缺省分配器中的非最佳对齐
将不保证对齐的new替换为对齐的版本,可能导致程序效率大幅提升。 - 为了将相关对象成簇集中
如果指定某个数据结构往往一起使用,而你有希望处理这些数据时,将“内存页错误”(page fault)的频率降至最低,那么为此数据结构创建另一个heap就有意义,这样它们就可以被成簇集中在尽可能少的内存页(page)上。见条款52。 - 为了获得非传统的行为
有时希望operator new和delete做编译器提供的缺省版本没做的事情,如将C API封装成C++ API,将归还内存覆盖为0。
小结
1)有许多理由写个自定义的new和delete,包括改善效能、对heap运用错误进行调试、收集heap使用信息。
[======]
条款51:编写new和delete时需固守常规
Adhere to conversion when writing new and delete.
条款50解释何时需要编写自己的operator new和operator delete。但如果定制自己的new和delete,应当遵守什么规则呢?
自定义new需要遵循的规则
1)内存不足时,必调用new-handler函数,必须有对付零内存需求的准备,需避免不慎掩盖正常形式的new(接口要求)。
2)operator new的返回值十分单纯。如果有能力提供客户申请的内存,就返回一个指针指向那块内存;如果没有能力,就遵循条款49,抛出bad_alloc异常。不过,operator new实际上不止一次尝试分配内存,并在每次失败后调用new-handling函数。这里假设new-handling函数也许能做某些动作,将某些内存释放。只有当指向new-handling函数的指针是null,operator new才会抛出异常。
3)处理零内存申请:即使客户要求0byte,operator new也得返回一个合法指针。这个看似诡异的行为,是为了简化语言的其他部分。
例,适用于单线程
void *operator new(std::size_t size) throw(std::bad_alloc)
{
using namespace std;
if (size == 0) { // 处理0byte申请
size = 1; // 将其视为1byte申请
}
while (true) {
尝试分配size bytes;
if (分配成功)
return (一个指针,指向分配得来的内存);
// 分配失败:找出目前的new-handling函数
new_handler globalHandler = set_new_handler(0);
set_new_handler(globalHandler);
if (globalHandler) (*globalHandler)();
else throw std::bad_alloc();
}
}
当上述operator new作为一个class的专属operator new时,存在一个问题:class作为Base可能会被继承,而针对class Base设计的operator new可能只刚好只为sizeof(Base)大小对象而设计,对于继承自Base的Derived,其对象大小很可能大于Base对象大小,这样就会导致“内存申请量错误”的问题。
class Base {
public:
static void* operator new(std::size_t size) throw(std::bad_alloc);
...
};
class Derived: public Base { ... }; //假设Derived未重写operator new
// 客户端
Derived *p = new Derived; // 这里调用Base::operator new
客户端调用operator new时,传入的是Derived对象大小sizeof(Derived),而Base::operator new中考虑申请对象大小是sizeof(Base),该参数由编译器自动生成并传入,Base对象大小通常大于Derived对象大小,这样就产生了问题。
解决办法:
void* Base::operator new(std::size_t size) throw(std::bad_alloc)
{
if (size != sizeof(Base)) //如果大小错误, 就令标准的operator new处理
return ::operator new(size);
... // 否则,在这里处理
}
撰写operator new时,不能保证要申请元素大小一定是当前class对象大小。传递给operator newp[]的大小,也不一定是每个元素大小 * 元素个数,因为可能还包含其他信息,如元素个数。
自定义delete需要遵循的规则
撰写operator delete时,需要记住:C++保证“删除null指针永远安全”。
1)non-member operator delete伪码(pseudocode)
void operator delete(void *rawMemory) throw()
{
if (rawMemory == 0) return; // 如果被删除的是null指针,那就什么都不做
现在,归还rawMemory所指内存;
}
2)member 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; // 如果被删除的是null指针,那就什么都不做
if (size != sizeof(Base)) {
::operator delete(rawMemory);
return;
}
现在,归还rawMemory所指内存;
return;
}
需要注意的是:如果即将被删除的对象派生自某个base class,而后者欠缺virtual析构函数,那么C++传给operator delete的size_t数值可能不正确。
小结
1)operator new应该内含一个无穷循环,并在其中尝试分配内存。无关无法满足内存需求,就应该调用new-handler(错误处理)。应该有能力处理0byte申请。Class专属版本还应该处理“比正确大小更大的(错误)申请”。
2)operator delete应该在收到null指针时,不做任何处理。Class专属版本应该处理“比正确大小更大的(错误)申请”。
[======]
条款52:写了placement new也要写placement delete
Write placement delete if you write placement new.
什么是placement new,placement delete?
默认情况下,我们使用operator new给对象分配存储空间并调用其构造函数
void* operator new(std::size_t) throw(std::bad_alloc); // 缺省new
// 客户端 对应调用方式
Widget* pw = new Widget;
// delete 对应正常的operator delete
void operator delete(void* )
客户端调用了2个函数:1)operator new分配内存,2)Widget的default构造函数。
假设第一个函数(operator new分配内存)调用成功,第二个函数(default构造函数)调用失败。那么第一步申请分配的内存就必须释放并恢复到旧的状态,否则会造成内存泄漏(memory leak)。但此时,客户还没用能力归还内存,因为Widget构造函数抛出了异常,也就是说,pw指针并没有被赋值,客户也就没有指向这块内存的指针。因此,归还内存就成了运行期系统的责任。
典型的正常形式operator new和delete:
void* operator new(std::size_t) throw(std::bad_alloc); // global和class作用域正常签名式
void operator delete(void* rawMem) throw(); // global作用域的正常签名式
void operator delete(void* rawMem, std::size_t size) throw(); // class作用域的典型签名式
placement new与placement delete
如果operator new接受的参数除了默认size_t外,还有其他参数,那么就称该operator new为placement new。
其中,有个特别的placement new版本,接受一个指针指向对象被构造之处,也就是说,pMem指向一块已经分配得到的内存,调用该placement new可以在指定内存(参数pMem指向的)上创建对象。大多数情况下,人们所指的placement new就是特定版本的operator new(唯一额外参数是void*),少数情况指包含任意额外参数。
与placement new相对地,也存在placement delete。
#include <new>
void* operator new(std::size_t, void* pMem) throw(std::bad_alloc); // placement new
void operator delete(void* pMem, std::size_t size) throw(); // placement delete
// Widget class 声明式
class Widget {
public:
static void* operator new(std::size_t, std::ostream& logStream) throw(std::bad_alloc); // placement new
static void operator delete(void* pMem, std::size_t size) throw(); // 正常class的专属delete
static void operator delete(void *pMem) throw();
static void operator delete(void* pMem, std::ostream& logStream) throw(); // 与placement new配套的placement delete
};
如果Widget构造函数抛出异常,调用哪个版本operator delete?
Widget构造函数抛出了异常,运行期系统有责任取消operator new的分配并恢复到旧状态,不过,运行期系统无法知道真正被调用的那个operator new如何运作(如在构造函数中又做了哪些事情),因此它无法取消分配、恢复旧状态。取而代之的是,运行期系统寻找“参数个数和类型都与operator new相同的某个operator delete”。如果找到,调用之;如果没有,就不会有任何operator delete被调用。
因此,Widget class的operator new抛出异常时,对应版本placement delete会被自动调用,让Widget有机会确保不泄漏任何内存。
// 客户端
Widget* pw = new (std::cerr) Widget; // 调用Widget::operator new(sizeof(Widget), cerr);
// 出现异常时,运行期系统选择调用operator new配套的operator delete(2者额外参数相同)
void Widget::operator delete(void*, std::ostream&) throw();
// 正常的释放内存操作
delete pw;
// delete pw调用Widget::operator delete(void*, size_t)
注意:对一个指针施行delete绝不会调用placement delete。
placement与名称遮掩问题
1)class专属placement new会遮掩正常的global new
class B
{
public:
...
// 该placement new会遮掩正常形式的global new
static void* operator new(std::size_t size, std::ostream& logStream) throw(std::bad_alloc);
}
B* pb = new B; // 错误:正常形式operator new会被遮掩
B* pb = new (std::cerr) B; // OK:调用B::operator new(size_t, ostream&)
2)derived class的专属operator new会这样global new和继承而来的operator new
class C : public B {
public:
...
// 该placement new会遮掩正常形式的global new和从B继承的placement new
static void* operator new(std::size_t size) throw(std::bad_alloc);
};
C* pc = new(std::clog) C; // 错误:B的placement new被遮掩
C* pc = new C; // OK:调用C::operator new(size_t)
如何解决placement名称遮掩问题?
除非确定就是想遮掩global new和Base class的placement版本,否则,可以使用using,或者在当前class明确定义专属placement new和placement delete。还有一个简便办法,就是建立一个base class,内含所有normal new和delete:
/* 标准形式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* pMem) throw()
{
::operator delete(pMem);
}
// placement new/delete
static void* operator new(std::size_t size, void* ptr) throw()
{
return ::operator new(size, ptr);
}
static void operator delete(void* pMem, void* ptr) throw()
{
return ::operator delete(pMem, 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* pMem, const std::nothrow_t) throw()
{
::operator delete(pMem);
}
};
class MyWidget : public StandardNewDeleteForms { // 继承标准形式
public:
// 避免基类的new/delete名称被遮掩
using StandardNewDeleteForms::operator new;
using StandardNewDeleteForms::operator delete;
// 添加一个自定义placement new
static void* operator new(std::size_t size, std::ostream& logStream) throw(std::bad_alloc);
// 添加一个自定义placement delete
static void operator delete(void* pMem, std::ostream& logStream) throw();
// ...
};
小结
1)当你写一个placement operator new,请确定也写出了对应的placement operator delete。如果没有这样做,程序可能会发生隐蔽的内存泄漏问题;
2)当你声明placement new和placement delete,请确定不要无意识(非故意)地遮掩了它们的正常版本。
[======]