天天看點

(一〇六)函數模闆

函數模闆的意義在于,可以在不同的參數下,起到同樣的作用。

按照教程所說,它們使用泛型來定義函數,其中泛型可用具體的類型(如int、double)替換。通過将類型作為參數傳遞給模闆,可使編譯器生成該類型的函數。

由于模闆允許以泛型(而不是具體的類型)的方式編寫程式,是以在有時也會被稱為是通用程式設計。

由于類型是用參數表示的,是以模闆特性有時也被稱為參數化類型。

格式:

解釋:

①xx指的是用于替代類型的名字,他将在後面被放置在平常置于 類型 的位置。例如,int name; 在這裡是xx name; 

在參數清單,也變成xx &a和xx &b

xx可以用其他替代,就像變量名那樣。

②template和typename是必須的。

使用函數模闆的原因:

①以以上代碼為例,作用是交換兩個變量的值。

假設要交換2個int值,那麼函數頭為:void aaa(int &a,int&b);

假如又要交換2個double值,那麼又需要一個函數:void bbb(double&a,double &b);

如果又需要交換2個char值,那麼需要第三個函數。

使用函數模闆的話,就可以合三(或更多)為一,降低工作量,減少出錯幾率。

②在c++98加入關鍵字typename之前,使用class,來建立模闆,方法同typename。

如果不考慮向後相容的問題,則應使用typename。若考慮到相容,則可使用class。

如代碼:

輸出:

對模闆的解釋:

①函數模闆不能縮短可執行程式,最終的代碼将不包含函數模闆,隻包含根據模闆、參數而為程式生成的實際函數。

例如代碼中的函數abc,最終将仍由兩個函數來起作用。分别是void abc(int&a, int *b)和vodi abc(char&a , char &b);

②使用函數模闆的好處,它使生成多個函數定義更簡單、可靠。

例如上述代碼,如果手動寫的話,需要寫兩組(每組2個)重載函數。

③常見的情形是:将模闆放入頭檔案之中,并在需要使用模闆的檔案中包含頭檔案。(頭檔案在第九章,是以還沒學到)

④并非所有函數模闆的參數類型,都是泛型(例如之前的xx),也可以是具體的類型。例如将 xx&b改為char &b等。

⑤注意,多個函數模闆,每個之前都要加上template<typename 名字>這一行代碼。

重載的模闆:

使用函數重載的模闆時,和函數重載的知識一樣,需要用特征标區分開。

例如:

注意:

①因為是函數模闆,是以xx可以充當多種類型名使用。是以,需要一個額外的參數,來區分重載的兩個函數。

②也可以用不同類型的參數來區分開。例如:

當傳遞的參數為基本類型時,如int、double、char、string等,則使用第一個函數模闆;

若傳遞的參數為指針時,則使用第二個模闆(原因涉及到函數最佳化比對,見後);

總結:

①從以上可以看出,模闆支援int、double、char、string,以及指針。由此可以推斷出,模闆也一定支援long、longlong、float、long double等。

但僅以上代碼來說,不支援(?)array、vector。因為這兩種既是模闆。(可能也因為能顯示數組,是以不一樣?)

就 以上代碼 而言,也不支援結構(模闆是允許結構類型的,隻是這個不支援)。如果需要支援結構,則在編寫代碼的時候需要注意。例如,以下模闆就适合結構作為參數名使用:

原因在于模闆的局限性。

模闆的局限性:

如果使用模闆,那麼參數需要能滿足模闆内的函數。

以上面的template <typename

xx>void aaa(xx

a, xx

b) 為例,函數内部有a+b的代碼,在套用string類的變量aa和bb是可行的。

假如是a*b的代碼,那麼必然不支援string類,于是,隻能使用int、double等算數類型的變量。

是以,這就是模闆的局限性。

又比如假如使用結構,以以上代碼而言,假如需要使用結構。那麼參數要麼是結構中的某個變量,要麼在函數内的代碼,預先附加逗号運算符。

例如代碼1:

在這種模闆裡,直接附加逗号運算符,是以,如果遇見非結構,或者其他結構裡面,沒有變量名為a的,就無法調用該函數。

例子2:

僅修改關鍵代碼:

......

aaa(l.a, p.a);

void aaa(xx

b)   

{

    cout<< a <<

", " << b << endl;

}

在這個模闆中,傳遞的參數是結構的某個變量,于是就可以适用非結構的情況。

個人感覺,模闆有一些類似文本替換的特性。即用int、double等類型,替換模闆的類型的名字(如上面代碼之中的xx),然後用參數替代模闆内的各個變量。

假如替換後不能正常運作,則模闆無法使用;

假如替換後可以運作(c++允許這種方法),則模闆可以使用。

是以,使用模闆,隻需要參數代入後,模闆能正常運作,結果符合預期,就可以用。

顯式具體化:

顯示具體化,就其用途來說,編譯器在找到與函數調用時比對的具體化定義時,将使用該定義,而不是使用模闆。

也就是說,如果函數在調用時,有比對的具體化定義,他會優先使用它,如果沒有,于是再找符合的模闆。

具體化機制的形式,随着c++的演變而不斷變化。(是以說可能有多種方式咯)

根據教程,以下是c++标準定義的形式——第三代具體化(iso/ansic++标準),c++98标準使用以下的方法。

①對于給定的函數名,可以有非模闆函數、模闆函數和顯式具體化模闆函數、以及他們的重載版本。

顯式具體化的原型的格式:template<>void函數名<類型名>(參數清單)

注①:參數清單像非模闆函數那樣使用

注②:<類型名>可以被省略

非模闆函數:void abc(int);

模闆函數:template <class xx>void abc (xx);

顯式具體化函數:template<>voidabc<double>(double);

重載版本(依次為):

void abc(int, int);

template<class xx>voidabc(xx a,xx b);

template<>voidabc<double>(double a,double b);

②顯式具體化的原型和定義,應以template<>打頭,并通過名稱來指出類型。

如:template<>void abc<int>(int a); 這樣,在使用int變量的時候,就會優先使用這個顯示具體化的函數定義。

③具體化優先于模闆,而非模闆函數優先于具體化和正常模闆。

①單用指針作為參數時,若無相應的函數(參數為指針的),則調用模闆,輸出為指針(這裡為位址);

②若使用指針外加解除運算符,則要看指針指向的位址的值是什麼類型,如果該類型有對應的非模闆函數、或顯式具體化,則按優先順序調用,若無,則使用模闆。

③一個參數,如果比對非模闆函數,那麼即使有适合他的顯式具體化和模闆函數,它依然會調用非模闆函數;

如果他無法比對模闆函數,那麼看是否有比對他的顯式具體化,如果有,則比對之,忽視模闆函數;

如果無法比對顯式具體化,則看是否有比對它的模闆函數,如果有,則比對之;

如果以上三者都不比對,那麼編譯器顯示函數調用錯誤。

④同名函數可以同時存在非模闆函數(及重載版本)、顯式具體化(及重載版本)、模闆函數(及重載版本);

⑤疑問:為什麼在有非模闆函數的情況下,使用顯式具體化?不能用非模闆函數替代顯式具體化麼?

某個解釋(但我沒看懂):某些時候必須使用顯式特化,譬如模闆元程式設計的時候作為遞歸的終止條件

template<int ival>int func(int*p)

  return *p+func<ival-1>(p+1);

template<>int func<1>func(int *p)

return *p;

void main()

 int ia[1000];

 func<1000>(ia);

執行個體化和具體化:

前提:函數在使用模闆的時候,模闆本身并不會生成函數定義,而是根據傳遞的參數來生成相應類型的定義,模闆是一個生成函數定義的方案。

編譯器在使用模闆為特定類型生成函數定義時,得到的是函數執行個體(instantiation)。

例如:在上面那一個代碼之中,調用函數abc(e); ,由于參數e是char類型,不比對非模闆函數和顯式具體化,于是調用模闆void abc(xx),導緻編譯器生成了abc()的一個執行個體。該執行個體為使用int類型。

模闆并非函數定義,但使用int的模闆執行個體(void abc(int))是函數定義,這種執行個體化方式稱為隐式執行個體化(implicit instantiation)(和顯式執行個體化相對應,注意,不是顯式具體化),編譯器之是以知道需要進行定義,是因為程式在調用模闆abc()函數時,提供了int參數。(于是,對應提供double參數就生成使用double類型abc()的執行個體——如果沒有顯式執行個體化和非模闆函數的話)。

顯式執行個體化(explicit instantiation)在最初編譯器是不允許的,隻允許模闆通過參數來隐式執行個體化,但後來c++允許了顯式執行個體化,這意味着編譯器可以直接指令編譯器建立特定的執行個體,如abc<int>()

——具體來說,例如abc調用兩個參數,一個是int a一個是double b,使用模闆的話,需要兩個參數類型相同(比如都是int),使用顯式執行個體化調用函數abc<int>(a,b),則b被強制轉換為int類型,于是模闆建立了一個int類型的執行個體。

其文法是:template 傳回類型函數名<類型名>(參數清單);

例如:template voidabc<int>(int);

在能實作這種特性的編譯器看到上述聲明後,将使用函數模闆生成一個使用int類型的執行個體。也就是說,該聲明的意思是“使用函數模闆生成int類型的函數定義。”

與顯式執行個體化不同,顯式具體化使用以下兩個等價的聲明之一:

(1)template<>void函數名<類型名>(參數清單);

如:template<>void abc<int>(int);

(2)template<>void函數名(參數清單);

如:template<>void abc(int);

顯式具體化和顯示執行個體化的差別在于,顯式具體化的聲明的意思是:“不要使用模闆來生成函數定義,而應該使用專門為int類型顯示地定義的函數定義”。也就是說,顯式具體化必須有自己的函數定義(那顯示執行個體化需要有麼?應該不需要吧?)

根據我查資料來看,顯示執行個體化的一個用途,是在多個.cpp檔案裡,共享一個模闆,

你是隻有一個 cpp 的情況. 如果有多個 cpp 檔案再使用這個模版, 你必須把它放在頭檔案裡, 然後每個 cpp 都要 #include 這個頭檔案. 顯示執行個體化之後頭檔案裡隻需要聲明, 然後在其中一個 cpp 裡面實作并顯示執行個體化, 其它的 cpp 就可以直接用了.

具體可以 google 一下 "模版聲明實作分離"

雖然并看不懂。

notice:試圖在同一個檔案(或轉換單元)中使用同一種類型的顯式執行個體和顯式具體化将出錯。

比較常見的顯式執行個體化的使用方法是:

         doublea = 1.1;

         intb = 2;

         abc<int>(a,b);

//強制轉換為int類型執行個體化

         abc<double>(a,b);  

//強制轉換為double類型執行個體化

一般情況下,使用template voidabc<int>(int,int)這樣的,似乎并沒有什麼必要,應該在需要的時候才使用顯式執行個體化。大部分時候隐式執行個體化貌似可以搞定。

編譯器選擇哪個函數版本:

當函數涉及到函數重載(不同參數清單)、函數模闆和函數模闆重載(可能建立多種隐式執行個體化)時,c++需要一個良好的政策來決定為函數比對哪一個函數定義,尤其是在有多個參數時,這個過程稱為重載解析(overloading resolution)。

過程大緻如下進行:

①建立候選函數清單。其中包含與被調用函數的名稱相同的函數和模闆函數;(即如果調用1個參數,使用該名稱的函數,凡是隻需要1個參數但不考慮類型,就将被添加到可行函數清單)(隻考慮特征标,不考慮傳回值);

②使用候選函數清單建立可行函數清單。這些都是參數數目正确的函數,為此有一個隐式轉換序列,其中包含實參類型與相應的形參類型完全比對的情況。例如,使用float參數的函數調用可以将該參數轉換為double,進而與double形參比對,而模闆可以為float生成一個執行個體。(而若函數特征标是指針,而float參數不能轉換為指針,則這個函數不會被添加到可行函數清單之中)

③确定是否有最佳的可行函數。如果有,則使用它,如果沒有,則該函數調用出錯。

确定最佳的可行函數的優先級:

①完全比對,但正常函數優先于模闆;(按照比對順序,非模闆優先于顯式具體化優先于模闆)

②提升轉換(例如char(這個可以表示為anscii編碼,是int值)、short之類可以自動轉換為int,float可以自動轉換為double(都是浮點類型));

③标準轉換(例如int轉換為char,long轉換double(非浮點轉換為浮點));

④使用者定義的轉換,如類聲明中定義的轉換。——這個不了解

提升轉換指:

資料類型的記憶體大小是不一樣的,有的小(如char),有的大(如int)

提升轉換就是将小的換成大的

比如:

char a = 'a';

int i = a;

在執行指派語句時,由于兩者的資料類型不一樣,是以先将字元變量a隐式轉換為int型

這就是提升轉換

關于記憶體大小(在我win7 64位電腦上):

char(1)<int(4)=float(4)=long(4)<double(8)=longlong(8)=long double(8);

标準轉換包括:

a、從任何整數類型或枚舉類型到其他整數類型(除了提升) ——long轉換int

b、從任何浮點類型到其他浮點類型(除了提升轉換)——double到float

c、從任何整數類型到浮點類型,或者反之——int到double、float到int

d、整數0到指針類型、任何指針類型到 void 指針類型——不懂

e、從任何整數類型、浮點類型、枚舉類型、指針類型到bool

感覺是:占記憶體小的到占記憶體大的類型是提升轉換,其他是标準轉換。

提升轉換:char->short(及以上),short到int(及同類),但似乎int到long long并不算。

其他為标準轉換。

如有函數調用:

int a=1;

abc(a);

首先建立候選函數清單(特征标數量相同);

void abc(int*);     //1#  非模闆函數

void abc(double);       //2#  非模闆函數

char* abc(char);        //3#  非模闆函數

char abc(char*);        //4#  非模闆函數

double abc(float);      //5#  非模闆函數

double* abc(double*);      //6#  非模闆函數

template<class xx>voidabc(xx);     //7#          模闆函數

template<>voidabc<int>(int);        //8#   顯式具體化

注意:假如有模闆template<class xx>xxabc(xx); 由于特征标和7#完全相同,隻有傳回值不同,而函數重載最重要的是特征表不同,在函數模闆裡,通常展現為函數參數數目不同,是以應判定其與7#不能共存。

然後建立可行函數清單:

1#是指針,無法被隐式轉換,排除;

2#是int可以被轉換為double,添加;

3#是int可以被轉換為char,添加;

4#是指針,排除;

5#是float,添加;

6#是指針,排除;

7#可以,添加;

8#是顯式具體化,int可以,添加;

于是可行函數清單有:

下來确定優先級:

2#,int轉換為double,标準轉換(參照标準轉換c,浮點和整型互相轉換),優先級③;

3#,int轉換為char,标準轉換(整型到整型,非提升),優先級③;

5#,int轉換為float,标準轉換(整型轉浮點),優先級③;

7#,可以建立int執行個體,完全比對,優先級①;

8#,參數為int,完全比對,優先級①;

于是,有兩個優先級①的,這個時候,考慮到顯式具體化的優先級高于模闆函數,于是,在7#、8#之間,選擇8#。

注:如果去掉模闆和函數具體化,則2#、3#、5#都為标準轉換,因為優先級相同。

完全比對允許的無關緊要的轉換:

從實參

到形參

type

type &

type []

type *

type (argument-list)

type (*)(argument-list)——這條不了解

const type

volatile type

type*

const type*

注:就象大家更熟悉的const一樣,volatile是一個類型修飾符(type

specifier)。它是被設計用來修飾被不同線程通路和修改的變量。

在以上的基礎上,即完全比對,函數仍有優先級用于比對函數。

①指向非const指針的調用(但隻對指針适用),優先比對非const,其次才比對const;使用const,則隻能比對const。

②如上面多次強調,非模闆函數》》》顯式具體化》》》模闆函數(生成的隐式具體化)

③轉換最小為原則。

例如兩個模闆,一個是非指針模闆,一個是指針模闆。

對于非指針模闆來說,如果傳遞給他指針,他會生成指針的執行個體;

一個位址傳遞給兩個模闆時,

非指針模闆說,我可以根據參數(指針)生成執行個體(帶指針的執行個體);

指針模闆說,我本身就是指針模闆,是以直接可以生成指針執行個體;

于是調用了指針模闆。

template<class xx>voidabc(xx);     //非指針模闆

template<class xx>voidabc(xx*);   //指針模闆

int*a=new int;

abc(a);   //傳遞的是一個指針

即若同時有指針和非指針模闆,且特征标一樣,那麼指針會優先比對指針,非指針會優先比對非指針

使用者自定義使用哪個函數:

如以上所說,非模闆函數優先于顯式具體化優先于模闆函數。

但假如在同時存在的時候,要求優先調用模闆函數,則使用在類型名和參數的括号之間,加入“<>”。例如abc<>(1);   這樣會優先調用模闆函數。

如果在<>加入類型名,則是顯式執行個體化,帶強制轉換功能。例如abc<int>(1.1),那麼1.1會被強制轉換為1,再使用模闆函數。

①加入<>則優先使用模闆(顯式具體化也是模闆範圍之内);

②使用模闆時,優先使用顯式具體化。

如abc<double>(a);是顯式執行個體化,a為int類型,被強制轉換為double,于是比對顯式具體化。

③顯式執行個體化時,優先強制轉換,然後根據強制轉換後的情況,決定比對哪一個(但不會去比對非模闆函數)。

④如果能優先比對顯式具體化,那麼加不加“<>”似乎并沒有什麼影響。

有多個參數時的函數:

當有多個參數的函數調用與有多個參數的原型進行比對時,情況将非常複雜。

編譯器必須考慮所有參數的比對情況。如果找到比其他可行函數都合适的函數,則選擇該函數。

一個函數要比其他函數都合适:

①所有參數的比對程度都是最高,或者最高之一;

②至少一個參數的比對程度最高,沒有之一;

也就是說,哪怕有一個參數,不是最高或之一,就無法作為最合适。

模闆函數的發展:

(一)關鍵字:decltype

假如有這麼一個情況,函數的參數為兩個不同類型的變量,模闆如下寫:

template<class t1,classt2>void abc(t1 a,t2 b);

那麼假如要在函數内部,讓a和b相加(即a+b),那麼他們的和是什麼類型?在調用函數前,并不能确定。

于是這麼寫:

template<classt1,

class t2>void abc(t1

a, t2

b)

         ?type ? c = a + b;      //注意,這裡的?type?并不能用,編譯器會指出未聲明的辨別符

這個時候,c的類型,根據實際相加時的情況決定,是以,隻能使用auto。

例如int和int類型相加為int,int和double相加為double,總之,會導緻整型提升,然後再看結果如何,決定c的類型。

是以,c++98中沒法聲明c的類型(注意,auto是c++11中加入的)。

但可以這麼寫:

         autoc =

a + b;          

//c++11可用

c++11中新增的關鍵字decltype提供了解決方案,他可以這麼用:

int x;

decltype(x) y;

意思是,讓y的類型同x。

括号裡可以使用表達式。

如:decltype(a+b)c;

但這個時候,c隻是和a+b的類型相同,但并不是把a+b的值賦給了c。

是以還需要 c=a+b;

也可以将其合并在一起。如上述代碼可以改為:

         decltype(a+

b)c = a+

b;

在以上代碼之中,decltype看似和auto相同,但若括号裡是一個函數,即若有函數:

int m(int a)

       cout<<a<<endl;

       return a;

decltype:

decltype(m(1)) c;

根據m的傳回類型是int,于是c是int類型。

auto:

auto c=m(1);      

這個時候,c也是int類型,但他會執行m函數,并給c指派1。

decltype也可以搭配typedef(别名使用):

         typedefdecltype(a+

b) xx;

         xx c =

a + b;

         xx d =

a - b;

在這個代碼之中,xx就是a+b的和的類型的别名(比如說double類型);

那麼xx c和xx d都是double類型。

(二)另一種函數聲明文法(c++11後置傳回類型)

例如上面的代碼:

class t2>傳回值類型abc(t1

a,t2

         returnc;

這個時候,傳回值類型c是什麼,我們并不能知道。

同時,又因為此時還沒聲明a和b,他們不在作用域之内(編譯器看不到他們,無法使用)。隻有在聲明參數後,才能使用decltype。

也就是說,函數原型不能這麼寫:

class t2> decltype(a+

b) abc(t1a,t2

為此,c++新增了一種聲明和定義函數的文法,下面使用内置類型來說明這種文法的工作原理。

int a(intm,

int n);

可以寫為:

autoa(int m,

intn)->int;

這樣的話,将傳回類型移到了參數聲明之後,->int被稱為後置傳回類型(trailing return type)。其中,auto在這裡是一個占位符,表示後置傳回類型提供的類型,這是c++11給auto新增的一種角色。這種文法也可以用于函數定義:

如上面的改為:

class t2>

auto abc(t1

b)->decltype(a+ b)

在這種情況下,decltype在參數之後,參數a和b在作用域之内,于是可以使用了。也可以正确提供類型了。

繼續閱讀