条款 48:认识模板元编程
模板元编程(Template metaprogramming,TMP)是编写基于模板的 C++ 程序并执行于编译期的过程,它并不是刻意被设计出来的,而是当初 C++ 引入模板带来的副产品,事实证明模板元编程具有强大的作用,并且现在已经成为 C++ 标准的一部分。实际上,在条款 47 中编写 traits classes 时,我们就已经在进行模板元编程了。
由于模板元程序执行于 C++ 编译期,因此可以将一些工作从运行期转移至编译期,这可以帮助我们在编译期时发现一些原本要在运行期时才能察觉的错误,以及得到较小的可执行文件、较短的运行期、较少的内存需求。当然,副作用就是会使编译时间变长。
模板元编程已被证明是“图灵完备”的,并且以“函数式语言”的形式发挥作用,因此在模板元编程中没有真正意义上的循环,所有循环效果只能藉由递归实现,而递归在模板元编程中是由 “递归模板具现化(recursive template instantiation)” 实现的。
常用于引入模板元编程的例子是在编译期计算阶乘:
template<unsigned n> // Factorial<n> = n * Factorial<n-1>
struct Factorial {
enum { value = n * Factorial<n-1>::value };
};
template<>
struct Factorial<0> { // 处理特殊情况:Factorial<0> = 1
enum { value = 1 };
};
std::cout << Factorial<5>::value;
模板元编程很酷,但对其进行调试可能是灾难性的,因此在实际应用中并不常见。我们可能会在下面几种情形中见到它的出场:
- 确保量度单位正确。
- 优化矩阵计算。
- 可以生成客户定制之设计模式(custom design pattern)实现品。
条款 49:了解 new-handler 的行为
当operator new
无法满足某一内存分配需求时,会不断调用一个客户指定的错误处理函数,即所谓的 new-handler,直到找到足够内存为止,调用声明于<new>
中的set_new_handler
可以指定这个函数。new_handler
和set_new_handler
的定义如下:
namespace std {
using new_handler = void(*)();
new_handler set_new_handler(new_handler) noexcept; // 返回值为原来持有的 new-handler
}
一个设计良好的 new-handler 函数必须做以下事情之一:
让更多的内存可被使用: 可以让程序一开始执行就分配一大块内存,而后当 new-handler 第一次被调用,将它们释还给程序使用,造成operator new
的下一次内存分配动作可能成功。
安装另一个 new-handler: 如果目前这个 new-handler 无法取得更多内存,可以调换为另一个可以完成目标的 new-handler(令 new-handler 修改“会影响 new-handler 行为”的静态或全局数据)。
卸除 new-handler: 将nullptr
传给set_new_handler
,这样会使operator new
在内存分配不成功时抛出异常。
抛出 bad_alloc(或派生自 bad_alloc)的异常: 这样的异常不会被operator new
捕捉,因此会被传播到内存分配处。
不返回: 通常调用std::abort
或std::exit
。
有的时候我们或许会希望在为不同的类分配对象时,使用不同的方式处理内存分配失败情况。这时候使用静态成员是不错的选择:
public:
static std::new_handler set_new_handler(std::new_handler p) noexcept;
static void* operator new(std::size_t size);
private:
static std::new_handler currentHandler;
};
// 做和 std::set_new_handler 相同的事情
std::new_handler Widget::set_new_handler(std::new_handler p) noexcept {
std::new_handler oldHandler = currentHandler;
currentHandler = p;
return oldHandler;
}
void* Widget::operator new(std::size_t size) {
auto globalHandler = std::set_new_handler(currentHandler); // 切换至 Widget 的专属 new-handler
void* ptr = ::operator new(size); // 分配内存或抛出异常
std::set_new_handler(globalHandler); // 切换回全局的 new-handler
return globalHandler;
}
std::new_handler Widget::currentHandler = nullptr;
- 调用标准set_new_handler,告知Widget的错误处理函数;
- 调用operator new,执行实际的内存分配。如果分配失败,operator new会调用Widget的new-handler。如果operator new最终无法分配足够的内存,会抛出一个bad_alloc异常,在此情况下Widget的operator new必须回复原本的global的new-handler,然后再传播该异常;
- 如果operator new能够分配足够一个Widget对象所用的内存,Widget的operator new会返回一个指针,指向分配所得,Widget的析构函数会自动将operator new被调用前的那个global的new-handler恢复回来。
Widget
的客户应该类似这样使用其 new-handling:
void OutOfMem();
Widget::set_new_handler(OutOfMem);
auto pw1 = new Widget; // 若分配失败,则调用 OutOfMem
string* ps = new string; // 如果内存分配失败,调用global new-handler函数(如果有的话)
Widget::set_new_handler(nullptr);
auto pw2 = new Widget; // 若分配失败,则抛出异常
实现这一方案的代码并不因类的不同而不同,因此对这些代码加以复用是合理的构想。一个简单的做法是建立起一个“mixin”风格的基类,让其派生类继承它们所需的set_new_handler
和operator new
,并且使用模板确保每一个派生类获得一个实体互异的currentHandler
成员变量:
template<typename T>
class NewHandlerSupport { // “mixin”风格的基类
public:
static std::new_handler set_new_handler(std::new_handler p) noexcept;
static void* operator new(std::size_t size);
... // 其它的 operator new 版本,见条款 52
private:
static std::new_handler currentHandler;
};
template<typename T>
std::new_handler NewHandlerSupport<T>::set_new_handler(std::new_handler p) noexcept {
std::new_handler oldHandler = currentHandler;
currentHandler = p;
return oldHandler;
}
template<typename T>
void* NewHandlerSupport<T>::operator new(std::size_t size) {
auto globalHandler = std::set_new_handler(currentHandler);
void* ptr = ::operator new(size);
std::set_new_handler(globalHandler);
return globalHandler;
}
template<typename T>
std::new_handler NewHandlerSupport<T>::currentHandler = nullptr;
class Widget : public NewHandlerSupport<Widget> {
public:
... // 不必再声明 set_new_handler 和 operator new
};
注意此处的模板参数T并没有真正被当成类型使用,而仅仅是用来区分不同的派生类,使得模板机制为每个派生类具现化出一份对应的currentHandler
。
这个做法用到了所谓的 CRTP(curious recurring template pattern,奇异递归模板模式) ,除了在上述设计模式中用到之外,它也被用于实现静态多态:
template <class Derived>
struct Base {
void Interface() {
static_cast<Derived*>(this)->Implementation(); // 在基类中暴露接口
}
};
struct Derived : Base<Derived> {
void Implementation(); // 在派生类中提供实现
};
除了会调用 new-handler 的operator new
以外,C++ 还保留了传统的“分配失败便返回空指针”的operator new
,称为 nothrow new,通过std::nothrow
对象来使用它:
Widget* pw1 = new Widget; // 如果分配失败,抛出 bad_alloc
if (pw1 == nullptr) ... // 这个测试一定失败
Widget* pw2 = new (std::nothrow) Widget; // 如果分配失败,返回空指针
if (pw2 == nullptr) ... // 这个测试可能成功
nothrow new 对异常的强制保证性并不高,使用它只能保证operator new
不抛出异常,而无法保证像new (std::nothrow) Widget
这样的表达式不会导致异常,因此实际上并没有使用 nothrow new 的必要。