1. 程式人生 > 其它 >很多人覺得C++模板很難學習和適應,不值得浪費時間,今天它的白痴指南來了(第一部分)

很多人覺得C++模板很難學習和適應,不值得浪費時間,今天它的白痴指南來了(第一部分)

技術標籤:c++程式語言模板經驗分享

文章背景

大多數C ++程式設計師由於其困惑的性質而遠離C ++模板。 反對模板的藉口:

  • 很難學習和適應。
  • 編譯器錯誤是模糊的,而且很長。
  • 不值得的努力。

承認模板很難學習,理解和適應。 然而,我們從使用模板中獲得的好處將超過負面影響。 有 比可以圍繞模板包裝的泛型函式或類要多得多。 我會說明他們。

從技術上講,C ++模板和STL(標準模板庫)是同級的。 在本文中,我只會介紹核心級別的模板。 本系列的下一部分將圍繞模板介紹更高階和有趣的內容,以及有關STL的一些專門知識。

目錄

語法句

  • 功能模板
  • 帶有模板的指標,引用和陣列
  • 帶有功能模板的多種型別
  • 功能模板-模板功能
  • 顯式模板引數規範
  • 功能模板的預設引數

類模板

  • 具有類模板的多種型別
  • 非型別模板引數
  • 模板類作為類模板的引數
  • 帶類模板的預設模板引數
  • 類的方法作為功能模板

文末雜談

【零聲學院官方許可】2小時精通掌握《STL模板庫》技術


語法句

您可能知道,模板很大程度上使用尖括號:小於( < )和大於( > )運算子。 對於模板,它們總是以這種形式一起使用:

< Content >

哪裡可以用Content

  1. class T / typename T
  2. 資料型別,對映到 T
  3. 整體規格
  4. 對映到上述規範的整數常量/指標/參考。

對於點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使用 函式 intdouble引數型別 兩個 編譯器將生成此函式的 例項:

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[] -編譯器是足夠聰明來推斷型別 intint[](或 int*)。 在上面給出的示例中,我已將其用作 T tArray[]函式模板的引數,並且 實際資料型別 T可以從中智慧地確定的 。

通常,您會碰到過,並且還需要使用初始化,例如:

T tSum = T();

首先,這不是模板特定的程式碼-它屬於C ++語言本身。 從本質上講,這意味著:呼叫 的 預設建構函式 此資料型別 。 對於 int,它將是:

int tSum = int();

有效地使用初始化變數 0。 同樣,對於 float,它將將此變數設定為 0.0f。 儘管尚未涵蓋,但是如果使用者定義的類型別來自 T,它將呼叫該類的預設建構函式(如果可呼叫,否則相關的錯誤)。 如您所知,它 T可能是任何資料型別,我們不能 初始化 tSum簡單地使用整數零( 0)進行 。 實際上,它可能是某個字串類,它使用空字串( 對其進行初始化 "") 。

由於模板型別 T可以是任何型別,因此它也必須 += operator可用。 正如我們所知,它是可用於所有的基本資料型別( intfloatchar等)。 如果實際型別(用於 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。 問問自己:如果某些類 這意味著什麼 StringDate實現或被要求實現以下運算子, :

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;
}

觀察到我已經將模板引數修改 TYPETYPE&,並且也新增 const了它。 很少或大多數讀者會意識到這種變化的重要性。 對於那些沒有的人:

  • TYPE型別的大小可能很大,並且將需要更多的堆疊空間(呼叫堆疊)。 它包括 double需要8個位元組 *,某些結構或類的類,這將需要更多位元組保留在堆疊上。 從本質上講,這意味著-將建立給定型別的新物件,呼叫複製建構函式,並將其放入呼叫堆疊中,然後在函式結尾處進行解構函式呼叫。
    引用( &)的新增避免了所有這些情況- 引用 傳遞同一物件的 。
  • 函式不會更改傳遞的引數,因此 const不會對其進行新增。 對於函式的呼叫者,它確保此函式(此處為 PrintTwice)不會更改引數的值。 如果函式本身錯誤地嘗試修改( 內容,則還可以確保發生編譯器錯誤 constant )引數的

在32位平臺上,函式引數至少需要4個位元組,並且至少需要4個位元組。 這意味著一個 charshort將在呼叫堆疊中需要4個位元組。 例如,一個11位元組的物件需要12位元組的堆疊。
同樣,對於64位平臺,將需要8個位元組。 一個11位元組的物件將需要16個位元組。 型別的引數 double將需要8個位元組。
在32位/ 64位平臺上,所有指標/引用分別佔用4位元組/ 8位元組,因此 位平臺,傳遞 doubledouble&對於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語句中,編譯器會抱怨 t1t2無法將其轉換為非常量。 如果我們 新增 const還將返回型別也 為( const T& GetMax(...) ),則呼叫網站上的以下行將無法編譯:

GetMax(x,y) = 0;

由於 const物件無法修改! 您絕對可以在函式中或在呼叫站點中進行強制const /非const型別轉換。 但這是一個不同的方面,一個糟糕的設計和一個不推薦的方法。

帶有功能模板的多種型別

到目前為止,我只介紹了一種型別作為模板型別引數。 使用模板,您可能有多個模板型別引數。 就像這樣:

template<class T1, class T2, ... >

其中 T1T2是功能模板的型別名稱。 您可以使用任何其他特定的名稱,而不是 T1T2。 需要注意的是的“使用 ...”上面並 沒有 意味著這個模板規範可以採取任何數量的引數。 僅說明模板可以具有任意數量的引數。
(與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將推斷不同資料型別( intdoubledoubleint)。 編譯器將 不會 執行任何自動轉換,就像正常函式呼叫可能會執行的那樣- 一個採用的正常函式 int例如,可以傳遞 , short反之亦然。 但是對於模板,如果您通過 short-它是絕對的 short,不是(升級為) int。 因此,如果您傳遞( shortint),( shortshort),( longint)-這將導致 三個不同的例項化 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 將要求降級值-編譯器必須轉換 doubleint,因此 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;
    }
};

一個初始化 建構函式 Data0,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成類模板,我更換的所有例項 intT。 我還使用 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()建設者
  • SetDataPrintData型別的方法 int

類似地,型別第二次例項化 float將產生:

  • Item<float>::Item()建設者
  • GetData方法 float型別的

如您所知 Item<int>Item<float>是兩種不同的類/型別; 因此,以下程式碼將不起作用:

item1 = item2; // ERROR : Item<float> to Item<int>

由於兩種型別不同,因此編譯器將不會呼叫 可能的 預設賦值運算子。 如果 item1item2具有相同的型別(都使用 Item<int>),則編譯器會很高興地呼叫賦值運算子。 儘管對於編譯器來說,可以在 之間 intfloat 進行轉換,但是即使基礎資料成員相同,也不可能進行不同的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;
};

這兩種結構都具有幾乎相似的資料成員。 與其重寫不同的結構,不如將它放在一個地方會更好,這也將有助於:

  • 具有一個或兩個給定型別的引數的建構函式,以及一個複製建構函式。
  • 比較兩個相同型別物件的方法。
  • 在兩種型別之間交換
  • 和更多。

您可能會說可以使用繼承模型,在該模型中定義所有必需的方法,然後讓派生類對其進行自定義。 它適合嗎? 您選擇的資料型別呢? 它可能是 intstringfloat一些類 的型別 簡而言之,繼承只會使設計複雜化,而不會允許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;

瞭解 型別 firstsecond現在 是 int和的 int分別 。 這是因為我們 例項化 Pair用這些型別 了。

當我們例項化它時:

Pair<int, double> SqRoot;

SqRoot.first = 90;
SqRoot.second = 9.4868329;

first將是 int型別,並且 second將是 double型別。 清楚地瞭解 firstsecond是資料成員,而不是函式,因此 不會對執行時造成任何損失 假定的 函式呼叫 。

注意 :在本文的此部分,所有定義僅在類宣告主體內。 在下一部分中,我將解釋如何在單獨的實現檔案中實現方法以及與此相關的問題。 因此,所示的所有方法定義都應僅假設在此範圍內 class ClassName{...};

下面給出的預設建構函式初始化會成員都為它們的預設值,按資料型別 Type1Type2

Pair() : first(Type1()), second(Type2())
{}

以下是帶引數的建構函式,採用 Type1Type2初始化 值 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.

雖然,之間有可能轉換 floatint,但之間沒有可能轉換 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是兩種型別的示例,可以使用它代替定義僅具有兩個資料成員的多個結構。 缺點是隻是記住什麼 firstsecond意味著(X或Y?)。 但是,當您很好地定義模板例項化時,您始終會 瞭解和使用 first及其 second適當地 成員。

忽略這一缺點,您將實現 中的所有功能 例項化 型別 :建構函式,複製建構函式,比較運算子,交換方法等。而且,無需重新編寫各種兩元結構所需的程式碼,您將獲得所有這些。您將需要。 此外,如您所知,只有一組 必需的 方法會被編譯和連結。 類模板中的錯誤修復將自動反映到所有例項中。 是的,如果修改不符合現有用法,則對類模板進行的輕微修改也可能會引發其他型別的錯誤。

同樣,您可以具有一個類模板 模板 tuple,該 允許三個(或更多)資料成員。 請儘量實現類 tuple具有三個成員( firstsecondthird)自己:

template<class T1, class T2, class T3>
class tuple

非型別模板引數

好了,我們已經看到類模板和函式模板一樣,可以採用多個型別引數。 但是類模板也允許很少的非型別模板引數。 在這一部分中,我將僅闡述一個非型別: integer

是的,類模板可以採用整數作為模板引數。 首先是一個樣本:

template<class T, int SIZE>
class Array{};

在此類模板宣告中, int SIZE是一個非型別引數,它是一個整數。

  • 只有積分資料型別可以是非整數型引數,它包括 intcharlonglong longunsigned變體和 enum第 諸如 這樣的型別 floatdouble不允許使用 。
  • 例項化時,只能傳遞編譯時常數整數。 這意味著 100100+991<<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;

您可以這樣分配 firstsecond成員 PairOfPair物件的 :

PairOfPair.first = 10;
PairOfPair.second.first = 10;
PairOfPair.second.second= 30;

請注意, second最後兩行中的member是type Pair<int,int>,因此它具有相同的一組成員以供進一步訪問。 這就是原因 firstsecond成員都可以使用,以 級聯 的方式。

現在,您(希望)瞭解到類模板( Pair)將模板類( Pair<int,int> 作為引數並引入了最終例項化!

在此討論中,一個有趣的例項將 Array與一起使用 Pair! 您知道 Pair有兩個模板型別引數, Array一個型別引數和一個大小(整數)引數。

Array< Pair<int, double>, 40> ArrayOfPair;

這是 intdouble的型別引數 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()符號),並且將呼叫的構造 ArrayArray()符號,如下圖所示:

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 int100。 清楚地瞭解到,第一個 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;
};

在此修改後的類模板中 PairType2現在預設為 Type1type。 例項化的例子:

Pair<int> IntPair;

您可以猜測,它與:

Pair<int,int> IntPair;

但是,您不必輸入第二個引數。 也可以將第一個引數 Pair設為default:

template<class Type1=int, class Type2 = Type1>
class Pair
{
    Type1 first;
    Type2 second;
};

這意味著,如果你沒有通過任何模板引數, Type1int,因此 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::IsEqualTofloat引數 。 如下所示,顯式規範將使用例項化它 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 ++巨集或函式/類要好得多。
  • 模板不僅是型別安全的,而且還減少了不會被引用(不是由編譯器生成)的不必要的程式碼膨脹。
  • 函式模板用於放置不屬於類的程式碼,並且對於不同的資料型別,該程式碼相同/幾乎相同。 在大多數地方,編譯器會自動確定型別。 否則,您必須指定型別,也可以自己指定顯式型別。
  • 類模板使圍繞特定實現包裝任何資料型別成為可能。 它可以是陣列,字串,佇列,連結串列,執行緒安全的 原子 實現等。類模板確實有助於預設模板型別規範,而功能模板不支援該規範。

希望您喜歡這篇文章,並清除了使模板變得複雜(不必要地是奇怪)的思想障礙。 第二部分將很快到達!