(莫寒C++學習之路)深入實踐C++模板編程讀書筆記01序章

概覽

這裏是通過記錄學習的紙質書籍《深入實踐C++模板編程》,因此放上華章的連接:

《深入實踐C++模板編程》

在我們日常學習C++的過程中,其實不難發現該語言是一門強類型的語言,是一個對於面向對象編程支持的非常好的語言。因此在強類型的要求下,如果只是單純地遵守**“萬物皆對象”**的法則,那麼必然會出現同一種操作/算法在不同的對象裏大量的重複實現的過程,或者有的程序員會採用,設計一個抽象的類型來專門設計容納一些通用的操作和算法。

但是,不然是後者高級的設計,對於大量的對象來說,工作起來依然是看起來捉襟見肘的,那麼對於一個C++程序員來講,是否理解模板編程的原理以及是否掌握模板編程的方法是普通C++程序員與高級C++程序員的分水嶺,今天我在此通過閱讀《深入實踐C++模板編程.溫宇傑》本書,記錄相關的學習過程,深入理解C++模板編程,進一步提升自己的編程能力,革新本人的C++編程思想。

該書主要包含以下知識點:

  • 函數模板的原理、用法、模板參數自動推導,以及函數體的實現方法;
  • 外名模板極其原理;
  • 類模板及其用法,異質鏈表、元組構造方法,以及類模板的靜態變量成員的處理;
  • 各種整數型、指針型以及引用型模板參數類型詳解;
  • 模板的特例、函數模板的重載、特例的寫法以及匹配規則;
  • 標準庫中的容器、迭代器和算法的定義、作用以及他們之間的關係
  • 標準庫中的序列型容器、關聯型容器、散列表容器、C++11新增的容器等多種類型容器的實現原理;
  • 多種分配器和迭代器的實現原理極其編寫方法;
  • 標準庫中常用算法的原理分析及其應用實踐;
  • 模板編程的常用技巧和元編程技術;
  • C++11新標準的模板新特性和新語法,以及C++11新標準中新增的語言特性解讀。

作者寫書的初衷

作者在工作實踐中,幾番研究與實踐下來,作者尚不敢稱對C++的模板編程掌握幾分,卻早已嘆於其對作者編程思想革新起到的巨大的幫助。從“萬物皆對象”,到如今漸漸變爲關注設計容器以及抽象算法的過程,這是拜模板所賜。讚歎之餘,不禁想嘗試寫一部書介紹C++中的模板編程,與人分享心得,於己則鞏固琢磨。不論章節條理,只求爲位到來。如果對讀者略有裨益,便是幸甚。

其實,很多程序員對於分享自己的見解和認識都還是比較熱衷的,相互學習各種思想,讓自己對於技術有更大的提升和思考。對於書中的技術和見解,像作者希望的那樣,希望能夠做到“擇其善者而從之,其不善者而改之”。

模板基礎

本部分的內容有:

  • Hello 模板
  • 類亦模板
  • 模板參數類型詳解
  • 凡事總有“特例”

Hello 模板

C++可以被稱爲強類型語言,凡是值必有類型,凡是變量聲明時必須聲明其類型,且變量類型“一生”不變。這樣就出現了,對於某些算法,針對不同數據其操作過程完全一致,只因爲操作數據類型不同,在C++中需實現爲不同的函數,難免重複了。

爲什麼需要使用模板

試想一個尋找最大值的案例,從一列值裏面計算最大的值的操作,用Python這種動態類型的語言來做,算法代碼就非常的簡潔:

def max_element(l):
	max_value = l[0]
	for elem in l[1:]if elem > max_value: max_value = elem
	return max_value
print(max_element([2, 0, 1, 1, 0, 8, 1, 8]))
print(max_element(['2011', 'August', '11', 'Thursday']))

Python實現上面的代碼,不需要操心類型的前提下,會發現實現是非常簡單的。但是,使用C++來實現一個通用的max_element函數就沒有那麼簡單了,C++中變量的類型不可變,不能像Python那樣用同一個函數、同一個變量處理不同的列表類型。如果沒有使用C++的模板類型的話,對於整數數組,代碼如下:

int max_element(int const *1, unsigned sz)
(
	int max_value = 1[0];
	for (unsigned i = 1; i < sz; ++i)
		if (1 [i] > max_value) max_value = 1[i];
	return max_value;
} 

對於字符數組,則函數代碼就改爲:

char max_element(char const *1, unsigned sz)
{
	char max_value = 1[0];
	for (unsigned i = 1; i < sz; ++i)
		if (1[i] > max_value) max_value = 1[i];
	return max_value;
}

會發現,除了函數返回值、第一個函數參數類型有變化之外,其他的內容基本是完全一摸一樣的。因此使用函數模板進行簡化就是一個值得學習的思路和方法。因此,我們與模板的第一次親密接觸就應該從模板函數開始了。函數模板是C++模板機制中的一種,其作用是爲不同類型的數據生成操作相同或相似的函數。

模板以關鍵字template開頭,其後是以一對尖括號劃分的模板參數列表。模板參數列 表中可以聲明多個模板參數,多個參數聲明之間以逗號分隔。使用函數模板實現就是以下的代碼:

template<typename T>
T const& max_element(T const *l, unsigned sz)
{
    T const *max_value(l);
    for (unsigned i = 1; i < sz; ++i)
        if (l[i] > *max_value) max_value = &(l[i]);
    return *max_value;
}

上面的例子中函數模板的函數體與前面兩份的函數代碼非常相似,不同之處在於凡是有關列表類型之處,皆由模板類型參數T代替。 另外,考慮到列表元素可能爲複雜自定義類型,其賦值會導致額外開銷,在模板中將max_ value改爲指針以避免無謂賦值。

那麼,就可以使用函數模板的過程來模擬實現上面兩個代碼的過程了:

int main()
{
    int l[] = {2, 0, 1, 1, 0, 8, 2, 5};
    char cl[] = "August";

    using namespace std;
    cout << max_element<int>(l, 8) << endl;
    cout << max_element<char>(cl, 6) << endl;

    return 0;
}

可以像調用一個普通函數那樣調用函數模板。不同之處在於,調用函數模板時需要指明模板參數的“值”。對於類型參數,其“值”即爲具體類型如int、char或者是用戶自定義的類。根據所給定的模板參數值以及完整的函數模板聲明,編譯器可自動生成一個對所需數據類型進行操作的函數,稱爲函數模板實例。模板參數的值在函數模板名後接尖括號內聲明。

模板參數自動推導

實際上,在C++語言中實現了這一自動推導模板參數值的功能。凡是可以推導出的模板參數"值”,就無需在模板實參列表中寫明。因此,例1.4中main函數的兩次max_ element調用,可以簡寫成以下形式:

std::cout << max_element(1, 8) << std::endl;
std::cout << max_element(cl, 6) << std::endl;

從而使得模板調用看起來與普通函數調用無異,也使代碼看起來更整潔。
利用模板參數推導時需要注意以下幾點:

  • 編譯器只根據函數調用時給出的實參列表來推導模板參數值,與函數參數類型無關 的模板參數其值無法推導。
  • 與函數返回值相關的模板參數其值也無法推導。
  • 所有可推導模板參數必須是連續位於模板參數列表尾部,中間不能有不可推導的模 板參數。

下面是一個簡單的多模板參數的函數使用案例:

#include <iostream>

template<typename T0,
         typename T1,
         typename T2,
         typename T3,
         typename T4>
T2 func(T1 v1, T3 v3, T4 v4);

int main() {

    double sv2;

    using namespace std;
    sv2 = func<double, int, int>(1, 2, 3);
    cout << "\tsv2: " << sv2 << endl;

    sv2 = func<double, int, int>(1, 2, 3);
    cout << "\tsv2: " << sv2 << endl;

    sv2 = func<double, int, int>(1, 0.1, 0.1);
    cout << "\tsv2: " << sv2 << endl;

    sv2 = func<int, double, double>(0.1, 0.1, 0.1);
    cout << "\tsv2: " << sv2 << endl;
}

template<typename T0,
         typename T1,
         typename T2,
         typename T3,
         typename T4>
T2 func(T1 v1, T3 v3, T4 v4)
{
    T0 static sv0 = T0(0);
    T2 static sv2 = T2(0);

    std::cout << "\tv1: " << v1
              << "\tv3: " << v3
              << "\tv4: " << v4
              << "\t|| sv0: " << sv0;
    T2 v2 = sv2;

    sv0 -= 1;
    sv2 -= 1;

    return v2;
}

模板實參列表中只可將T3及T4省略,而T0、T1及T2 不能省略。

模板參數默認值

最新的C++11標準允許爲函數模板參數賦默認值,在爲func中無法根據函數參數推導的模板參數賦予默認值後,調用模板時的模板實參列表可以完全省略。例如將func的聲明改爲以下形式:

tempiate<	typename	T0	= float,
			typename	Tl,	
			typename	T2	= float,
			typename	T3,	
			typename	T4>	
T0 func (T1 vl, T3	v3,	T4 v4)

如何處理函數模板中的函數體

既然編譯器是在需要生成模板實例時自動生成,這就帶來一個與傳統C/C++編程習慣 的衝突,即函數模板中的函數體應該放在哪裏。

hpp文件還是cpp文件

按照C++語言習慣,普通函數及類的聲明應該放在一個頭文件(通常以h、hpp或者 hh爲擴展名)裏,而將其實現放在一個主代碼文件(通常以c、cpp或者cc爲擴展名)裏,這樣便於將代碼分散編譯到多個目標文件中,最後通過鏈接形成一個完整的目標文件。但是由於模板的實現是隨用隨生成,並不存在真實的函數實現代碼,如果還是按照“頭文件放聲明,主文件放實現”的做法,則會導致編譯失敗。

template<typename T>
T const& func(T const &v) {return v;}

template int const& func(int const &v);

例中用到一種尚未介紹過的語法——明確生成模板實例。當關鍵字template後沒有模板參數列表,而是一個函數聲明時,意味着指示編譯器根據此函數聲明尋找合適的模板實現。當然,所聲明函數必須與某一已知模板函數同名,並且其參數可用模板匹配。

例中將函數聲明爲T=int,從而在編譯func2.cpp時,會在目標文件中生成 func< int >的代碼而不會在鏈接時產生錯誤。但這只是權宜之計,倘若還需要func< float > 或 者func< char >,那麼在代碼文件中還得增加相應的語句,以促使編譯器生成相應函數模板實例。如此一來, 又變成由人工生成模板實例,違背了當初由編譯器隨用隨生成的初衷。

可見,雖然模板中的函數也可以有自己的聲明和實現,但編譯器不會在讀到模板實現時立刻生成實際代碼,因爲具體的模板參數類型還未知,無法進行編譯。對於編譯器來說, 模板實現也是一種聲明,聲明如何自動生成代碼。所以模板的實現也應該放在頭文件內, 這樣,在其他代碼文件中可以直接將模板的實現也包含進來,當需要生成模板實例時,編譯器可根據已知模板實現當場生成,而無需依賴在別的目標文件中生成的模板實例。

但這樣會帶來另一個問題,即重複模板實例。對此問題,C++標準中給出的解決方案是:在鏈接時識別及合併等價的模板實例。

caller1.cpp

//======================================
//文件名caller1.cpp
#include <iostream>

template<typename T>
void func(T const &v)
{
    std::cout << "func1: " << v << std::endl;
}

void caller1() {
    func(1);
    func(0.1);
}


caller2.cpp

//======================================
//文件名caller2.cpp
#include <iostream>

template<typename T>
void func(T const &v)
{
    std::cout << "func2: " << v << std::endl;
}

void caller2() {
    func(2);
    func(0.2f);
}

main.cpp

//======================================
//文件名main.cpp
void caller1();
void caller2();

int main()
{
    caller1();
    caller2();
    return 0;
}

這兩個目標文件再與main.cpp編譯所得目標文件共同鏈接成可執行文件後會出現什麼情況呢?
執行結果如下:

$ ./a.out
func1 : 1
func1: 0.1
func1: 2
func2: 0.2

由此例的運行結果可以推知,鏈接器不考慮函數具體內容,僅僅通過函數名模板實參列表以及**參數列表等“接口”**信息來判斷兩個函數是否等價。

常情況下,根據函數接口判斷等價函數實例並在鏈接時合併的簡單方法,可以有效解決重複模板實例的問題。但正如例中所演示那樣,使用這種方法也有弊端。倘若有不同的作者在寫不同的模板庫時,碰巧用到同一函數名以及相同的模板參數列表和函數形參列表,對於一些簡單函數,這也是非常有可能的。

降低落入這一陷阱的可能性,最好的方法就是避免使用相同的函數名。此時,C++中的命名空間(namespace)機制就顯得異常重要。

尷尬的 Export Template

實際上,除了將模板的聲明與實現一同放在頭文件中編譯之外,C++標準(98版)還 提供了另一種組織模板代碼的方式——Export Template(暫且稱爲外名模板)。而定義與實現放在同一頭文件中的模板可稱爲內名模板

外名模板提倡將模板的聲明與實現分別寫在頭文件和主文件內,如此實現模板需要在模板聲明前加關鍵字export以標記外名模板,可以看如下代碼:

square.hpp

//======================================
//文件名:square.hpp
export template<typename T>
T square(T const &v);

square.cpp

//======================================
//文件名:square.cpp
#include "square.hpp"
export template<typename T>
T square(T const &v) {return v * v;}

main.cpp

//======================================
//文件名:main.cpp
#include "square.hpp"
#include <iostream>

int main()
{
    std::cout << square(0.1f) << std::endl;
}

而在鏈接時,會因爲無法找到main.cpp中 所需要的square< float >模板實例而報錯。例如用GCC編譯時情況大抵如此,並且GCC還會給出一個“不支持export”的警告。

由於實現成本高,而且需求可替代,使得外名模板不受編譯器開發者的青睞並且最終 被逐出最新的C++11標準。雖然如此,這樣一種設計思想仍然值得了解,並且現在也有完全支持外名模板的編譯器可供使用。對於看重編譯速度及目標碼質量而不太看重代碼移植 性的開發者來說,外名模板仍然是一種不錯的選擇。

本章小結

本章所討論的只是C++模板編程中的一小部分——函數模板而已。通過對函數模板編譯過程的介紹,希望能對讀者理解模板的本質有所幫助。

模板本身不是可編譯的代碼,而是用來指導編譯器生成可編譯代碼的文本。函數模板 實際上是提取一系列具有相同(或者近似)操作流程的函數中的共性並給予規範描述,從而使得編譯器可以在需要時,根據描述自動生成相應可編譯代碼並編譯。有了模板的支持, 程序員的工作從體力勞動向着腦力勞動又前進了一小步。但是如果僅僅是函數模板而已, 這一小步並不會走岀太遠,仍然有許多問題是單純用函數模板無法解決的,另一小步便是第2章將要介紹的類模板及其功用。

在這裏插入圖片描述

成長,就是一個不動聲色的過程,一個人熬過一些苦,才能無所不能。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章