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

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

2021-01-13 11:34:02 軟體設計

文章背景

大多數C ++程式員由于其困惑的性質而遠離C ++模板, 反對模板的借口:

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

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

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

目錄

語法句

  • 功能模板
  • 帶有模板的指標,參考和陣列
  • 帶有功能模板的多種型別
  • 功能模板-模板功能
  • 顯式模板引數規范
  • 功能模板的默認引數

類模板

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

文末雜談

<iframe id="ykvU6c6Y-1610435947459" src="https://player.bilibili.com/player.html?aid=288385586" allowfullscreen="true" data-mediaembed="bilibili"></iframe>

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


語法句

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

< Content >

哪里可以用Content

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

對于點1和2,符號 T不過是某種資料型別,它可以是任何資料型別-基本資料型別( intdouble等)或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 ++宏或函式/類要好得多,
  • 模板不僅是型別安全的,而且還減少了不會被參考(不是由編譯器生成)的不必要的代碼膨脹,
  • 函式模板用于放置不屬于類的代碼,并且對于不同的資料型別,該代碼相同/幾乎相同, 在大多數地方,編譯器會自動確定型別, 否則,您必須指定型別,也可以自己指定顯式型別,
  • 類模板使圍繞特定實作包裝任何資料型別成為可能, 它可以是陣列,字串,佇列,鏈表,執行緒安全的 原子 實作等,類模板確實有助于默認模板型別規范,而功能模板不支持該規范,

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

轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/248135.html

標籤:其他

上一篇:面試:Redis為什么快呢?查詢為何會變慢呢?

下一篇:【每日藍橋】3、一三年省賽JavaC組真題“馬虎的算式”

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 面試突擊第一季,第二季,第三季

    第一季必考 https://www.bilibili.com/video/BV1FE411y79Y?from=search&seid=15921726601957489746 第二季分布式 https://www.bilibili.com/video/BV13f4y127ee/?spm_id_fro ......

    uj5u.com 2020-09-10 05:35:24 more
  • 第三單元作業總結

    1.前言 這應該是本學期最后一次寫作業總結了吧。總體來說,對作業的節奏也差不多掌握了,作業做起來的效率也更高了。雖然和之前的作業一樣,作業中都要用到新的知識,但是相比之前,更加懂得了如何利用工具以及資料。雖然之間卡過殼,但總體而言,這幾次作業還算完成的比較好。 2.作業程序總結 相比前兩個單元,此單 ......

    uj5u.com 2020-09-10 05:35:41 more
  • 北航OO(2020)第四單元博客作業暨課程總結博客

    北航OO(2020)第四單元博客作業暨課程總結博客 本單元作業的架構設計 在本單元中,由于UML圖具有比較清晰的樹形結構,因此我對其中需要進行查詢操作的元素進行了包裝,在樹的父節點中存盤所有孩子的參考。考慮到性能問題,我采用了快取機制,一次查詢后盡可能快取已經遍歷過的資訊,以減少遍歷次數。 本單元我 ......

    uj5u.com 2020-09-10 05:35:48 more
  • BUAA_OO_第四單元

    一、UML決議器設計 ? 先看下題目:第四單元實作一個基于JDK 8帶有效性檢查的UML(Unified Modeling Language)類圖,順序圖,狀態圖分析器 MyUmlInteraction,實際上我們要建立一個有向圖模型,UML中的物件(元素)可能與同級元素連接,也可與低級元素相連形成 ......

    uj5u.com 2020-09-10 05:35:54 more
  • 6.1邏輯運算子

    邏輯運算子 1. && 短路與 運算式1 && 運算式2 01.運算式1為true并且運算式2也為true 整體回傳為true 02.運算式1為false,將不會執行運算式2 整體回傳為false 03.只要有一個運算式為false 整體回傳為false 2. || 短路或 運算式1 || 運算式2 ......

    uj5u.com 2020-09-10 05:35:56 more
  • BUAAOO 第四單元 & 課程總結

    1. 第四單元:StarUml檔案決議 本單元采用了圖模型決議UML。 UML檔案可以抽象為圖、子圖、邊的邏輯結構。 在實作中,圖的節點包括類、介面、屬性,子圖包括狀態圖、順序圖等。 采用了三次遍歷UML元素的方法建圖,第一遍遍歷建點,第二、三次遍歷設定屬性、連邊,實作圖物件的初始化。這里借鑒了一些 ......

    uj5u.com 2020-09-10 05:36:06 more
  • 談談我對C# 多型的理解

    面向物件三要素:封裝、繼承、多型。 封裝和繼承,這兩個比較好理解,但要理解多型的話,可就稍微有點難度了。今天,我們就來講講多型的理解。 我們應該經常會看到面試題目:請談談對多型的理解。 其實呢,多型非常簡單,就一句話:呼叫同一種方法產生了不同的結果。 具體實作方式有三種。 一、多載 多載很簡單。 p ......

    uj5u.com 2020-09-10 05:36:09 more
  • Python 資料驅動工具:DDT

    背景 python 的unittest 沒有自帶資料驅動功能。 所以如果使用unittest,同時又想使用資料驅動,那么就可以使用DDT來完成。 DDT是 “Data-Driven Tests”的縮寫。 資料:http://ddt.readthedocs.io/en/latest/ 使用方法 dd. ......

    uj5u.com 2020-09-10 05:36:13 more
  • Python里面的xlrd模塊詳解

    那我就一下面積個問題對xlrd模塊進行學習一下: 1.什么是xlrd模塊? 2.為什么使用xlrd模塊? 3.怎樣使用xlrd模塊? 1.什么是xlrd模塊? ?python操作excel主要用到xlrd和xlwt這兩個庫,即xlrd是讀excel,xlwt是寫excel的庫。 今天就先來說一下xl ......

    uj5u.com 2020-09-10 05:36:28 more
  • 當我們創建HashMap時,底層到底做了什么?

    jdk1.7中的底層實作程序(底層基于陣列+鏈表) 在我們new HashMap()時,底層創建了默認長度為16的一維陣列Entry[ ] table。當我們呼叫map.put(key1,value1)方法向HashMap里添加資料的時候: 首先,呼叫key1所在類的hashCode()計算key1 ......

    uj5u.com 2020-09-10 05:36:38 more
最新发布
  • 【中介者設計模式詳解】C/Java/JS/Go/Python/TS不同語言實作

    * 中介者模式是一種行為型設計模式,它可以用來減少類之間的直接依賴關系,
    * 將物件之間的通信封裝到一個中介者物件中,從而使得各個物件之間的關系更加松散。
    * 在中介者模式中,物件之間不再直接相互互動,而是通過中介者來中轉訊息。 ......

    uj5u.com 2023-04-20 08:20:47 more
  • 露天煤礦現場調研和交流案例分享

    他們集團的資訊化公司及研究院在一個礦區正在做智能礦山的統一平臺的 試點,專案投資大概1億,包括了礦山的各方面的內容,顯示得我們這次交流有點多余。他們2年前開始做智能礦山的規劃,有很多煤礦行業專家的加持,他們的描述是非常完美,但是去年底應該上線的平臺,現在還沒有看到影子。他們確實有很多場景需求,但是被... ......

    uj5u.com 2023-04-20 08:20:25 more
  • 《社區人員管理》實戰案例設計&個人案例分享

    設計是一個讓人夢想成真程序,開始編碼、測驗、除錯之前進行需求分析和架構設計,才能保證關鍵方面都做正確 ......

    uj5u.com 2023-04-20 08:20:17 more
  • 軟體架構生態化-多角色交付的探索實踐

    作為一個技術架構師,不僅僅要緊跟行業技術趨勢,還要結合研發團隊現狀及痛點,探索新的交付方案。在日常中,你是否遇到如下問題 “ 業務需求排期長研發是瓶頸;非研發角色感受不到研發技改提效的變化;引入ISV 團隊又擔心質量和安全,培訓周期長“等等,基于此我們探索了一種新的技術體系及交付方案來解決如上問題。 ......

    uj5u.com 2023-04-20 08:20:10 more
  • 【中介者設計模式詳解】C/Java/JS/Go/Python/TS不同語言實作

    * 中介者模式是一種行為型設計模式,它可以用來減少類之間的直接依賴關系,
    * 將物件之間的通信封裝到一個中介者物件中,從而使得各個物件之間的關系更加松散。
    * 在中介者模式中,物件之間不再直接相互互動,而是通過中介者來中轉訊息。 ......

    uj5u.com 2023-04-20 08:19:44 more
  • 露天煤礦現場調研和交流案例分享

    他們集團的資訊化公司及研究院在一個礦區正在做智能礦山的統一平臺的 試點,專案投資大概1億,包括了礦山的各方面的內容,顯示得我們這次交流有點多余。他們2年前開始做智能礦山的規劃,有很多煤礦行業專家的加持,他們的描述是非常完美,但是去年底應該上線的平臺,現在還沒有看到影子。他們確實有很多場景需求,但是被... ......

    uj5u.com 2023-04-20 08:19:07 more
  • 《社區人員管理》實戰案例設計&個人案例分享

    設計是一個讓人夢想成真程序,開始編碼、測驗、除錯之前進行需求分析和架構設計,才能保證關鍵方面都做正確 ......

    uj5u.com 2023-04-20 08:18:57 more
  • 軟體架構生態化-多角色交付的探索實踐

    作為一個技術架構師,不僅僅要緊跟行業技術趨勢,還要結合研發團隊現狀及痛點,探索新的交付方案。在日常中,你是否遇到如下問題 “ 業務需求排期長研發是瓶頸;非研發角色感受不到研發技改提效的變化;引入ISV 團隊又擔心質量和安全,培訓周期長“等等,基于此我們探索了一種新的技術體系及交付方案來解決如上問題。 ......

    uj5u.com 2023-04-20 08:18:49 more
  • 05單件模式

    #經典的單件模式 public class Singleton { private static Singleton uniqueInstance; //一個靜態變數持有Singleton類的唯一實體。 // 其他有用的實體變數寫在這里 //構造器宣告為私有,只有Singleton可以實體化這個類! ......

    uj5u.com 2023-04-19 08:42:51 more
  • 【架構與設計】常見微服務分層架構的區別和落地實踐

    軟體工程的方方面面都遵循一個最基本的道理:沒有銀彈,架構分層模型更是如此,每一種都有各自優缺點,所以請根據不同的業務場景,并遵循簡單、可演進這兩個重要的架構原則選擇合適的架構分層模型即可。 ......

    uj5u.com 2023-04-19 08:42:41 more