在理解Traits时会涉及的知识点

Traits出现的契机

Traits,可以解释为特征萃取技术,即提取被传入对象对应的返回类型,以STL为例,其容器和算法是分开实现的,两者通过迭代器链接(算法通过迭代器访问容器中的元素而不用关心容器具体的实现细节),对算法而言,传入的容器类型是透明的,所以就需要加一层Traits封装来根据细节调用合适的方法。为了让文章的目的更明确,专注于实现一个简化的Tensor,只有一个模板参数Tensor内元素的数据类型,并默认Tensor为二维的:

  • 如下面一段代码所示,定义的func模板函数可以接收任意类型的参数作为输入。
    template<typename T>
    void func(I iter){
      // Do something......
    }
    int main(){
      Tensor<int> mat(10, 10);
      func(mat);
    }
    
    把Tensor类型当做参数传入,希望在func函数中能获取到Tensor中存放元素的类型,毕竟不想针对每一个元素类型都声明一个func函数吧 ,比较惨的是虽然C++中的RTTI typeid()可以获取型别的名称,但是无法拿它用来声明对象,也没有所谓的typeof之类的操作,所以这一步看上去要略费周折。

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_typedifferent_type等的关系又是什么,看了一些博客,对它有了比较初步的理解:
一直很困惑STL是怎么定义iterator的,导致有点舍本逐末,不如先抛开iterator这个问题,因为它本身暂时可以理解为指向容器内元素的指针,和int*等的原生指针类似。Traits其实更多的对如何获得int*指针指向的元素类型(也就是int),以及容器中存放的元素类型感兴趣(像value_typedifferent_type等就可以直接理解为容器迭代器iterator的内置型别)

还有一些涉及到迭代器的iterator_category还包括5个类型,怎么避免写大规模判断代码,在执行时才决定使用哪个版本,转而在编译期间就决定调用函数的版本,还有一堆坑要填.....不过以上记录了我从新接触Traits这个概念,到初步入门的全过程,耗时两个星期......虽然后续可能有些例子还要补充,但感觉这篇科普文写到这也算是有头有尾,可以暂时先发出来了,就这样。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,640评论 6 507
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,254评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,011评论 0 355
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,755评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,774评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,610评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,352评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,257评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,717评论 1 315
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,894评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,021评论 1 350
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,735评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,354评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,936评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,054评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,224评论 3 371
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,974评论 2 355

推荐阅读更多精彩内容