条款四十七:请使用trait classes来表示类型信息
这一条款主要来讨论模板中迭代器的属性iterator_category,它可以通过类似于vector::iterator::iterator_category的方式来取得。
到这里我们有必要学习一下STL迭代器的类型,总共有五种,分别是:
input_iterator:只读,只能逐个前移
output_iterator:只写,只能逐个前移
forward_iterator:可读可写,只能逐个前移
bidirectional_iterator:可读可写,支持逐个前移和后移
random_access_iterator:可读可写,支持随机访问(任意步数移动)
为了表明容器内使用的是哪一种迭代器,STL在定义迭代器会总会打个一个标记“tag”,每个tag都是一个空的结构体,对应于以上五种迭代器,tag也有五个:
struct input_iterator_tag{};
struct output_iterator_tag{};
struct forward_iterator_tag: public input_iterator_tag{};
bidirectional_iterator: public forward_iterator_tag{};
random_access_iterator: public bidirectional_iterator_tag{};
注意这五个tag之间,有些存在继承关系。
这个标记有什么用呢?STL在写vector的时候,会这样:
template class <T>
class vector
{
public:
class iterator
{
public:
typedef random_access_iterator iterator_category;
…
}
…
}
写list的时候,会这样写:
template class <T>
class list
{
public:
class iterator
{
public:
typedef bidirectional_iterator iterator_category;
…
}
…
}
然迭代器已经由tag说明了它的类型(双向的,还是随机访问),那我们如何去利用它呢?比如现在我想要写一个迭代器前移的通用函数DoAdvance,不同迭代器类型会有不同的实现方式,所以我们可以像下面这样:
template <class T>
void DoAdvance(T Container)
{
typedef T::iterator::iterator_category IteratorCategory;
if (typeid(IteratorCategory) == typeid(input_iterator_tag))
{
cout << "Do manner in input_iterator_tag" << endl;
}
else if (typeid(IteratorCategory) == typeid(output_iterator_tag))
{
cout << "Do manner in output_iterator_tag" << endl;
}
else if (typeid(IteratorCategory) == typeid(forward_iterator_tag))
{
cout << "Do manner in forward_iterator_tag" << endl;
}
else if (typeid(IteratorCategory) == typeid(bidirectional_iterator_tag))
{
cout << "Do manner in bidirectional_iterator_tag" << endl;
}
else if (typeid(IteratorCategory) == typeid(random_access_iterator_tag))
{
cout << "Do manner in random_access_iterator_tag" << endl;
}
}
参数T是容器的类型,比如vector<int>
,如果像下面这样调用:
vector<int> v;
DoAdvance(v);
那么输出是Do manner in random_access_iterator_tag
,因为vector的迭代器是随机访问型的,可以按随机访问类型的处理方式来去实现前移操作。typeid返回结果是名为type_info的标准库类型的对象的引用,它指明了这个对象/定义的类型。
因为这里讨论的是迭代器,所以更常见的是直接传迭代器进去,像这样:
template <class IterT>
void DoAdvance(IterT Iter)
{
typedef IterT::iterator_category IteratorCategory;
if (typeid(IteratorCategory) == typeid(input_iterator_tag))
{
cout << "Do manner in input_iterator_tag" << endl;
}
…
}
iterator_traits的定义如下:
template<class IterT>
struct iterator_traits<IterT>
{
typedef typename IterT::iterator_category iterator_category;
…
};
这个感觉只是简化了输入代码量而已,本质上还是去获得迭代器的tag,它有一个针对指针的偏特化版本,像下面这样:
template<class IterT>
struct iterator_traits<IterT*>
{
typedef random_access_iterator_tag iterator_category;
};
这里都是用typeid去进行类型判断的,它是在运行期才能执行,那么能不能放在编译期呢,当然可以,就是要用到函数的重载,像下面这样:
template <class IterT>
void DoAdvance(IterT Iter, input_iterator_tag)
{
cout << "Do manner in input_iterator_tag" << endl;
}
template <class IterT>
void DoAdvance(IterT Iter, random_access_iterator_tag)
{
cout << "Do manner in random_access_iterator_tag" << endl;
}
像下面这样使用;
1 vector<int>::iterator iter;
2 DoAdvance(iter, iterator_traits<vector<int>::iterator>::iterator_category());
注意迭代器的tag是可以直接作为函数形参的,这样就可以在编译期决定到底执行哪一种迭代器的行为了。
条款标题的traint classes是一个广义的概念,我们之前讨论的iterator_traits只是其一部分,除以之外,还有四份迭代器相关的信息(如value_type等),TR1导入许多新的trait classes,比如is_fundamental等(判断T是否是内置类型)。
最后,我们来总结一下:
- Traits class使得类型相关信息可以在编译期可用,它们以template和template特化完成实现;
- 整合重载技术后,traits classes有可能在编译期对类型执行if-else测试。
条款四十八:了解模板元编程
作为模板部分的结束节,本条款谈到了模板元编程,元编程本质上就是将运行期的代价转移到编译期,它利用template编译生成C++源码,举下面阶乘例子:
template <int N>
struct Factorial
{
enum
{
value = N * Factorial<N - 1>::value
};
};
// 特化版本
template <>
struct Factorial<0>
{
enum
{
value = 1
};
};
int main()
{
cout << Factorial<5>::value << endl; // 输出120
}
在编译期,Factorial<5>::value就被翻译成了5 * 4 * 3 * 2 * 1,在运行期直接执行乘法即可。
元编程有何优点?
- 以编译耗时为代价换来卓越的运行期性能,因为对于产品级的程序而言,运行的时长远大于编译时长。
- 将原来运行期才能发现的错误提前到了编译期,要知道,错误发现的越早,代价越小。
元编程有何缺点?
- 代码可读性差,写起来要运用递归的思维,非常困难。
- 调试困难,元程序执行于编译期,不能debug,只能观察编译器输出的error来定位错误。
- 编译时间长,运行期的代价转嫁到编译期。
- 可移植性较差,老的编译器几乎不支持模板或者支持极为有限。
模板元编程TMP虽然有这么多的缺点,但它已经被证明是“图灵完全”的了,意思是它的威力大到足以计算任何事物。目前boost库中用到了一些TMP技术,但大部分项目还是因为TMP的一些缺点而没有广泛采用,所以这里我们只要略做了解即可。
下面总结一下:
- TMP可将工作由运行期转移到编译期,因而得以实现早期错误侦测或者更高的执行效率。
- TMP可被用来生成“基于政策选择组合”的客户定制代码,也可以用来避免生成对某些特殊类型并不适合的代码。(这句话看不懂也没关系)
条款四十九:了解new_handler的行为
本章开始讨论内存分配的一些用法,C/C++内存分配采用new和delete。在new申请内存时,可能会遇到的一种情况就是,内存不够了,这时候会抛出out of memory的异常。有的时候,我们希望能够调用自己定制的异常处理函数,这就是本条款要说的。
在声明于的一个标准程序库中,有如下的接口:
namespace std
{
typedef void (*new_handler)();
new_handler set_new_handler(new handler p) throw();
}
注意这里面typedef了一个函数指针new_handler,它指向一个函数,这个函数的返回值为void,形参也是void。set_new_handler就是将new_handler指向具体的函数,在这个函数里面处理out of memory异常(函数末尾的throw()表示它不抛出任务异常),如果这个new_handler为空,那么这个函数没有执行,就会抛出out of memory异常。
void MyOutOfMemory()
{
cout << "Out of memory error!" << endl;
abort();
}
int main()
{
set_new_handler(MyOutOfMemory);
int *verybigmemory = new int[0x1fffffff];
delete verybigmemory;
}
这里预先设定好new异常时调用的函数为MyOutOfMemory,然后故意申请一个很大的内存,就会走到MyOutOfMemory中来了。
好,我们更进一步,现在想要在不同的类里面定制不同的new_handler处理机制,一种想法是在类内部定义set_new_handler函数,将new_handler作为私有的成员变量,具体的new_handler函数可以由构造函数传入,但编译器要求set_new_handler是静态的,所以通过构造函数传入new_handler不被编译器支持,只能将set_new_handler与operator new都写成静态的,同时定义一个静态的new_handler变量,像下面这样:
class Widget
{
private:
static new_handler CurrentHandler;
public:
void set_new_handler(new_handler h) throw()
{
CurrentHandler = h;
}
static void* operator new(size_t size)
{
Widget::set_new_handler(CurrentHandler);
return ::operator new(size);
}
};
new_handler Widget::CurrentHandler = 0;
属于类的静态变量CurrentHandler用于保存当前环境下的new_handler函数,在operator_new中,先设置成当前的new异常处理函数,再去调用std的operator new,执行内存分配操作。但这里就存在问题了,set_new_handler到下一次设置它为止,一直都是生效的,我们只想在处理这个类对象的分配时用自定义的new_handler函数,但是类似于new int,new char这些基本类型,还是希望走默认的new_handler(就是null,就是什么也不执行,如我们期望,这样会抛出异常)。
一种自然的想法,就是在调用operator new末尾处还原new_handler,这就需要保存之前的new_handler,为此,我们构造一个NewHandlerHolder类,像下面这样:
class NewHandlerHolder
{
private:
new_handler SavedHandler;
NewHandlerHolder(const NewHandlerHolder&);
NewHandlerHolder& operator= (const NewHandlerHolder&);
public:
explicit NewHandlerHolder(new_handler h) :SavedHandler(h){}
~NewHandlerHolder()
{
set_new_handler(SavedHandler);
}
};
这里有一个SavedHandler成员变量,它在NewHandlerHolder构造时确定具体的指向(其实就是指向系统默认的new_handler函数(即null),将拷贝构造函数与赋值运算符重载设置为private是为了防止出现拷贝的行为(在编译期就可以阻止),这点可以参照之前的条款。还要特别注意这里的析构函数,它调用了set_new_handler,将new异常的处理恢复成SavedHandler(其实就是null)。
这样我们重新整理一下Widget,如下:
class Widget
{
private:
static new_handler CurrentHandler;
public:
static new_handler set_new_handler(new_handler h) throw()
{
new_handler OldHandler = CurrentHandler;
CurrentHandler = h;
return OldHandler;
}
static void* operator new(size_t size)
{
NewHandlerHolder h(Widget::set_new_handler(CurrentHandler));
return ::operator new(size);
}
};
new_handler Widget::CurrentHandler = 0;
为了返回系统默认的new_handler,我们在set_new_handler处理完之后,进行了旧handler的返回,同时在operator new的调用中进行了NewHandlerHolder的包装,这样在return之后,h会自动调用析构函数,恢复成默认的new_handler。
到这一步,本条款的重要内容已经说完了,但为了避免重复劳动,即为每一个需要重定义new_handler的类都写一份set_new_handler和operator new,书上在最后对之进行了封装,其实就是将Widget专门作为这两个函数(set_new_handler和operator new)的类,然后将需要自行处理new_handler的类public继承于Widget即可。
最后总结一下:
set_new_handler允许客户指定一个函数,在内存分配无法获得满足时被调用。