文章背景
大多數C ++程式員由于其困惑的性質而遠離C ++模板, 反對模板的借口:
- 很難學習和適應,
- 編譯器錯誤是模糊的,而且很長,
- 不值得的努力,
承認模板很難學習,理解和適應, 然而,我們從使用模板中獲得的好處將超過負面影響, 有 比可以圍繞模板包裝的泛型函式或類要多得多, 我會說明他們,
從技術上講,C ++模板和STL(標準模板庫)是同級的, 在本文中,我只會介紹核心級別的模板, 本系列的下一部分將圍繞模板介紹更高級和有趣的內容,以及有關STL的一些專門知識,
目錄
語法句
- 功能模板
- 帶有模板的指標,參考和陣列
- 帶有功能模板的多種型別
- 功能模板-模板功能
- 顯式模板引數規范
- 功能模板的默認引數
類模板
- 具有類模板的多種型別
- 非型別模板引數
- 模板類作為類模板的引數
- 帶類模板的默認模板引數
- 類的方法作為功能模板
文末雜談
【零聲學院官方許可】2小時精通掌握《STL模板庫》技術
語法句
您可能知道,模板很大程度上使用尖括號:小于( < )和大于( > )運算子, 對于模板,它們總是以這種形式一起使用:
< Content >
哪里可以用Content:
class T/typename T- 資料型別,映射到
T - 整體規格
- 映射到上述規范的整數常量/指標/參考,
對于點1和2,符號 T不過是某種資料型別,它可以是任何資料型別-基本資料型別( int, double等)或UDT,
讓我們跳到一個例子, 假設您撰寫了一個輸出數字兩倍(兩倍)的函式:
void PrintTwice(int data)
{
cout << "Twice is: " << data * 2 << endl;
}
可以稱為傳遞一個 int:
PrintTwice(120); // 240
現在,如果要列印a的兩倍 double,則可以將此函式多載為:
void PrintTwice(double data)
{
cout << "Twice is: " << data * 2 << endl;
}
有趣的是,型別 ostream (該 型 的 cout物件)具有用于多個多載 operator << -適用于所有基本資料型別, 因此,相同/相似的代碼對 int和都適用 double,并且我們的 不需要更改 PrintTwice多載 -是的,我們只是 復制粘貼了 它, 如果我們使用 printf-functions之一,則這兩個多載看起來像:
void PrintTwice(int data)
{
printf("Twice is: %d", data * 2 );
}
void PrintTwice(double data)
{
printf("Twice is: %lf", data * 2 );
}
這里的關鍵是不是 cout還是 print要在控制臺上顯示,但有關代碼-這是 絕對相同的 , 這是 之一 我們可以利用C ++語言提供的常規功能的眾多情況 :模板!
模板有兩種型別:
- 功能模板
- 類模板
C ++模板是一種編程模型,它允許 將 插入 任何資料型別 到代碼(模板代碼)中, 沒有模板,您將需要為所有必需的資料型別一次又一次地復制相同的代碼, 顯然,如前所述,它需要代碼維護,
無論如何,這是 的 簡化版 PrintTwice使用模板 :
void PrintTwice(TYPE data)
{
cout<<"Twice: " << data * 2 << endl;
}
在此,實際 型別 的 TYPE將被推斷通過根據傳遞給函式的引數的編譯器(確定), 如果 PrintTwice被稱為 PrintTwice(144);這將是一個 int,如果你通過 3.14這個功能, TYPE就可以推斷為 double型別,
您可能會感到困惑 TYPE,即編譯器將如何確定這是一個函式模板, 是否在 TYPE使用 定義了型別 typedef某處 關鍵字 ?
不,我的孩子! 在這里,我們使用關鍵字 template讓編譯器知道我們正在定義函式模板,
功能模板
這是 模板 函式 PrintTwice:
template<class TYPE>
void PrintTwice(TYPE data)
{
cout<<"Twice: " << data * 2 << endl;
}
第一行代碼:
template<class TYPE>
告訴編譯器這是一個 功能模板, 的實際含義 TYPE將由編譯器根據傳遞給此函式的引數推匯出, 這里的名稱 TYPE稱為 模板型別形參 ,
例如,如果我們將該函式稱為:
PrintTwice(124);
TYPE將被編譯器替換為 int,并且編譯器 實體 將該模板函式 化為:
void PrintTwice(int data)
{
cout<<"Twice: " << data * 2 << endl;
}
并且,如果我們將此函式稱為:
PrintTwice(4.5547);
它將另一個實體化為:
void PrintTwice(double data)
{
cout<<"Twice: " << data * 2 << endl;
}
這意味著,在您的程式中,如果 呼叫 ,則 PrintTwice使用 函式 int和 double引數型別 兩個 編譯器將生成此函式的 實體:
void PrintTwice(int data) { ... }
void PrintTwice(double data) { ... }
是的,代碼是重復的, 但是這兩個多載是由編譯器而不是程式員實體化的, 真正的好處是您不必 也不必 也不必 復制粘貼 相同的代碼, 為不同的資料型別手動維護代碼, 為稍后出現的新資料型別撰寫新的多載, 您只需要提供 的 模板 函式 ,其余的將由編譯器管理,
由于現在有兩個函式定義,因此代碼大小也會增加, 代碼大小(在二進制/匯編級別)將幾乎相同, 實際上,對于 N 個資料型別, N 將創建 個相同函式(即多載函式)的實體, 如果實體化的函式相同,或者函式主體的某些部分相同,則存在高級的編譯器/聯結器級別優化,可以在某種程度上減小代碼大小, 我現在不討論它,
但是,積極的一面是,當您手動定義 N個 不同的多載(例如 N=10)時, 這 N個 無論如何都將對 不同的多載進行編譯,鏈接和打包為二進制檔案(可執行檔案), 但是,使用模板, 只有 所需的函式實體化才能進入最終可執行檔案, 使用模板,函式的多載副本可能少于N,并且可能超過N-但恰好是所需副本的數量-不少!
另外,對于非模板實作,編譯器必須編譯所有這N個副本-因為它們在您的源代碼中! 當您 附加 模板 使用通用函式 時,編譯器將僅針對所需的資料型別集進行編譯, 這基本上意味著,如果不同資料型別的數量小于 則編譯會更快 N, ,
這將是一個完全有效的論據,即編譯器/聯結器可能會進行所有可能的優化,以從最終映像中洗掉未使用的非模板函式的實作, 但是,再次,請理解編譯器必須 編譯 所有這些多載(用于語法檢查等), 使用模板,僅針對所需的資料型別進行編譯-您可以將其稱為“ 按需編譯 ”,
現在只有純文字內容! 您可以回傳并再次閱讀, 讓我們繼續前進,
現在,讓我們撰寫另一個函式模板,該模板將回傳給定數字的兩倍:
template<typename TYPE>
TYPE Twice(TYPE data)
{
return data * 2;
}
您應該已經注意到,我使用的是typeName,而不是class,不需要,如果函式回傳某些內容,則不需要使用typeName關鍵字,對于模板編程,這兩個關鍵字非常相似,有兩個關鍵字用于同一目的是有歷史原因的,我討厭歷史,
但是,在某些情況下,您只能使用較新的關鍵字-TypeName,(當特定型別在另一個型別中定義,并且依賴于某個模板引數時-讓我們將此討論推遲到另一個部分),
繼續前進,當我們將此函式呼叫為:
cout << Twice(10);
cout << Twice(3.14);
cout << Twice( Twice(55) );
將生成以下函式集:
int Twice(int data) {..}
double Twice(double data) {..}
在上面截取的第三行代碼中,呼叫了兩次-第一次呼叫的回傳值/型別將是第二次呼叫的引數/型別,因此,這兩個呼叫都是int型別(因為引數型別和回傳型別是相同的),
如果模板函式是針對特定資料型別實體化的,則編譯器將重用相同函式的實體-如果針對相同資料型別再次呼叫該函式,這意味著,無論在代碼中的何處,您都可以使用相同型別的函式模板來呼叫函式模板-在相同的函式中,在不同的函式中,或者在另一個源檔案(相同的專案/構建)中的任何位置,
讓我們撰寫一個回傳兩個數字相加的函式模板:
template<class T>
T Add(T n1, T n2)
{
return n1 + n2;
}
首先,我只是將模板型別引數的name-type替換為符號T,在模板編程中,您通常會使用T-但這是個人選擇,最好使用反映型別引數含義的名稱,這樣可以提高代碼的可讀性,此符號可以是遵循C++語言中變數命名規則的任何名稱,
其次,我為兩個引數(n1和n2)重用了模板引數T-,
讓我們稍微修改一下Add函式,該函式將把加法結果存盤在區域變數中,然后回傳計算值,
template<class T>
T Add(T n1, T n2)
{
T result;
result = n1 + n2;
return result;
}
很容易解釋,我在函式體中使用了型別引數T,您可能會問(您應該):“當編譯器試圖編譯/決議函式add時,它如何知道結果的型別?”
那么,當查看函式模板體(Add)時,編譯器不會看到T(模板型別引數)是否正確,它只需檢查基本語法(如分號、關鍵字的正確使用、匹配的大括號等),并報告這些基本檢查的錯誤,同樣,它依賴于編譯器來編譯它如何處理模板代碼-但是它不會報告任何由于模板型別引數而導致的錯誤,
為了完整起見,我要重申,編譯器不會檢查(目前僅與函式添加相關):
T具有默認建構式(因此T result;有效)T支持使用operator +(這樣才<code>n1+n2有效)T具有 可訪問的 副本/移動建構式(因此該return陳述句成功)
本質上,編譯器必須分兩個階段編譯模板代碼:一次進行基本語法檢查; 稍后對 每個實體化 函式模板的 -它將對模板資料型別執行實際的代碼編譯,
如果您不完全理解這兩個階段的編譯程序,那完全可以, 閱讀本教程時,您將獲得堅定的理解,然后稍后再閱讀這些理論課程!
也許干巴巴的文字看起來有些枯燥,如果單看文字不是很容易消化的話,可以進群973961276來跟大家一起交流學習,群里也有許多視頻資料和技術大牛,配合文章一起理解應該會讓你有不錯的識訓,
推薦一個不錯的c/c++ 初學者課程,這個跟以往所見到的只會空談理論的有所不同,這個課程是從六個可以寫在簡歷上的企業級專案入手帶領大家學習c/c++,正在學習的朋友可以了解一下,
帶有模板的指標,參考和陣列
首先是一個代碼示例(不用擔心-這是簡單的代碼段!):
template<class T>
double GetAverage(T tArray[], int nElements)
{
T tSum = T(); // tSum = 0
for (int nIndex = 0; nIndex < nElements; ++nIndex)
{
tSum += tArray[nIndex];
}
// Whatever type of T is, convert to double
return double(tSum) / nElements;
}
int main()
{
int IntArray[5] = {100, 200, 400, 500, 1000};
float FloatArray[3] = { 1.55f, 5.44f, 12.36f};
cout << GetAverage(IntArray, 5);
cout << GetAverage(FloatArray, 3);
}
對于第一個電話 GetAverage,在那里 IntArray通過,編譯器將實體化這個功能:
double GetAverage(int tArray[], int nElements);
和類似的 float, 型別,因此保留回傳 double由于數字的平均值在邏輯上適合 double資料 型別, 請注意,這僅是本示例-所包含的實際資料型別 T可能是一個類,可能無法轉換為 double,
您應該注意,函式模板可能具有模板型別引數以及非模板型別引數, 它不需要具有功能模板的所有引數即可從模板型別到達, int nElements是這樣的函式引數,
顯然,注意和理解,模板型別引數只是 T,而不是 T*或 T[] -編譯器是足夠聰明來推斷型別 int從 int[](或 int*), 在上面給出的示例中,我已將其用作 T tArray[]函式模板的引數,并且 實際資料型別 T可以從中智能地確定的 ,
通常,您會碰到過,并且還需要使用初始化,例如:
T tSum = T();
首先,這不是模板特定的代碼-它屬于C ++語言本身, 從本質上講,這意味著:呼叫 的 默認建構式 此資料型別 , 對于 int,它將是:
int tSum = int();
有效地使用初始化變數 0, 同樣,對于 float,它將將此變數設定為 0.0f, 盡管尚未涵蓋,但是如果用戶定義的型別別來自 T,它將呼叫該類的默認建構式(如果可呼叫,否則相關的錯誤), 如您所知,它 T可能是任何資料型別,我們不能 初始化 tSum簡單地使用整數零( 0)進行 , 實際上,它可能是某個字串類,它使用空字串( 對其進行初始化 "") ,
由于模板型別 T可以是任何型別,因此它也必須 += operator可用, 正如我們所知,它是可用于所有的基本資料型別( int, float, char等), 如果實際型別(用于 T)沒有 +=可用的運算子(或任何可能性),則編譯器將引發一個錯誤,即實際型別不具有該運算子,或任何可能的轉換,
同樣,型別 T必須能夠將其自身轉換為 double(請參見以下 return陳述句), 稍后,我將掩蓋這些棘手的問題, 為了更好地理解,我 重新列出了所需的 支持 從型別中 T(現在僅適用于 GetAverage功能模板):
- 必須具有 可訪問的 默認建構式,
- 必須具有
+= operator可通話性, - 必須能夠將自身轉換為
double(或等效值),
對于 GetAverage 功能模板原型,可以使用 T*代替 T[],并且含義相同:
template<class T>
GetAverage(T* tArray, int nElements){}
由于呼叫方將傳遞一個陣列(分配在堆疊或堆上)或型別為的變數的地址 T, 但是,您應該知道,這些規則屬于C ++的規則集,而并非專門來自模板編程!
前進, 讓我們問 演員 “ 參考 ”來為模板編程 輕彈 , 現在,不言而喻,您只是將其 T& 用作基礎型別的函式模板引數 T:
template<class T>
void TwiceIt(T& tData)
{
tData *= 2;
// tData = tData + tData;
}
它計算引數的兩倍值,并將其放入相同引數的值中, 您可以簡單地稱呼它為:
int x = 40;
TwiceIt(x); // Result comes as 80
請注意,我過去常常 operator *=兩次爭論 tData, 您也可以使用 operator + 以獲得相同的效果, 對于基本資料型別,兩個運算子均可用, 對于型別別,不是兩個運算子都可用,您可能會要求該類實作必需的運算子,
我認為, 是合乎邏輯的 operator +按班級定義 , 原因很簡單- 這樣做 T+T對于大多數UDT(用戶定義型別)而言, 更合適 *= operator, 問問自己:如果某些類 這意味著什么 String或 Date實作或被要求實作以下運算子, :
void operator *= (int); // void return type is for simplicity only.
在這一點上,你現在清醒地認識到模板引數型別 T可以推斷 T&, T*或 T[], 因此, 也是可行且非常 合理的 將 添加 const屬性 到要到達功能模板的引數 ,并且該引數不會被功能模板更改, 放輕松,它很簡單:
template<class TYPE>
void PrintTwice(const TYPE& data)
{
cout<<"Twice: " << data * 2 << endl;
}
觀察到我已經將模板引數修改 TYPE為 TYPE&,并且也添加 const了它, 很少或大多數讀者會意識到這種變化的重要性, 對于那些沒有的人:
- 該
TYPE型別的大小可能很大,并且將需要更多的堆疊空間(呼叫堆疊), 它包括double需要8個位元組*,某些結構或類的類,這將需要更多位元組保留在堆疊上, 從本質上講,這意味著-將創建給定型別的新物件,呼叫復制建構式,并將其放入呼叫堆疊中,然后在函式結尾處進行解構式呼叫,
參考(&)的添加避免了所有這些情況- 參考 傳遞同一物件的 , - 函式不會更改傳遞的引數,因此
const不會對其進行添加, 對于函式的呼叫者,它確保此函式(此處為PrintTwice)不會更改引數的值, 如果函式本身錯誤地嘗試修改( 內容,則還可以確保發生編譯器錯誤 constant )引數的
在32位平臺上,函式引數至少需要4個位元組,并且至少需要4個位元組, 這意味著一個
char或short將在呼叫堆疊中需要4個位元組, 例如,一個11位元組的物件需要12位元組的堆疊,
同樣,對于64位平臺,將需要8個位元組, 一個11位元組的物件將需要16個位元組, 型別的引數double將需要8個位元組,
在32位/ 64位平臺上,所有指標/參考分別占用4位元組/ 8位元組,因此 位平臺,傳遞double或double&對于64 意味著相同,
同樣,我們應該將其他功能模板更改為:
template<class TYPE>
TYPE Twice(const TYPE& data) // No change for return type
{
return data * 2;
}
template<class T>
T Add(const T& n1, const T& n2) // No return type change
{
return n1 + n2;
}
template<class T>
GetAverage(const T tArray[], int nElements)
// GetAverage(const T* tArray, int nElements)
{}
注意 不可能有參考并將其 const,除非我們打算回傳傳遞給函式模板的原始物件的參考(或指標),否則 添加到回傳型別, 以下代碼舉例說明了它:
template<class T>
T& GetMax(T& t1, T& t2)
{
if (t1 > t2)
{
return t2;
}
// else
return t2;
}
這就是我們利用回傳參考的方式:
int x = 50;
int y = 64;
// Set the max value to zero (0)
GetMax(x,y) = 0;
請注意,這只是出于說明目的,您很少會看到或撰寫此類代碼, 但是,如果回傳的物件是某個UDT的參考,則可能會看到這樣的代碼并且可能需要撰寫, 在這種情況下,成員訪問運算子點( .)或箭頭( ->)將跟隨函式呼叫, 無論如何,此函式模板回傳 的 參考 贏得大于競賽的物件 , 當然,這需要 operator >按type定義 T,
您應該已經注意到,我尚未添加 const任何兩個傳遞的引數, 這是必需的; 由于函式回傳型別的非常量參考 T, 曾經是這樣的:
T& GetMax(const T& t1, const T& t2)
在這些 return陳述句中,編譯器會抱怨 t1或 t2無法將其轉換為非常量, 如果我們 添加 const還將回傳型別也 為( const T& GetMax(...) ),則呼叫網站上的以下行將無法編譯:
GetMax(x,y) = 0;
由于 const物件無法修改! 您絕對可以在函式中或在呼叫站點中進行強制const /非const型別轉換, 但這是一個不同的方面,一個糟糕的設計和一個不推薦的方法,
帶有功能模板的多種型別
到目前為止,我只介紹了一種型別作為模板型別引數, 使用模板,您可能有多個模板型別引數, 就像這樣:
template<class T1, class T2, ... >
其中 T1和 T2是功能模板的型別名稱, 您可以使用任何其他特定的名稱,而不是 T1, T2, 需要注意的是的“使用 ...”上面并 沒有 意味著這個模板規范可以采取任何數量的引數, 僅說明模板可以具有任意數量的引數,
(與C ++ 11標準一樣,模板將允許可變數量的引數-但目前為止,這已經超出了主題,)
讓我們看一個使用兩個模板引數的簡單示例:
template<class T1, class T2>
void PrintNumbers(const T1& t1Data, const T2& t2Data)
{
cout << "First value:" << t1Data;
cout << "Second value:" << t2Data;
}
我們可以簡單地稱其為:
PrintNumbers(10, 100); // int, int
PrintNumbers(14, 14.5); // int, double
PrintNumbers(59.66, 150); // double, int
每個呼叫都需要為傳遞的第一種和第二種型別(或說是 單獨的模板實體化 推斷的 )使用 , 因此,編譯器將填充以下三個功能模板實體:
// const and reference removed for simplicity
void PrintNumbers(int t1Data, int t2Data);
void PrintNumbers(int t1Data, double t2Data);
void PrintNumbers(double t1Data, int t2Data);
認識到,第二和第三實體是不一樣的, T1并且 T2將推斷不同資料型別( int, double和 double, int), 編譯器將 不會 執行任何自動轉換,就像正常函式呼叫可能會執行的那樣- 一個采用的正常函式 int例如,可以傳遞 , short反之亦然, 但是對于模板,如果您通過 short-它是絕對的 short,不是(升級為) int, 因此,如果您傳遞( short, int),( short, short),( long, int)-這將導致 三個不同的實體化 PrintNumbers!的 ,
以類似的方式,函式模板可以具有3個或更多型別引數,并且它們每個都將映射到函式呼叫中指定的引數型別, 例如,以下功能模板是合法的:
template<class T1, class T2, class T3>
T2 DoSomething(const T1 tArray[], T2 tDefaultValue, T3& tResult)
{
...
}
Where T1指定呼叫者將傳遞的陣列型別, 如果未傳遞陣列(或指標),則編譯器將呈現適當的錯誤, 該型別 T2用作回傳型別以及通過值傳遞的第二個引數, 型別 T3作為參考(非常量參考)傳遞, 上面給出的此功能模板示例只是隨意選擇的,但它是有效的功能模板規范,
到目前為止,我已經詳細介紹了多個模板引數,但出于某種原因,我現在開始使用一個引數函式,這是有原因的,你很快就會明白的,
假設有一個函式( 非 模板化),它帶有一個 int引數:
void Show(int nData);
您將其稱為:
Show( 120 ); // 1
Show( 'X' ); // 2
Show( 55.64 ); // 3
- 呼叫 1 完全有效,因為函式接受
int引數,而我們正在傳遞120, - 呼叫 2 是有效的呼叫,因為我們正在傳遞
char,編譯器會將其提升為int, - 呼叫 3 將要求降級值-編譯器必須轉換
double為int,因此55將傳遞而不是55.64, 是的,這將觸發適當的編譯器警告,
一種解決方案是修改函式,使其采用 double,可以傳遞所有三種型別, 但這并不支持所有型別,并且可能不適合或轉換為 double, 因此,您可以使用適當的型別撰寫一組多載函式, 有了知識,現在,您將了解模板的重要性,并要求將其撰寫為功能模板:
template<class Type>
void Show(Type tData) {}
當然,假設所有現有的多載 Show都在做相同的事情,
好吧,你知道這個練習, 那么,導致我 辭職的新訊息是 什么?
好吧,如果您想傳遞 int給函式模板 Show,但希望編譯器像 一樣實體化 double傳遞通過 呢?
// This will produce (instantiate) 'Show(int)'
Show ( 1234 );
// But you want it to produce 'Show(double)'
截至目前,要求這件事似乎不合邏輯, 但是有充分的理由要求這種實體化,您很快就會理解和贊賞!
無論如何,首先要了解如何要求這樣荒唐的事情:
Show<double> ( 1234 );
實體化以下 模板函式 (如您所知):
void Show(double);
使用這種特殊的語法( Show<>()),您要求編譯器為 實體化 Show顯式傳遞的型別 函式,并要求編譯器 不要 按函式引數推斷型別,
功能模板-模板功能
重要! 之間有區別 函式模板 和 模板函式 ,
一個 函式模板 是括號周圍的函式體 template的關鍵字,這是不實際的功能,并不會 完全 由編譯器編譯,而不是通過鏈接的責任, 至少需要一個針對特定資料型別的呼叫來實體化它,并將其納入編譯器和聯結器的職責范圍, 因此,功能模板 Show的實體被實體化為 Show(int)或 Show(double),
一個 模板函式 ? 簡而言之,就是一個“函式模板的實體”,它是在您呼叫它時生成的,或者使它針對特定的資料型別實體化, 函式模板的實體實際上是有效的函式,
在編譯器和聯結器的名稱裝飾系統的保護下,功能模板的一個實體(又稱為模板功能)不是普通功能, 這意味著函式模板的一個實體:
template<class T>
void Show(T data)
{ }
對于模板引數 double,它 不是 :
void Show(double data){}
但實際上:
void Show<double>(double x){}
長期以來,我只是為了簡單而未發現這個問題,現在您知道了! 使用編譯器/除錯器找出函式模板的實際實體,并在呼叫堆疊或生成的代碼中查看函式的完整原型,
因此,現在您知道了這兩者之間的映射:
Show<double>(1234);
...
void Show<double>(double data); // Note that data=1234.00, in this case!
顯式模板引數規范
退一步(向上)到多模板引數討論,
我們有以下功能模板:
template<class T1, class T2>
void PrintNumbers(const T1& t1Data, const T2& t2Data)
{}
并具有以下函式呼叫,導致此函式模板的3個不同實體:
PrintNumbers(10, 100); // int, int
PrintNumbers(14, 14.5); // int, double
PrintNumbers(59.66, 150); // double, int
而且,如果您只需要一個實體-兩個引數都取用 double怎么辦? 是的,您愿意通過 int并讓他們晉升 double, 加上您剛付訓得的理解,您可以將此函式模板稱為:
PrintNumbers<double, double>(10, 100); // int, int
PrintNumbers<double, double>(14, 14.5); // int, double
PrintNumbers<double, double>(59.66, 150); // double, int
這只會產生以下 模板函式:
void PrintNumbers<double, double>(const double& t1Data, const T2& t2Data)
{}
從呼叫站點以這種方式傳遞模板型別引數的概念被稱為“ 顯式模板引數規范”,
為什么需要顯式型別說明? 好吧,有多種原因:
- 您只希望傳遞特定型別,而不希望編譯器 智能地 推斷 僅通過實際引數(函式引數) 一種或多種模板引數型別,
例如,有一個函式模板, max帶有 兩個 引數(僅通過 一個 模板型別引數):
template<class T>
T max(T t1, T t2)
{
if (t1 > t2)
return t1;
return t2;
}
您嘗試將其稱為:
max(120, 14.55);
這將導致編譯器錯誤,并指出template-type含糊不清 T, 您要讓編譯器從兩種型別中推斷出一種型別! 一種解決方案是更改 max模板,使其具有兩個模板引數-但您不是該功能模板的作者,
在那里使用顯式引數規范:
max<double>(120, 14.55); // Instantiates max<double>(double,double);
毫無疑問地注意到并理解,我僅對 傳遞了明確的規范 第一個 模板引數 ,第二個型別是從函式呼叫的第二個引數推匯出的,
- 當function-template采用template-type時,而不是從其function引數中獲取時,
一個簡單的例子:
template<class T>
void PrintSize()
{
cout << "Size of this type:" << sizeof(T);
}
您不能簡單地呼叫以下函式模板:
PrintSize();
由于此函式模板將需要模板型別引數規范,因此編譯器無法自動推導該模板, 正確的呼叫是:
PrintSize<float>();
將 實體化 PrintSize使用 float模板引數 ,
- 當函式模板具有不能從引數推匯出的回傳型別時,或者當函式模板沒有任何引數時,
一個例子:
template<class T>
T SumOfNumbers(int a, int b)
{
T t = T(); // Call default CTOR for T
t = T(a)+b;
return t;
}
這需要兩個 ints并將其求和, 盡管將它們 求和 int本身 是合適的,但是此函式模板提供了 機會 來計算 的和(使用 operator+呼叫者要求的任何型別 ), 例如,在中獲取結果 double,您可以將其稱為:
double nSum;
nSum = SumOfNumbers<double>(120,200);
最后兩個只是為了完整起見而簡化的示例,只是為了 您 提示 適合使用“顯式模板引數規范”,在更具體的場景中, 這種 顯式性 需要 ,并將在下一部分中進行介紹,
功能模板的默認引數
對于 讀者而言, 確實 了解模板領域中默認模板型別規范的 這 無關 與默認模板型別引數 , 無論如何,默認模板型別是功能模板所不允許的, 對于讀者來說,誰也 不會 知道這件事情,不用擔心-這一段是不是默認的模板型別規范,
如您所知,C ++函式可能具有默認引數, defaultness只能從右到左,這意味著,如果 ,則 第n個 第 要求 引數為默認值 ( n + 1 ) 個 也必須為默認值,依此類推直到函式的最后一個引數,
一個簡單的例子來說明這一點:
template<class T>
void PrintNumbers(T array[], int array_size, T filter = T())
{
for(int nIndex = 0; nIndex < array_size; ++nIndex)
{
if ( array[nIndex] != filter) // Print if not filtered
cout << array[nIndex];
}
}
您可能會猜到,此函式模板將列印所有數字,除了被第三個引數過濾掉的數字 filter, 最后一個可選的函式引數默認為type的default-value T,對于所有基本型別均表示為零, 因此,當您將其稱為:
int Array[10] = {1,2,0,3,4,2,5,6,0,7};
PrintNumbers(Array, 10);
它將被實體化為:
void PrintNumbers(int array[], int array_size, int filter = int())
{}
該 filter引數將呈現為: int filter = 0,
很明顯,當您將其稱為:
PrintNumbers(Array, 10, 2);
第三個引數獲取值 2,而不是默認值 0,
應該清楚地了解:
- 型別
T必須具有可用的默認建構式, 當然,函式主體可能會要求type的所有運算子T, - 默認引數必須 可以 推匯出來 從模板采用的其他非默認型別中 , 在
PrintNumbers例子中,型別array將有助于扣除filter,
如果不是,則必須使用顯式模板引數規范來指定默認引數的型別,
可以肯定的是,默認引數不一定是型別的默認值 T(請原諒), 這意味著,默認引數可能并不總是需要依賴于型別的default-constructor T:
template<class T>
void PrintNumbers(T array[], int array_size, T filter = T(60))
在這里,默認函式引數不為type使用default-value T, 相反,它使用value 60, 當然,這要求該型別 T具有可接受 copy-constructor int(for 60)的 ,
最后,本文這一部分的“功能模板”到此結束, 我認為您喜歡閱讀和掌握這些 基礎知識 功能模板的 , 下一部分將涵蓋模板編程的更多有趣方面,
類模板
通常,您將設計和使用類模板而不是功能模板, 通常,您使用類模板來定義一種抽象型別,該抽象型別的行為是通用的并且可重用,適應性強, 雖然有些文本將從給出有關資料結構的示例開始,例如鏈表,堆疊,佇列和類似的 容器 , 我將從非常簡單的非常簡單的示例開始,
讓我們看一個簡單的類,該類設定,獲取和列印存盤的值:
class Item
{
int Data;
public:
Item() : Data(0)
{}
void SetData(int nValue)
{
Data = nValue;
}
int GetData() const
{
return Data;
}
void PrintData()
{
cout << Data;
}
};
一個初始化 建構式 Data為 0,Set和Get方法的 ,以及一個用于列印當前值的方法, 用法也很簡單:
Item item1;
item1.SetData(120);
item1.PrintData(); // Shows 120
當然,沒有什么適合您的! 但是,當您需要對其他資料型別進行類似的抽象時,則需要復制整個類的代碼(或至少復制所需的方法), 它引起代碼維護問題,增加源代碼和二進制級別的代碼大小,
是的,我能感覺到我將要提到C ++模板的情報! 形式的同一類的模板化版本 類模板 如下:
template<class T>
class Item
{
T Data;
public:
Item() : Data( T() )
{}
void SetData(T nValue)
{
Data = nValue;
}
T GetData() const
{
return Data;
}
void PrintData()
{
cout << Data;
}
};
類模板宣告以與函式模板相同的語法開頭:
template<class T>
class Item
請注意,該關鍵字 class使用了兩次-首先用于指定模板型別規范( T),其次用于指定這是C ++類宣告,
要完全轉 Item成類模板,我更換的所有實體 int用 T, 我還使用 T()語法 呼叫的默認建構式 T,而不是硬編碼 0在建構式的初始值設定項串列中 (零), 如果您已 閱讀 功能模板 完整 部分,則知道原因!
而且用法也很簡單:
Item<int> item1;
item1.SetData(120);
item1.PrintData();
與函式模板實體化不同,函式模板的引數本身會幫助編譯器推斷模板型別的引數,而使用類模板,則必須顯式傳遞模板型別(在尖括號中),
上面顯示的代碼片段使類模板 Item實體化為 Item<int>, 當使用 創建具有不同型別的另一個物件時 Item類模板 :
Item<float> item2;
float n = item2.GetData();
這將導致 Item<float>實體化, 重要的是要知道,類模板- 兩個實體之間絕對沒有關系 Item<int>和的 Item<float>, 對于編譯器和聯結器,這兩個是不同的物體,或者說是不同的類,
使用type的第一個實體 int產生以下方法:
Item<int>::Item()建設者SetData和PrintData型別的方法int
類似地,型別第二次實體化 float將產生:
Item<float>::Item()建設者GetData方法float型別的
如您所知 Item<int>, Item<float>是兩種不同的類/型別; 因此,以下代碼將不起作用:
item1 = item2; // ERROR : Item<float> to Item<int>
由于兩種型別不同,因此編譯器將不會呼叫 可能的 默認賦值運算子, 如果 item1和 item2具有相同的型別(都使用 Item<int>),則編譯器會很高興地呼叫賦值運算子, 盡管對于編譯器來說,可以在 之間 int和 float 進行轉換,但是即使基礎資料成員相同,也不可能進行不同的UDT轉換-這是簡單的C ++規則,
在這一點上,清楚地了解到只有以下方法集會被實體化:
Item<int>::Item()-建構式void Item<int>::SetData(int)方法void Item<int>::PrintData() const方法Item<float>::Item()-建構式float Item<float>::GetData() const方法
以下方法將 無法 進行第二階段編譯:
int Item<int>::GetData() constvoid Item<float>::SetData(float)void Item<float>::PrintData() const
現在,什么是第二階段編譯? 好了,正如我已經闡述的那樣,無論是否呼叫/實體化模板代碼,都將對其進行編譯以進行基本語法檢查, 這稱為第一階段編譯,
當您實際呼叫或以某種方式觸發它呼叫特定型別的函式/方法時-只有它才能得到 特殊待遇 第二階段 編譯的 , 只有通過第二階段的編譯,代碼才實際針對實體化的型別進行完全編譯,
雖然,我本可以早點詳細說明,但是這個地方合適, 您如何確定函式是否正在進行第一階段和/或第二階段編譯?
讓我們做一些奇怪的事情:
T GetData() const
{
for())
return Data;
}
末尾有一個括號, for這是不正確的, 編譯它時, 它,都會收到很多錯誤 無論 是否呼叫 , 我已經使用Visual C ++和GCC編譯器對其進行了檢查,并且都抱怨, 這驗證了第一階段的編譯,
讓我們將其稍微更改為:
T GetData() const
{
T temp = Data[0]; // Index access ?
return Data;
}
現在,在 編譯 不 呼叫 GetData為任何型別 方法的情況下進行 -編譯器將不會產生任何終止作用, 這意味著,此功能目前尚未得到第二階段的編譯處理!
致電后:
Item<double> item3;
item2.GetData();
您會從 編譯器中得到錯誤,而該錯誤 Data不是陣列或指標的 可能已 陣列或指標 operartor []附加到 上, 證明只有選擇的函式才能獲得第二階段編譯的特殊特權, 對于實體化類/函式模板的所有唯一型別,此第二階段編譯將分別進行,
您可以做的一件有趣的事情是:
T GetData() const
{
return Data % 10;
}
可以成功為編譯 Item<int>,但失敗 Item<float>:
item1.GetData(); // item1 is Item<int>
// ERROR
item2.GetData(); // item2 is Item<float>
由于 operator %不適用于 float型別, 有趣嗎?
具有類模板的多種型別
我們的第一類模板 Item只有一個模板型別, 現在,讓我們構造一個具有兩個模板型別引數的類, 同樣,可能會有一些復雜的類模板示例,我想保持簡單,
有時,您確實需要一些本機結構來保留少量資料成員, 一件商品制作獨特商品 struct為同 似乎有些不必要和不必要的作業, 您很快就會從名稱很少的不同結構中脫穎而出, 另外,它增加了代碼長度, 無論您對此有何看法,我都以它為例,并派生一個包含兩個成員的類模板,
STL程式員會發現這等同于 std::pair類模板,
假設您有一個結構 Point,
struct Point
{
int x;
int y;
};
其中有兩個資料成員, 此外,您可能還具有其他結構 Money:
struct Money
{
int Dollars;
int Cents;
};
這兩種結構都具有幾乎相似的資料成員, 與其重寫不同的結構,不如將它放在一個地方會更好,這也將有助于:
- 具有一個或兩個給定型別的引數的建構式,以及一個復制建構式,
- 比較兩個相同型別物件的方法,
- 在兩種型別之間交換
- 和更多,
您可能會說可以使用繼承模型,在該模型中定義所有必需的方法,然后讓派生類對其進行自定義, 它適合嗎? 您選擇的資料型別呢? 它可能是 int, string或 float, 一些類 的型別 , 簡而言之,繼承只會使設計復雜化,而不會允許C ++模板促進插件功能,
在那里,我們使用類模板! 只需 定義 的類 模板即可 為兩種型別 具有所有必需方法 , 開始吧!
template<class Type1, class Type2>
struct Pair
{
// In public area, since we want the client to use them directly.
Type1 first;
Type2 second;
};
現在,我們可以使用 Pair類模板來 派生 具有兩個成員的任何型別, 一個例子:
// Assume as Point struct
Pair<int,int> point1;
// Logically same as X and Y members
point1.first = 10;
point1.second = 20;
了解 型別 first和 second現在 是 int和的 int分別 , 這是因為我們 實體化 Pair用這些型別 了,
當我們實體化它時:
Pair<int, double> SqRoot;
SqRoot.first = 90;
SqRoot.second = 9.4868329;
first將是 int型別,并且 second將是 double型別, 清楚地了解 first和 second是資料成員,而不是函式,因此 不會對運行時造成任何損失 假定的 函式呼叫 ,
注意 :在本文的此部分,所有定義僅在類宣告主體內, 在下一部分中,我將解釋如何在單獨的實作檔案中實作方法以及與此相關的問題, 因此,所示的所有方法定義都應僅假設在此范圍內 class ClassName{...};,
下面給出的默認建構式初始化會成員都為它們的默認值,按資料型別 Type1和 Type2:
Pair() : first(Type1()), second(Type2())
{}
以下是帶引數的建構式,采用 Type1和 Type2初始化 值 firstand的 second:
Pair(const Type1& t1, const Type2& t2) :
first(t1), second(t2)
{}
以下是一個復制建構式,它將 復制一個 Pair從另一個 物件 Pair完全相同型別的物件 :
Pair(const Pair<Type1, Type2>& OtherPair) :
first(OtherPair.first),
second(OtherPair.second)
{}
請注意,非常需要 指定的模板型別引數 Pair<>為此復制建構式的引數 , 下列規范沒有意義,因為 Pair它 不是 非模板型別:
Pair(const Pair& OtherPair) // ERROR: Pair requires template-types
這是一個使用引數化建構式和copy-constructor的示例:
Pair<int,int> point1(12,40);
Pair<int,int> point2(point1);
重要的是要注意,如果您更改任何一個物件 任何模板型別引數 point2或的 point1, 則將無法使用 復制構造它 point1object , 以下將是一個錯誤:
Pair<int,float> point2(point1); // ERROR: Different types, no conversion possible.
雖然,之間有可能轉換 float到 int,但之間沒有可能轉換 Pair<int,float>到 Pair<int,int>, 復制建構式不能將其他 型別 用作可復制物件, 有一個解決方案,但是我將在下一部分中討論它,
您可以類似的方式實作比較運算子,以比較兩個相同 物件 Pair型別的 , 以下是等于運算子的實作:
bool operator == (const Pair<Type1, Type2>& Other) const
{
return first == Other.first &&
second == Other.second;
}
請注意,我使用 const屬性,引數和方法本身, 請充分理解上述方法定義的第一行!
就像copy-constructor呼叫一樣,您必須將完全相同的型別傳遞給此比較運算子-編譯器不會嘗試轉換不同的 Pair型別, 一個例子:
if (point1 == point2) // Both objects must be of same type.
...
為了對此處介紹的概念有扎實的理解,請自行實作以下方法:
- 其余所有5個關系運算子
- 賦值運算子
Swap方法- 修改兩個建構式(copy-constructor除外),并將它們組合為一個,以便它們將兩個引數都用作默認值, 這意味著,僅實作一個可以接受0,1或2個引數的建構式,
Pairclass是兩種型別的示例,可以使用它代替定義僅具有兩個資料成員的多個結構, 缺點是只是記住什么 first和 second意味著(X或Y?), 但是,當您很好地定義模板實體化時,您始侄訓 了解和使用 first及其 second適當地 成員,
忽略這一缺點,您將實作 中的所有功能 實體化 型別 :建構式,復制建構式,比較運算子,交換方法等,而且,無需重新撰寫各種兩元結構所需的代碼,您將獲得所有這些,您將需要, 此外,如您所知,只有一組 必需的 方法會被編譯和鏈接, 類模板中的錯誤修復將自動反映到所有實體中, 是的,如果修改不符合現有用法,則對類模板進行的輕微修改也可能會引發其他型別的錯誤,
同樣,您可以具有一個類模板 模板 tuple,該 允許三個(或更多)資料成員, 請盡量實作類 tuple具有三個成員( first, second, third)自己:
template<class T1, class T2, class T3>
class tuple
非型別模板引數
好了,我們已經看到類模板和函式模板一樣,可以采用多個型別引數, 但是類模板也允許很少的非型別模板引數, 在這一部分中,我將僅闡述一個非型別: integer ,
是的,類模板可以采用整數作為模板引數, 首先是一個樣本:
template<class T, int SIZE>
class Array{};
在此類模板宣告中, int SIZE是一個非型別引數,它是一個整數,
- 只有積分資料型別可以是非整數型引數,它包括
int,char,long,long long,unsigned變體和enum第 諸如 這樣的型別float和double不允許使用 , - 實體化時,只能傳遞編譯時常數整數, 這意味著
100,100+99,1<<3等是允許的,因為它們被編譯時間常數運算式, 包含涉及函式呼叫的引數,例如abs(-120)不允許 ,
作為模板引數,如果可以將浮點數/雙精度數等轉換為整數,則可以允許它們,
精細, 我們可以實體化類模板 Array為:
Array<int, 10> my_array;
所以呢? 的目的是 SIZE爭論 什么?
好吧,在類模板中,可以在任何可能使用整數的地方使用此非型別整數引數, 這包括:
- 分配類的靜態const資料成員,
template<class T, int SIZE>
class Array
{
static const int Elements_2x = SIZE * 2;
};
[類宣告的前兩行將不再顯示,假定所有內容都在類的主體之內,]
由于允許 初始化 static-constant-integer 在類宣告中 ,因此我們可以使用非型別的整數引數,
- 指定方法的默認值,
(盡管,C ++還允許將任何非常量作為函式的默認引數,我已經指出了這一點只是為了說明,)
void DoSomething(int arg = SIZE);
// Non-const can also appear as default-argument...
- 定義陣列的大小,
這一點很重要,非型別整數引數通常用于此目的, 因此,讓我們 實作類模板 Array利用 SIZE引數來 ,
private:
T TheArray[SIZE];
T是陣列的型別, SIZE是大小(整數)-就這么簡單, 由于陣列位于類的私有區域中,因此我們需要定義幾個方法/運算子,
// Initialize with default (i.e. 0 for int)
void Initialize()
{
for(int nIndex = 0; nIndex < SIZE; ++nIndex)
TheArray[nIndex] = T();
}
當然,型別 T必須具有默認建構式和賦值運算子, 我將介紹 這些內容( 要求 在下一部分中, 功能模板和類模板的 ),
我們還需要實作陣列元素訪問運算子, 一個多載的索引訪問運算子集,另一個獲得值(型別 T):
T operator[](int nIndex) const
{
if (nIndex>0 && nIndex<SIZE)
{
return TheArray[nIndex];
}
return T();
}
T& operator[](int nIndex)
{
return TheArray[nIndex];
}
請注意,第一個多載(宣告為 const)是get / read方法,并檢查索引是否有效,否則回傳型別的默認值 T.
第二次多載回傳 的 參考 元素 ,呼叫者可以對其進行修改, 沒有索引有效性檢查,因為它必須回傳參考,因此 local-object( T()無法回傳 ), 但是,您可以檢查index引數,回傳默認值,使用 斷言 和/或引發例外,
讓我們定義另一個方法,該方法將在 邏輯上 求和 Array:
T Accumulate() const
{
T sum = T();
for(int nIndex = 0; nIndex < SIZE; ++nIndex)
{
sum += TheArray[nIndex];
}
return sum;
}
如您所解釋的,它要求 operator +=可用于target type T, 還要注意,回傳型別 T本身就是合適的, 因此,當 實體化 Array用某個字串類 時,它將 時呼叫 +=在每次迭代 并回傳組合的字串, 如果目標型別沒有 此 +=定義 運算子,而您呼叫此方法,則將出現錯誤, 在這種情況下,您要么-不要打電話; 或在目標類中實作所需的運算子多載,
模板類作為類模板的引數
盡管這是一個模糊的陳述,并引起一些歧義,但我會盡力消除模糊感,
首先,回顧一下 之間的區別 template-function 和 function-template , 如果 神經元 已經幫助將正確的資訊傳遞到 的 快取 大腦 中,那么您現在可以 回呼 template-function是function-template的一個實體, 如果您的大腦搜索子系統沒有回應,請 重新加載資訊 !
一個實體 類模板的 是 模板類, 因此,對于以下類模板:
template<class T1, class T2>
class Pair{};
該模板的實體是一個模板類:
Pair<int,int> IntPair;
清醒地認識到 IntPair是 不是 一個模板類,是 不是 對于類模板實體, 它是一個 物件 的特定實體/類模板的, 模板類/實體是 Pair<int,int>,它產生了另一個型別別(編譯器,我們的朋友做到了,您知道!), 本質上,這是在這種情況下編譯器將生成的模板類:
class Pair<int,int>{};
模板類有更精確的定義,請選擇以下單行代碼以便于理解, 詳細的說明將在本系列的下一部分中進行,
現在,讓我們說清楚, 如果您將模板類傳遞給某個類模板怎么辦? 我的意思是,以下陳述意味著什么?
Pair<int, Pair<int,int> > PairOfPair;
是否有效-如果是這樣,這是什么意思?
首先,它是完全有效的, 其次,它實體化了 兩個 模板類:
Pair<int,int>- 一個Pair<int, Pair<int,int> >- 乙
無論 一個 和 乙 型別會被編譯器進行實體化,如果有任何錯誤,因為任何型別的這兩個模板類的產生,編譯器將報告, 為了簡化這種 復雜的 實體,您可以執行以下操作:
typedef Pair<int,int> IntIntPair;
...
Pair<int, IntIntPair> PairOfPair;
您可以這樣分配 first和 second成員 PairOfPair物件的 :
PairOfPair.first = 10;
PairOfPair.second.first = 10;
PairOfPair.second.second= 30;
請注意, second最后兩行中的member是type Pair<int,int>,因此它具有相同的一組成員以供進一步訪問, 這就是原因 first和 second成員都可以使用,以 級聯 的方式,
現在,您(希望)了解到類模板( Pair)將模板類( Pair<int,int>) 作為引數并引入了最終實體化!
在此討論中,一個有趣的實體將 Array與一起使用 Pair! 您知道 Pair有兩個模板型別引數, Array一個型別引數和一個大小(整數)引數,
Array< Pair<int, double>, 40> ArrayOfPair;
這是 int和 double的型別引數 Pair, 因此, 的第一個模板型別 Array(標記為粗體) 為 Pair<int,double>, 第二個引數是常量 40, 您能回答這個問題嗎:的建構式會 Pair<int,double>被呼叫嗎? 什么時候會被呼叫? 在您回答之前,我只是將實體反轉為:
Pair<int, Array<double, 50>> PairOfArray;
哇! 這是什么意思?
好吧,這意味著: PairOfArray是的實體化 Pair,將第一種型別作為 int (對于 first成員),而第二種型別( second)是一個 Array, 其中 Array(的第二種型別 type的 Pair)是 50元素 double!
不要為此而殺了我! 慢慢并清楚地了解模板的這些基本概念, 一旦獲得了清晰的理解,您就會 喜歡 模板!
再一次,我使用了模板類( Array<double,50>)作為其他型別( 實體的引數 Pair<int,...>) ,
好的,但是 的右移運算子( >>上面 )在做什么? 嗯,這不是運算子,而只是 Array型別說明的結尾,然后是 的結尾 Pair型別說明 , 一些舊的編譯器要求我們在兩個大于號之間插入一個空格,以避免出現錯誤或混亂,
Pair<int, Array<double, 50> > PairOfArray;
當前,幾乎所有現代C ++編譯器都足夠聰明,足以了解它用于結束模板型別規范,因此您不必擔心, 因此,您可以隨意使用兩個或多個 >符號來結束模板規范,
請 注意,在C ++術語中,傳遞模板類(實體化)并不是很具體-它只是類模板所采用的一種型別,
最后,在這里我將用法示例放到兩個物件中, 首先是建構式,
Array< Pair<int, double>, 40> ArrayOfPair;
這將導致的建構式 Pair被呼叫 40 次,因為在 宣告了常量大小的陣列 Array類模板中 :
T TheArray[SIZE];
這將意味著:
Pair<int,double> TheArray[40];
因此,需要呼叫數量的建構式 Pair,
對于以下物件構造:
Pair<int, Array<double, 50>> PairOfArray;
構造 Pair將與初始化第一個引數 0(使用 int()符號),并且將呼叫的構造 Array與 Array()符號,如下圖所示:
Pair() : first(int()), second(Array())
{}
由于 的默認建構式 Array類模板 由編譯器提供,因此它將被呼叫, 如果您不理解此處撰寫的內容,請提高您的C ++技能,
分配的一個元素 ArrayOfPair:
ArrayOfPair[0] = Pair<int,double>(40, 3.14159);
在這里,您正在呼叫的非常量版本 版本 Array::operator[],該 將回傳 的 參考 第一個元素 Array (from TheArray) , 如您所知,該元素是type Pair<int,double>, 賦值運算子右側的運算式只是呼叫建構式, Pair<int,double>并傳遞所需的兩個引數, 分配完成!
帶類模板的默認模板引數
首先,讓我消除與“默認引數”短語的任何歧義, “功能模板”部分中使用了相同的短語, 在該小節中,默認引數是指功能引數本身的引數,而不是功能模板的型別引數, 無論如何,函式模板 不 支持模板引數的默認引數, 附帶說明一下,請知道類模板的方法可以采用默認引數,就像任何普通函式/方法都可以采用那樣,
另一方面,類模板確實為模板引數的type / non-type引數支持default-argument, 舉個例子:
template<class T, int SIZE=100>
class Array
{
private:
T TheArray[SIZE];
...
};
我剛剛 了修改 SIZE在類模板的第一行中進行 Array, 第二個模板引數,即整數常量規范,現在設定為 100, 這意味著,當您以以下方式使用它時:
Array<int> IntArray;
從本質上講,這意味著:
Array<int, 100> IntArray;
在實體化此類模板期間,編譯器會自動將其放置, 當然,您可以通過顯式傳遞第二個模板引數來指定自定義陣列的大小:
Array<int, 200> IntArray;
請記住,當您通過類模板宣告中指定的相同引數顯式傳遞默認引數的引數時,它將僅實體化一次, 我的意思是,創建的以下兩個物件將僅實體化一個類: Array<int,100>
Array<int> Array1;
Array<int,100> Array2;
當然,如果您在類模板定義中更改默認的template引數(值為以外的值) 100,則會導致兩個模板實體化,因為它們是不同的型別,
您可以使用 自定義默認引數 const或 #define:
const int _size = 120;
// #define _size 150
template<class T, int SIZE=_size>
class Array
當然,使用 _size符號代替硬編碼的常數表示相同, 但是使用符號會簡化默認的'規范, 無論您如何為整數指定默認模板引數(這是一個非型別模板引數),它都必須是一個編譯時間常數運算式,
你一般會 不 使用默認規范非型別整型引數,除非你正在使用的模板,先進的東西,如元編程,靜態斷言,SFINAE等,這肯定需要一個單獨的部分, 更常見的是,您會看到并實作類模板的默認引數,即 資料型別 , 一個例子:
template<class T = int>
class Array100
{
T TheArray[100];
};
它定義了一個 型別的陣列 Tsize 100, 在這里,type引數默認為 int, 這意味著,如果您在實體化時未指定型別 Array100,則它將映射到 int, 以下是有關如何使用它的示例:
Array100<float> FloatArray;
Array100<> IntArray;
在第一個實體中,我 傳遞 float以模板型別 ,而在第二個呼叫中,我 將其保留為默認值( int使用 ) <>符號 , 盡管此符號在模板編程中有更多用途,我將在后面的部分中進行介紹,但這種情況也非常需要, 如果您嘗試將類模板用作:
Array100 IntArray;
這將導致編譯器錯誤,即 Array100需要模板引數, 因此, 必須使用尖括號( 空集 <>如果所有模板引數均為默認值,并且您希望使用默認值,則 )的 實體化類模板,
要記住重要的事情是一個非模板類的名字 Array100將 不會 被也是允許的, 如下所示,非模板類的定義以及模板類(彼此之間或之上或之下)的定義將使編譯器不滿意:
class Array100{}; // Array100 demands template arguments!
現在,讓我們在類中混合使用type和non-type引數 Array:
template<class T = int, int SIZE=100>
class Array
{
T TheArray[SIZE];
...
};
最后,type和size引數分別用 標記為default int和 100, 清楚地了解到,第一個 int用于的默認規范 T,第二個 int用于非模板常量規范, 為了簡化和提高可讀性,應將它們放在不同的行中:
template<class T = int,
int SIZE=100>
class Array{};
現在,使用您的智能來決議以下實體化的含義:
Array<> IntArray1;
Array<int> IntArray2;
Array<float, 40> FlaotArray3;
就像 一樣 函式模板中的顯式說明 ,不允許僅指定尾隨模板引數, 以下是錯誤:
Array<, 400> IntArrayOf500; // ERROR
最后,請記住,在創建兩個物件之后將僅實體化一個類模板,因為它們實際上是完全相同的:
Array<> IntArray1;
Array<int> IntArray2
Array<int, 100> IntArray3;
將模板型別默認為其他型別
也可以在先前到達的模板引數上默認設定type / non-type引數, 例如 ,我們可以修改 Pair,如果未明確指定第二種型別 該類,以使第二種型別與第一種型別相同,
template<class Type1, class Type2 = Type1>
class Pair
{
Type1 first;
Type2 second;
};
在此修改后的類模板中 Pair, Type2現在默認為 Type1type, 實體化的例子:
Pair<int> IntPair;
您可以猜測,它與:
Pair<int,int> IntPair;
但是,您不必輸入第二個引數, 也可以將第一個引數 Pair設為default:
template<class Type1=int, class Type2 = Type1>
class Pair
{
Type1 first;
Type2 second;
};
這意味著,如果你沒有通過任何模板引數, Type1會 int,因此 Type2 也將是 int !
用法如下:
Pair<> IntPair;
實體化以下類:
class Pair<int,int>{};
當然,也可以在另一個非型別引數上默認非型別引數, 一個例子:
template<class T, int ROWS = 8, int COLUMNS = ROWS>
class Matrix
{
T TheMatrix[ROWS][COLUMNS];
};
但是, 從屬 模板引數必須在 的 右邊 其所依賴 , 以下將導致錯誤:
template<class Type1=Type2, class Type2 = int>
class Pair{};
template<class T, int ROWS = COLUMNS, int COLUMNS = 8>
class Matrix
類的方法作為功能模板
雖然,這不是絕對的初學者,但是由于我同時介紹了函式模板和類模板,因此對該概念的闡述對于本系列文章的第一部分而言是合乎邏輯的,
考慮一個簡單的例子:
class IntArray
{
int TheArray[10];
public:
template<typename T>
void Copy(T target_array[10])
{
for(int nIndex = 0; nIndex<10; ++nIndex)
{
target_array[nIndex] = TheArray[nIndex];
// Better approach:
//target_array[nIndex] = static_cast<T>(TheArray[nIndex]);
}
}
};
該類 IntArray是簡單的非模板類,具有 的整數陣列 10元素 , 但是該方法 Copy被設計為功能模板(方法模板?), 它采用一個模板型別引數,該引數將由編譯器自動推導, 這是我們如何使用它:
IntArray int_array;
float float_array[10];
int_array.Copy(float_array);
您可能猜到了, IntArray::Copy將使用type實體化 float,因為我們將float陣列傳遞給了它, 為了避免混淆,并更好地理解它,只是覺得 int_array.Copy作為 Copy唯一的,并 IntArray::Copy<float>(..)作為 Copy<float>(..)唯一的, 類的 方法 模板不過是嵌入在類中的普通功能模板,
請注意,我 使用 10到處都 陣列大小, 有趣的是,我們還可以將類修改為
template<int ARRAY_SIZE>
class IntArray
{
int TheArray[ARRAY_SIZE];
public:
template<typename T>
void Copy(T target_array[ARRAY_SIZE])
{
for(int nIndex = 0; nIndex<ARRAY_SIZE; ++nIndex)
{
target_array[nIndex] = static_cast<T>(TheArray[nIndex]);
}
}
};
使得類 IntArray和方法 Copy,更好的候選人在模板編程領域!
就像您已經聰明地猜到的那樣, Copy方法只不過是一個陣列轉換例程,該例程可以從轉換 int為任何型別,只要 轉換為 int有可能就可以 給定型別, 這是一種有效的情況,其中類方法可以作為函式模板撰寫,可以自己獲取模板引數, 請修改此類 模板 ,以使其可用于任何型別的陣列,而不僅限于 int.
當然,帶有方法模板的“顯式模板引數指定”也是可能的, 考慮另一個示例:
template<class T>
class Convert
{
T data;
public:
Convert(const T& tData = T()) : data(tData)
{ }
template<class C>
bool IsEqualTo( const C& other ) const
{
return data == other;
}
};
可以用作:
Convert<int> Data;
float Data2 = 1 ;
bool b = Data.IsEqualTo(Data2);
實體化 Convert::IsEqualTo用 float引數 , 如下所示,顯式規范將使用實體化它 double:
bool b = Data.IsEqualTo<double>(Data2);
令人驚訝的事情之一是,借助模板,您可以通過在模板之上定義轉換運算子來做到這一點!
template<class T>
operator T() const
{
return data;
}
將 Convert只要有可能,就可以 '類模板實體轉換為任何型別, 考慮以下用法示例:
Convert<int> IntData(40);
float FloatData;
double DoubleData;
FloatData = IntData;
DoubleData = IntData;
它將實體化以下兩種方法(完全限定的名稱):
Convert<int>::operator<float> float();
Convert<int>::operator<double> double();
一方面,它提供了良好的靈活性,因為無需撰寫額外的代碼, Convert就可以將自身(特定的實體化)轉換為任何資料型別-只要在編譯級別可以進行轉換即可, 如果無法進行轉換,例如從 轉換為 doubleto 字串型別,則會引發錯誤,
但是,另一方面,它也可能由于無意中插入錯誤而引起麻煩, 您可能不希望呼叫轉換運算子,并且在您不了解轉換運算子的情況下呼叫了該轉換運算子(生成了編譯器代碼),
在最后
您剛剛看到模板所提供的強大功能和靈活性, 下一部分將介紹更多高級和有趣的概念, 我對所有的讀者謙遜和有抱負的要求是 發揮 與模板越來越多, 嘗試首先在一個方面獲得牢固的了解(僅像功能模板一樣),而不是匆忙跳到其他概念, 最初是使用您的 測驗 專案/代碼庫,而不是任何現有的/正在運行的/生產的代碼,
以下是我們所涵蓋內容的摘要:
- 為了避免不必要的代碼重復和代碼維護問題,特別是當代碼完全相同時,我們可以使用模板, 模板比使用在空指標之上運行的C / C ++宏或函式/類要好得多,
- 模板不僅是型別安全的,而且還減少了不會被參考(不是由編譯器生成)的不必要的代碼膨脹,
- 函式模板用于放置不屬于類的代碼,并且對于不同的資料型別,該代碼相同/幾乎相同, 在大多數地方,編譯器會自動確定型別, 否則,您必須指定型別,也可以自己指定顯式型別,
- 類模板使圍繞特定實作包裝任何資料型別成為可能, 它可以是陣列,字串,佇列,鏈表,執行緒安全的 原子 實作等,類模板確實有助于默認模板型別規范,而功能模板不支持該規范,
希望您喜歡這篇文章,并清除了使模板變得復雜(不必要地是奇怪)的思想障礙, 第二部分將很快到達!
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/248135.html
標籤:其他
