在理解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閱讀 228,197評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,415評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,104評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,884評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,647評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,130評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,208評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,366評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,887評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,737評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,939評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,478評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,174評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,586評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,827評論 1 283
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,608評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,914評論 2 372

推薦閱讀更多精彩內容