条款48:认识template元编程
Template metaprogramming(TMP,模板元编程)是编写template_based C++程序并执行于编译期的过程。所谓的TMP,就是以C++写成,执行于C++编译器内的程序。一旦TMP程序结束执行,其输出,也就是从template具现出来的若干C++源码,便会一如往常地被编译。
TMP可将工作由运行期移往编译期,因而得以实现早期错误侦测和更高的执行效率。而它对于“难以或甚至不可能在运行期实现出来的行为”的表现能力也很吸引人。
条款47中的traits解法就是TMP,其针对不同类型而进行的代码,被拆分为不同的函数,每个函数所使用的操作(操作符)都可施行于该函数所对付的类型。
下面示范TMP循环的例子,因为TMP循环并不涉及递归函数的调用,而是涉及“递归模板具现化”。这个示范TMP的用途就像“hello world”示范任何传统语言的用途一样。
我们通过TMP的阶乘运算示范如何通过“递归模板具现化”实现循环,以及如何在TMP中创建和使用变量:
template<unsigned n>
struct Factorial{
enum{value=n*Factorial<n-1>::value};
};
template<>
struct Factorial<0>{
enum{value=1};
};
//你可以这样使用Factorial
int main()
{
std::cout<<Factorial<5>::value;//印出120
std::cout<<Factorial<10>::value;//印出3628800
}
为求领悟TMP之所以值得学习,很重要的一点是先对它能够达成什么目标有一个较好的理解:
- 确保量度单位正确。使用TMP可以确保在编译期程序中所有量度单位的组合都正确,不论计算多么复杂,这也是为什么TMP可以被用来进行早期错误侦测。
- 可以生成客户定制的设计模式实现品。TMP可被用来生成“基于政策选择组合”的客户定制代码,也可用来避免生成某些特殊类型并不适合的代码。
- 优化矩阵运算。考虑以下代码:
typedef SquareMatrix<double,10000> BigMatrix; BigMatrix m1,m2,m3,m4,m5; ... BigMatrix result=m1*m2*m3*m4*m5;
以“正常的函数”调用动作来计算result,会创建4个暂时性矩阵,每一个用来存储operator*的调用结果。如果使用与TMP相关的template技术,就有可能消除哪些临时对象并合并循环,这一切都无需改变客户端的用法(像上面那样)
条款49:了解new-handler的行为
了解C++内存管理例程的行为,两个主角是分配例程(operator new)和归还例程(operator delete),配角是new-handler,这是当operator new 无法满足客户的内存需求时所调用的函数。
operator new 和operator delete的一些事情也都适用于operator new[]和operator delete[]。
请注意:STL容器所使用的heap内存是由容器所拥有的分配器对象管理,不是被new 和delete直接管理。
当operator new无法满足某一内存分配需求时,它会抛出异常,当operator new抛出异常以反映一个未获满足的内存需求之前,它会先调用一个客户指定的错误处理函数,一个所谓的new-handler。set_new_handler允许客户指定一个函数,在内存分配无法满足的时候被调用。
namespace std{
typedef void (*new_handler)();//new_handler是个typedef,定义出一个指针指向函数,该函数没有参数也不返回任何东西
new_handler set_new_handler(new_handler p) throw();//表示该函数不抛出任何异常
//返回的是在set_new_handler()被调用前正在执行(但马上要被替换)的那个new_handler函数的指针
你可以这样使用set_new_handler
void outOfMem()
{
std::cerr<<"unable to satisfy request for memory\n";
std::abort();
}
int main()
{
std::set_new_handler(outOfMem);
int* pBigDataArray=new int[100000000L];
...
}
当operator new无法满足内存申请时,它会不断的调用new-handler函数,直到找到足够内存。
一个设计良好的new-handler函数必须做以下事情:
- 让更多的内存可被使用。这便造成operator new内的下一次内存分配动作可能成功。实现此策略的一个做法是:程序一开始执行就分配一大块内存,而后当new-handler第一次被调用,将他们 释还给程序使用
- 安装另一个new-handler。使用set_new_handler调用其他版本的new-handler函数,这个新版本修改自己的行为以做些不同的事情。常见的做法之一是令new-handler修改“会影响new-handler行为”的static数据、namespace数据或global数据。
- 卸除new-handler。也就是将null指针传给set_new_handler。一旦没有安装任何new-handler,operator new会在内存分配不成功时抛出异常。
- 抛出bad_alloc(或派生自它)的异常。这样的异常不会被operator new捕捉,因此会被传播到内存索求处。
- 不返回。通常调用abort或exit。
template<typename T>
class NewHandlerSupport{
public:
static std::new_handler set_new_handler(std::new_handler p) throw();//static成员必须在class定义式之外被定义,除非他们是const而且是整数型,见条款2
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_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)
{
NewHandleSupport h(std::set_new_handler(currrentHandler));//安装T的new_handler。
return::operator new(size);//分配内存或抛出异常。恢复global new_handler。
}
template<typename T>
std::new_handler NewHandlerSupport<T>::currentHandler=0;//将每一个currentHandler初始化为null
//有了这个class template,为一个名为Widget的类添加set_new_handler支持能力就很简单了,如下:
class Widget:public NewHandlerSupport<Widget>{
...
};//不必声明set_new_handler和operator new
类型参数T只是用来区分不同的derived class。template机制会自动为每一个T(NewHandlerSupportL赖以具现化的依据)生成一份currentHandler。
class Widget{...}
Widget* pw1=new Widget;
if(pw1==0)...//这个测试一定失败
Widget* pw2=new(std::nothrow)Widget;//如果分配失败,返回0
if(pw2==0)...//这个测试可能成功
虽然new(std::nothrow) Widget调用operator new失败后返回null并不抛掷异常,但Widget构造函数却有可能抛出异常,该异常会一如往常地传播。 所以使用nothrow new 只能保证operator new不抛掷异常,不能保证像new(std::nothrow) Widget的表达式不抛掷异常,nothrow是一个颇为具现的工具,其实你没有运用它的需要。nothrow new对异常的强制性保证性并不高。
- 用来检测运用上的错误。如果将new所得内存delete掉却不幸失败,或导致内存泄露。如果在new所得内存身上多次delete则会导致不明确的行为。此外各式各样的编程错误可能会导致数据overruns(写入点在分配区尾端之后)或underruns(写入点在分配区起点之前)。如果我们自定义一个operator new,便可以超额分配内存,以额外空间(位于分配区之前或之后)放置特定的签名。使用operator delete得以检查上述签名是否原封不动。
- 为了强化效能。因为现实存在许多不同的堆内存管理器的要求,因此编译器所带的new 和delete采用中庸之道,他们的工作对每个人都是适当地好,但不对特定任何人有最佳表现。定制版的new 和delete性能胜过缺省版本。
- 为了收集 使用上的统计数据。自行定义的new和delete使我们得以轻松收集到很多信息,比如分配区块的大小分布、寿命分布、是以先进先出还是后进先出次序互随机次序来分配和归还、任何时刻所使用的最大动态分配量是多少等等。
- 为了增加分配和归还的速度。泛用型的分配器往往(并不总是)比定制型的分配器慢,特别是当定制型分配器专门针对某特定类型的对象而设计时。
- 为了降低缺省内存管理器带来的空间额外开销。泛用型内存管理器往往比定制型使用更多的内存。
- 为了弥补缺省分配器中的非最佳齐位。在X86体系结构中double的访问速度最快速——如果他们都是8-byte齐位。但是编译器自带的operator new并不保证对动态分配而得的double采取8-byte齐位。
- 为了将相关对象成簇集中。如果你知道特定某个数据结构往往被一起使用,而你希望在处理这些数据时将“内存页错误”的频率降至最低,那么为此数据创建另一个heap就有意义。
- 为了获得非传统的行为。有时候你会希望operator new和operator delete做编译器附带版没做的事情。例如你可能会希望分配和归还内存内的区块,但唯一能够管理该内存的只有C API函数,那么写下一个定制版的new 和delete,你便为C API穿上C++外套,你也可以写自定的operator delete,在其中将所有归还内存内容覆盖为0,增加应用程序的数据安全性。