STL 源码阅读笔记-类型萃取(Traits)
简介
Traits,是一种约定俗称的技术方案,在STL中广泛被使用。常常被用于使不同的类型可以用于相同的操作 , 或者针对不同类型提供不同的实现。
笔记基本是根据《STL源码剖析》上第三章的内容进行总结。
抛出问题
在算法中使用迭代器的时候,会用到迭代器的相关类型。假设现在需要声明一个变量,这个变量的类型是“迭代器所指向的对象的类型”(这个属于迭代器相关类型之一),那么应该怎么做?
function template的解决方式
利用function template的方式可以做到这一点
template<class I, class T>
void func_imp(I iterator, T t){
T tmp; // 这个tmp就是迭代器指向的类型
// ........ 其他操作
}
template<class I>
void func(I iterator){
func_imp(I, *I);
}
int main(){
func(&100);
}
虽然利用function template的方式解决了之前的问题,但存在一些其他问题:
- “迭代器所指向的对象的类型”只是迭代器相关类型中的其中一种,而其他相关类型并不能完全按这种方式生成。这个问题后续会有更详细的解释。
- 如何在func中返回 “迭代器所指向的对象的类型”
Traits编程技法
我们把“迭代器所指向的对象的类型”称为value_type, 如何将value_type在func中返回是我们的问题。
声明内嵌类型可以解决上述问题
template <class T>
class MyIter{
typedef T value_type;
T* ptr;
}
template<class I>
typename I::value_type
func(I iterator){
return *I;
}
MyIter<int> it();
it->ptr = new int(10);
func(it);
假设我们传入的迭代器对象中有一个内嵌的value_type,那么我们就可以在func的返回值上填上I::vlaue_type,这样就返回正确的类型了。但这样的话原生指针类型就有问题了,因为原生指针是不存在value_type的。
但我们可以通过偏特化的方式,专门为原生类型的指针编写一个函数,这样在调用的时候,编译器就会有限匹配特化后的版本
template<class I>
I func(I* iterator){
return *I;
}
有了上面的思路,我们可以更进一步,编写一个专门获取value_type的模板类。
template<class I>
struct iterator_traits{
typedef typename I::value_type value_type;
}
template<class I>
struct iterator_traits<I *>{
typedef I value_type;
}
这样对于之前的func内容就可以变成
template<class I>
typename iterator_traits<I>::value
func(I iterator){
return *I;
}
这样返回值类型就统一了,不必额外特化一个新的模板函数。
迭代器的相关类型
迭代器中会有5个常用的相关类型:
- value_type 表示迭代器所指对象的类型
- pointer 表示迭代器所指对象的指针类型
- reference 表示迭代器所指对象的引用类型
- difference_type 表示两个迭代器之间的距离的类型
- iterator_category 表示迭代器的类型,这个类型只会有5类。
接上之前function template的存在问题1,这5个相关类型并不是每个都可以像func template中获取value_type那样获取到的对应的类型的。
iterator_category
iterator_category 分为5类:
- input_iterator 迭代器对象不允许被外界改变,相当于只读
- output_iterator 迭代器对象只写
- forward_iterator 迭代器对象可以读写
- bidirectional_iterator 迭代器对象可双向移动
- random_access_iterator 前三种迭代器对象支持operator ++, 第四种额外支持 operator --, 而第五种支持 +n, -n 这种操作。
Traits能将运行期的行为变成编译期
假设我们有一个函数func,他对不同的迭代器类型有不同的操作
template<class I>
void func(I& i){
if(I::iterator_category == forward_iterator)
func_forward_iterator(i);
elif(I::iterator_category == bidirectional_iterator )
func_bidirectional_iterator(i);
elif(I::iterator_category == random_access_iterator )
func_random_access_iterator(i);
}
虽然这样也能运行,但这个方式是在运行的时候通过判断类型的方式来决定执行的函数,会影响运行时效率。
Traits能够将这个行为提前到编译期,让编译器在编译的时候就选择好正确的执行版本
struct input_iterator_tag {};
struct output_iterator_tag {};
struct forward_iterator_tag : public input_iterator_tag {};
……
// 继承能满足上述迭代器分类关系
template<class I>
void func(I& i)
{
typedef typename Iterator_traits<Iterator>::iterator_category category; //tratis 提取出对应的category
__func(i, category()); // 各型别的重载
}
template<class I>
__func(I& i, input_iterator_tag ){
//func_forward_iterator(i) 的实现
}
template<class I>
__func(I& i, bidirectional_iterator ){
//func_bidirectional_iterator(i) 的实现
}
……
通过tratis 提取出对应的category以及重载不同参数的__func函数,就可以在编译期确定具体调用的函数。
更多例子
- std::advance
- std::distance
总结
trais这个技巧大量运用在STL中,它利用的是内嵌类型和编译器template参数推导等特性,实现对自定义数据和编译器内置数据的统一,同时带来的好处还有统一外部接口,降低代码冗余、提高运行时效率等等。
参考:
- 《STL源码剖析》
- https://www.cnblogs.com/Tonarinototoro/p/14037845.html
- https://www.cnblogs.com/mangoyuan/p/6446046.html