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
這個概念,到初步入門的全過程,耗時兩個星期......雖然后續可能有些例子還要補充,但感覺這篇科普文寫到這也算是有頭有尾,可以暫時先發出來了,就這樣。