Effective C++ 条款 47、48、49

条款四十七:请使用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是否是内置类型)。

最后,我们来总结一下:

  1. Traits class使得类型相关信息可以在编译期可用,它们以template和template特化完成实现;

  2. 整合重载技术后,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,在运行期直接执行乘法即可。

元编程有何优点?

  1. 以编译耗时为代价换来卓越的运行期性能,因为对于产品级的程序而言,运行的时长远大于编译时长。

  2. 将原来运行期才能发现的错误提前到了编译期,要知道,错误发现的越早,代价越小。

元编程有何缺点?

  1. 代码可读性差,写起来要运用递归的思维,非常困难。

  2. 调试困难,元程序执行于编译期,不能debug,只能观察编译器输出的error来定位错误。

  3. 编译时间长,运行期的代价转嫁到编译期。

  4. 可移植性较差,老的编译器几乎不支持模板或者支持极为有限。

模板元编程TMP虽然有这么多的缺点,但它已经被证明是“图灵完全”的了,意思是它的威力大到足以计算任何事物。目前boost库中用到了一些TMP技术,但大部分项目还是因为TMP的一些缺点而没有广泛采用,所以这里我们只要略做了解即可。

下面总结一下:

  1. TMP可将工作由运行期转移到编译期,因而得以实现早期错误侦测或者更高的执行效率。

  2. 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允许客户指定一个函数,在内存分配无法获得满足时被调用。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值