第二章 Traits和类型操作(Type Manipulation)
我们希望第一章关于数学的例子不会以一个错误的印象误导你,使你认为大部分元编程本质上都是数字计算。实际上,编译期数字计算只是很少的情况。在这一章,你会学习什么是递归:元编程进行类型计算。
2.1类型关联
在C++中,可以在编译时期操作的实体(entity)称为元数据(metadata),元数据大体上分成两个种类:类型(type)和非类型(non-type)。巧合的是,所有类型的元数据都可以作为模板参数。第一章使用的整型常量值属于非类型,非类型还包括几乎所有其它可以在编译期确定值的东西:其它整数类型,枚举,指向函数、全局对象的指针和引用以及成员指针[1]。
[1]C++
标准还允许模板作为模板参数。如果这对你来说并不难理解,这些参数在标准中被解释为“作为可描述的目的的类型(
as types for descriptive purposes
)。”模板不是类型,因此,不能传递给另外一个需要类型的模板。
很容易能够想象在一些非类型元数据上进行计算,但是也同样有方法使用类型进行计算也许会使你感到惊讶。为了使你意识到那究竟是什么,我们准备来看看C++标准库中一个最简单的算法:iter_swap。Iter_swap的基本功能是通过两个迭代器交换它们所指向的对象的值,它看起来像这样:
template
void iter_swap(ForwardIterator1 i1, ForwardIterator2 i2)
{
T tmp = *i1;
*i1 = *i2;
*i2 = tmp;
}
如果此时你对T的来源提出疑问,说明你眼光很锐利。没错,T没有被定义,因此如果我们这样写,iter_swap不会被编译通过。非正式的,当然,T是迭代器解引用时得到的类型,在C++标准中提到的迭代器的value type。但是我们怎么命名这个类型呢?
2.1.1使用直接的方式
如果你已经知道标准库作者选择的答案,我们建议你暂时忘记它。我们有两个更深层次的结论要做。想像我们正在自己实现标准库并且选择操作迭代器的方法。我们正准备写很多的算法,这些算法中的大部分都需要在迭代器类型和值类型之间建立关联,我们可以要求所有的迭代器实现都提供一个内嵌类型叫做value_type,我们可以像下面这样直接访问:
template
void iter_swap(ForwardIterator1 i1, ForwardIterator2 i2)
{
typename // (参考语言记录)
ForwardIterator1::value_type tmp = *i1;
*i1 = *i2;
*i2 = tmp;
}
C++
语言记录
C++
标准要求在使用依赖名称(
dependent name
)时需要使用
typename
关键字以表明是一个类型。
ForwardIterator1::value_type
可能是或者不是一个类型名称,依赖于传递的ForwardIterator1参数。更多关于typename参考附录B。
创建类型关联是一个极佳的策略,但还不是很通用。特别的,C++中的迭代器是模拟指针实现的,因此,一个一般的指针应该是一个合法的迭代器。不幸的是,指针不能拥有内嵌类型,只有类类型可以享有该特权:
void f(int* p1, int* p2)
{
iter_swap(p1,p2); // 错误: int* 没有 'value_type'成员
}
2.1.2 增加一层间接
引入一个间接层可以使我们解决任何问题。
Butler Lampson
Lampson的思想在程序设计领域如此适用以至于Andrew Koenig[2]非常欣赏的称其为“软件工程的基本思想(the Fundamental Theorem of Software Engineering (FTSE)”。我们可能不能为所有的迭代器增加一个::value_type的内嵌类型,但是我们可以把它加到一个以迭代器类型作为模板参数的模板中。在标准库中,这个模板称作iterator_traits,有一个简单的形式:
[2] Andrew Koenig
是
Accelerated C++
的合作者和
C++
标准的项目编辑。在几乎任何一本
Bjarne Stroustrup
的书中,都对他为
C++
多年的贡献进行了首肯和感谢。
template struct iterator_traits;
下面是它和iter_swap一起工作的情况:
template
void iter_swap(ForwardIterator1 i1, ForwardIterator2 i2)
{
typename
iterator_traits::value_type tmp = *i1;
*i1 = *i2;
*i2 = tmp;
}
iterator_traits之所以这样命名是因为它描述了它的参数的属性(或者特性(trait))。这里,被描述的特性是迭代器的五个关联类型:value_type,reference,pointer,difference_type和iterator_catagory。
trait模板的最重要的特色是它给了我们一种将信息和类型关联起来的非侵入(non-intrusively)方法。换句话说,如果你的一个爱争吵的同事Hector给了你一个看上去像迭代器的类型hand_off,它指向int类型,你可以在打乱组内和谐气氛的情况下为它指派一个value_type。所有你需要做的只是是为iterator_traits增加一个显示特化,然后当iter_swap访问Hector的迭代器的value_type时,会得到类型int:[3]
[3]
要简单回顾一下模板的特化和具现,可以参考本章后边的“细节”部分
namespace std
{
template <>
struct iterator_traits
{
typedef int value_type;
更多typedefs...
};
}
通过iterator_traits间接访问,一般的函数都可以使用统一的方法获取迭代器关联的类型,不管它是不是一个指针。
2.1.3 找到捷径
尽管特化是非常通用的机制,在类中增加内嵌类型还不是很方便。特化伴随很多成本:你可能不得不从当前工作的namespace切换到traits模板的namesapce,然后你不得不自己编写trait特化的代码,依靠敲击键盘并不是最有效率的方法,只有内嵌的类型定义才是真正有价值的东西。
经过仔细斟琢,标准库为迭代器的作者提供了一个使iterator_trait可以获取迭代器内嵌类型的捷径,即在迭代器中定义typedef的成员。主(primary)iterator_trait模板[4]到迭代器内部抽取它的成员类型。
[4]C++
标准将区别于局部或者显示特化模板的平凡模板声明和定义成为主
(primary)
模板。
template
struct iterator_traits {
typedef typename Iterator::value_type value_type;
...其它四个
typedefs
};
现在你看到“多一层间接”的运转:代替直接使用Iterator::value_type,iter_swap通过iterator_traits获取迭代器的value_type达到同样的效果。除非一些特化覆盖了主(primary)iterator_traits模板,iter_swap会得到和直接访问迭代器的内嵌类型一样的value_type。