很多人覺得C++模板很難學習和適應,不值得浪費時間,今天它的白痴指南來了(第一部分)
文章背景
大多數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;
}
這需要兩個 int
s並將其求和。 儘管將它們 求和 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() const
void 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
初始化 值 first
and的 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
,
則將無法使用 複製構造它 point1
object 。 以下將是一個錯誤:
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個引數的建構函式。
Pair
class是兩種型別的示例,可以使用它代替定義僅具有兩個資料成員的多個結構。 缺點是隻是記住什麼 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];
};
它定義了一個 型別的陣列 T
size 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
現在預設為 Type1
type。 例項化的例子:
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
就可以將自身(特定的例項化)轉換為任何資料型別-只要在編譯級別可以進行轉換即可。 如果無法進行轉換,例如從 轉換為 double
to 字串型別,則會引發錯誤。
但是,另一方面,它也可能由於無意中插入錯誤而引起麻煩。 您可能不希望呼叫轉換運算子,並且在您不瞭解轉換運算子的情況下呼叫了該轉換運算子(生成了編譯器程式碼)。
在最後
您剛剛看到模板所提供的強大功能和靈活性。 下一部分將介紹更多高階和有趣的概念。 我對所有的讀者謙遜和有抱負的要求是 發揮 與模板越來越多。 嘗試首先在一個方面獲得牢固的瞭解(僅像功能模板一樣),而不是匆忙跳到其他概念。 最初是使用您的 測試 專案/程式碼庫,而不是任何現有的/正在執行的/生產的程式碼。
以下是我們所涵蓋內容的摘要:
- 為了避免不必要的程式碼重複和程式碼維護問題,特別是當代碼完全相同時,我們可以使用模板。 模板比使用在空指標之上執行的C / C ++巨集或函式/類要好得多。
- 模板不僅是型別安全的,而且還減少了不會被引用(不是由編譯器生成)的不必要的程式碼膨脹。
- 函式模板用於放置不屬於類的程式碼,並且對於不同的資料型別,該程式碼相同/幾乎相同。 在大多數地方,編譯器會自動確定型別。 否則,您必須指定型別,也可以自己指定顯式型別。
- 類模板使圍繞特定實現包裝任何資料型別成為可能。 它可以是陣列,字串,佇列,連結串列,執行緒安全的 原子 實現等。類模板確實有助於預設模板型別規範,而功能模板不支援該規範。
希望您喜歡這篇文章,並清除了使模板變得複雜(不必要地是奇怪)的思想障礙。 第二部分將很快到達!