Traits出现的契机
Traits,可以解释为特征萃取技术,即提取被传入对象对应的返回类型,以STL为例,其容器和算法是分开实现的,两者通过迭代器链接(算法通过迭代器访问容器中的元素而不用关心容器具体的实现细节),对算法而言,传入的容器类型是透明的,所以就需要加一层Traits封装来根据细节调用合适的方法。为了让文章的目的更明确,专注于实现一个简化的Tensor,只有一个模板参数Tensor内元素的数据类型,并默认Tensor为二维的:
- 如下面一段代码所示,定义的func模板函数可以接收任意类型的参数作为输入。
把Tensor类型当做参数传入,希望在func函数中能获取到Tensor中存放元素的类型,毕竟不想针对每一个元素类型都声明一个func函数吧 ,比较惨的是虽然C++中的RTTI typeid()可以获取型别的名称,但是无法拿它用来声明对象,也没有所谓的typeof之类的操作,所以这一步看上去要略费周折。template<typename T> void func(I iter){ // Do something...... } int main(){ Tensor<int> mat(10, 10); func(mat); }
Traits的出现就是为了解决这类问题——得到“传入对象”其内部元素的类别。
template的参数推导
实例化一个函数模板时,必须知晓每个模板实参,但不必指定每个模板实参,编译器会从函数实参推导出缺失的模板实参。
详细参考官方文档。
后续推出相关博客再补链接....挖一个坑。
官方文档中给出的第一个例子可以帮助对参数推导这个概念有一个初步的理解。
template<typename To, typename From> To convert(From f);
void g(double d)
{
int i = convert<int>(d); // 调用 convert<int, double>(double)
char c = convert<char>(d); // 调用 convert<char, double>(double)
int(*ptr)(float) = convert; // 实例化 convert<int, float>(float)
}
就单一针对这个例子有以下几点:
- 例子会用模板实参d对模板函数形参f进行替换,并通过实参的类型对形参类型进行推导,得到在这次实例化中模板参数对应的值。
- 参数推导只适用于参数,并不能对返回值进行推导,这就是在实例化convert函数时<>内需传入内容的原因。
利用template的参数推导机制,可以得到上述问题的一个初步解决方法:
template<typename I, typename T>
void func_helper(I iter, T t){
T tmp; // tmp变量的类型和Tensor中元素的类型一致
}
template<typename I>
void func(I iter){
func_helper(iter, *iter); // 利用参数推导类型
}
int main(){
// 首先Tensor总要有类似指针的东西可以访问到其内部存储的元素
// 不妨假设已通过某种手段得到了其内部存储的元素
int value = ....; // 从Tensor中取值的操作
func(&value);
}
我尽量试着说清楚这个流程:
- 在main中不管利用Tensor中定义的什么操作(这个不是关注的实现点),得到了Tensor中存储的某个数据value。
- 把value作为参数传入
func
,将默认进行参数推导,此时I的类型为int
。 - 在
func
函数中调用了helper
函数,其传入参数iter
和*iter
会对func_helper
中的参数类型进行默认推导,得到I的类型为int*
,T的类型为int
。 - 看看这样就最终得到了我们希望看到的结果,T代表的就是Tensor中存放元素的类型了。
那问题解决了是不是就可以收拾收拾电脑回去睡觉了,然而在模板参数推导一开始的那个例子中总结出的tip中就提到,参数推导不适用于对返回值进行推导,具体看下面这个例子:
template<typename I, typename T>
void func_helper(I iter, T t){
T tmp; // tmp变量的类型和vector中元素的类型一致
}
template<typename I>
?? func(I iter){
return *iter;
}
int main(){
// 还是通过某些透明的手段先得到Tensor中某个数据元素
int value = ...;
func(&value);
}
上面代码虽然是跑不通的但是可以很好的说明问题,对于??部分无从下手,我也不知道该怎么定义它才能以value的类型作为函数的返回类型。
声明内嵌类别
于是乎,就需要寻找新的解决办法,嗯比较粗暴的方法是在定义Tensor
时直接把类型当参数返回不就行了,好像看到了希望,撸个袖子写一下例子:
template<typename TScalarType>
class Tensor{
typedef TScalarType value_type;
// 其他的一些定义
// ...
};
template<typename I>
typename I::value_type
func(I iter){
// 可以随便返回Tensor中的元素
return ...;
}
int main(){
Tensor<int> mat(8,8);
func(mat);
}
在上面构造的例子中,直接将Tensor实例作为参数,可推导出模板参数I
的为Tensor
,由于Tensor
结构体中定义的value_type
保存Tensor的元素类型,直接用它作为函数func
的返回值即可。附注:I::value_type
作为Dependent Name
其前面必须要加typename
关键字,对于Dependent Name
的定义,参见官方文档,挖第二个坑,后面推文章再补博文链接。
看着好像解决了问题,写到这里是不是可以下楼吃饭了(好吧,暴露了这一篇文章我写了N天的事实....)。然而,上面的解决方法对原生指针不可用,诶又出现了什么奇奇怪怪我看不懂的词,所谓原生指针简单的来说就是int*, float*
之类的指针,让我思考一下感觉这里应该不用挖坑吧,就先贴个参考链接。很明显的是,我们不可能对这种指针加一个value_type
参数吧,还是补个例子比较清楚:
template<typename I>
typename I::value_type
func(I iter){
return ...;
}
int main(){
int value = 10;
func(&value);
}
上面这个例子中int*
类型的参数不可能拥有value_type这个类型,哎于是乎,又一个我看不懂的词偏特例化就出现了。好吧我看了看,既然有偏特例化这个概念,和它相对的就有全特例化,所以抱着先理解全特例化有助于理解偏特例化的蜜汁自信的想法,就先去看了什么是全特例化。完了,为了更好的讲清楚什么是全特例化,就打算追根溯源的更彻底一点,什么是特例化,毕竟每一个名词都不可能是凭空出现的,一般情况下是出现了某个问题,针对这个问题给出解决方案,提出某个专有名词对这个解决方案备案,后面再出现新的问题,以此循环,整个事情才能得到发展。
特例化
前面说了模板,模板不是用的挺好的,为啥还要提出一个特例化,举一个比较生活化的例子,上学的时候,老师会说XX班的同学比较听话,但是听话可以概括XX班的大部分同学,一个班总有一个很有想法的同学吧,所以这一两个有想法的同学就需要用其他词描述,就可以说这一两个同学需要被特例化的表现。现在如果把XX班当做是一个类,为了该类能代表该班所有同学,就需要支持一般性和特殊性,总不能说把特殊的同学排除该班之外吧。所以以此类比,特例化模板可以概括为:
“ 针对某个特殊的类型,在定义的时候给出不同于一般数据类型的逻辑实现,但是在使用时,这个特殊性对用户是透明的,遵循统一的模板,把选择权交给编译器。”
全特例化
有了特例化的概念,全特例化就比较容易理解,诶又要开个坑的节奏,为了不把这篇科普性文章的主题带跑,毕竟我只是想先梳理一下整个知识线,就先贴个官网链接挖个坑以后补。不过还是要先简单说一下全特例化,要不怎么继续后面的部分。
直接举个例子来说
// 类模板
template <typename T1, typename T2>
class A{
T1 data1;
T2 data2;
};
// 函数模板
template <typename T>
T max(const T lhs, const T rhs){
return lhs > rhs ? lhs : rhs;
}
// 全特化类模板
template <>
class A<int, double>{
int data1;
double data2;
};
// 全特化函数模板
template <>
int max(const int lhs, const int rhs){
return lhs > rhs ? lhs : rhs;
}
针对这个例子有以下几点:
- 当A的实例化对象传入的模板参数类型是int和double时,编译器会自动选择全特例化类模板中的内容对该对象进行实例化。
- 当max函数的参数传入的类型是const int时,编译器会自动调用全特例化函数的内容。
偏特例化
对全特例化有了基本认识后,偏特例化就比较容易理解了,同样,偏特例化的出现肯定也是为了解决某个问题,如果在特例化模板的时候不想给出所有模板参数的具体类型,只想给出其中某个或某几个参数的类型怎么办?诶不可避免的又挖一个坑,贴个官方链接。
给一个偏特例化的例子:
template <typename T2>
class A<int, T2>{
...
};
需要说明的是,偏特例化只针对类模板,不允许对函数模板进行偏特例化,挖个坑思考,为什么不支持对函数模板偏特例化?
既然理解了基本概念,接下来就要想一想偏特例化是怎么解决上上上面说的原生指针问题。
很快能类比想到,把原生指针类型当做特例处理不就行了,增加一个萃取器,对传入的原生指针和自定义的类分开进行处理:
template<typename TScalarType>
class Tensor{
typedef TScalarType value_type;
// 其他的一些定义
// ...
};
template <typename I>
struct iterTensor_traits {
typedef typename I::value_type value_type;
};
template <typename T>
struct iterTensor_traits<T*> {
typedef T value_type;
};
template<typename I>
inline typename iterTensor_traits<I>::value_type
func(I iter){
return .... ;
}
int main(){
int value = 10;
func(&value);
Tensor<int> mat(8,8);
func(mat);
}
我尝试把这个流程整个说清楚,func
会先把main
函数中带入的实参I传入萃取器iterTensor_traits
中,若传入的是原生指针int*
,则会自动匹配带<T*>
的偏特例化版本,这样value_type
的值为int
;其他传入参数则会寻找其内部定义的value_type
属性。
以上除了一些坑以后要补,感觉已经把traits
这个概念基本说清楚了。
以下要说的可以当做是一些扩展,就是在STL
源码中是怎么使用Traits
的。
STL中的Traits
STL
的设计感觉要仔细的把每个细节都搞懂需要费很大的功夫,以下只是我的一些比较浅层次的想法。
前面一直在纠结iterator
到底是什么,它和value_type
,different_type
等的关系又是什么,看了一些博客,对它有了比较初步的理解:
一直很困惑STL
是怎么定义iterator
的,导致有点舍本逐末,不如先抛开iterator
这个问题,因为它本身暂时可以理解为指向容器内元素的指针,和int*
等的原生指针类似。Traits
其实更多的对如何获得int*
指针指向的元素类型(也就是int
),以及容器中存放的元素类型感兴趣(像value_type
,different_type
等就可以直接理解为容器迭代器iterator
的内置型别)
还有一些涉及到迭代器的iterator_category
还包括5个类型,怎么避免写大规模判断代码,在执行时才决定使用哪个版本,转而在编译期间就决定调用函数的版本,还有一堆坑要填.....不过以上记录了我从新接触Traits
这个概念,到初步入门的全过程,耗时两个星期......虽然后续可能有些例子还要补充,但感觉这篇科普文写到这也算是有头有尾,可以暂时先发出来了,就这样。