先序文章請(qǐng)看
C++模板元編程詳細(xì)教程(之一)
C++模板元編程詳細(xì)教程(之二)
有了前兩篇的基礎(chǔ),相信大家對(duì)模板編程已經(jīng)有一點(diǎn)初步的感覺(jué)了。趁熱打鐵,這一篇我們主要來(lái)介紹一下模板特化。
首先來(lái)看一下下面的例子:
templatevoid add(T &t1, const T &t2) {t1 += t2;
}
上面是一個(gè)簡(jiǎn)單的模板函數(shù),用于把第二個(gè)參數(shù)的值加到第一個(gè)參數(shù)中去。這個(gè)模板函數(shù)對(duì)于基本數(shù)據(jù)類型的實(shí)例化都是沒(méi)什么問(wèn)題的,但是如果是字符串的話,那將會(huì)有問(wèn)題:
void Demo() {int a = 1, b = 3;
add(a, b); // add,調(diào)用符合預(yù)期
char c1[16] = "abc";
char c2[] = "123";
add(c1, c2); // add, 調(diào)用不符合預(yù)期
}
這里的問(wèn)題就在于,對(duì)于字符串類型(這里指原始C字符串,而不是std::string
)來(lái)說(shuō),「相加」并不是簡(jiǎn)單的+=
,因?yàn)樽址饕怯米址羔榿?lái)承載的,指針相加是不合預(yù)期的。我們希望的是字符串拼接。
因此,我們希望,單獨(dú)針對(duì)于char *
的實(shí)例化能夠擁有不同的行為,而不遵從「通用模板」中的定義。這種語(yǔ)法支持就叫做「特化」,或「特例」。可以理解為,針對(duì)于模板參數(shù)是某種特殊情況下進(jìn)行的特殊實(shí)現(xiàn)。
因此,我們?cè)谕ㄓ媚0宓亩x基礎(chǔ)上,再針對(duì)char *
類型定義一個(gè)特化:
#includetemplatevoid add(T &t1, const T &t2) {t1 += t2;
}
template<>// 模板特化也要用模板前綴,但由于已經(jīng)特化了,所以參數(shù)為空
void add(char *&t1, char *const &t2) {// 特化要指定模板參數(shù),模板體中也要使用具體的類型
std::strcat(t1, t2);
}
void Demo() {int a = 1, b = 3;
add(a, b); // add是通過(guò)通用模板生成的,因此本質(zhì)是a += b,符合預(yù)期
char c1[16] = "abc";
char c2[] = "123";
add(c1, c2); // add有定義特化,所以直接調(diào)用特化函數(shù),因此本質(zhì)是strcat(c1, c2),符合預(yù)期
}
上例簡(jiǎn)單展示了一下模板特化目標(biāo)解決的問(wèn)題,和其基本的語(yǔ)法。但其實(shí)模板特化遠(yuǎn)不止如此,它有著巨大的潛力。
模板的特化分兩種情況,一種是全特化(有的地方也叫特例),一種是偏特化(有的地方也叫部分特化)。全特化相對(duì)簡(jiǎn)單一些,筆者會(huì)先來(lái)介紹。而偏特化會(huì)伴隨SFINAE理論,它將會(huì)成為模板元編程最核心的理論基礎(chǔ)。
全特化與模板的鏈接方式首先復(fù)習(xí)一下我們?cè)陂_(kāi)篇時(shí)候所提到的一個(gè)非常重要的概念。**模板本身不是可使用的代碼,而是一種代碼的升成方法。需要經(jīng)過(guò)實(shí)例化后才能成為實(shí)際可用的代碼。**比如說(shuō)模板函數(shù)需要指定模板參數(shù)(可以是顯式指定,也可以是編譯器自動(dòng)推導(dǎo))實(shí)例化后,才能成為函數(shù),同理,模板類也需要實(shí)例化后才能成為類。
然而「全特化」就是說(shuō),當(dāng)所有模板參數(shù)都指定了的時(shí)候,才叫「全」。那么上一節(jié)中add
的示例就是一個(gè)全特化,因?yàn)樗局挥幸粋€(gè)模板參數(shù),把它特化了自然是「完全」特化的。
而要談到全特化,就不得不談到一個(gè)非常容易踩坑的點(diǎn),那就是模板的鏈接方式。在一個(gè)單獨(dú)的.cpp文件中使用模板并不會(huì)有什么鏈接性問(wèn)題,但如果在多個(gè)文件中都要使用呢?自然要通過(guò)「頭文件聲明+鏈接」的方式來(lái)完成了。
但模板本身又很特殊,它本身不是可用的代碼,而是代碼生成器,因此編譯器會(huì)在編譯期用模板來(lái)生成代碼,注意,這個(gè)時(shí)候還沒(méi)有開(kāi)始鏈接!所以問(wèn)題就產(chǎn)生了,假如我們按照直覺(jué)和習(xí)慣,把模板的聲明和定義分文件來(lái)寫(xiě),會(huì)怎么樣呢?請(qǐng)看下面示例:
tmp.h
#pragma once
templatevoid f(const T &t); // 聲明
tmp.cpp
#include "tmp.h"
templatevoid f(const T &t) {} // 實(shí)現(xiàn)
main.cpp
#include "tmp.h"
int main() {f(1); // ff(1.0); // freturn 0;
}
如果我們真的這么做了,你會(huì)發(fā)現(xiàn)鏈接時(shí)會(huì)報(bào)錯(cuò)。原因是這樣的,我們?cè)趖mp.h中的這種寫(xiě)法,并不是「聲明了一個(gè)模板函數(shù)」,模板函數(shù)本不是函數(shù),是不需要聲明的,大家記住模板永遠(yuǎn)是生成代碼的工具。所以tmp.h中的寫(xiě)法是「聲明了一組函數(shù)」,包括我們?cè)?code>main函數(shù)中使用的f
和f
,之所以能通過(guò)編譯,就是因?yàn)閠mp.h中存在它們的聲明。換句話說(shuō),template
相當(dāng)于void f
,void f
,void f
……這一系列的「函數(shù)聲明」。
所以,編譯是沒(méi)問(wèn)題的,但是鏈接的時(shí)候會(huì)報(bào)找不到f
和f
的實(shí)現(xiàn)。這是為什么呢?明明我在tmp.cpp中實(shí)現(xiàn)了呀!那我們來(lái)「換位思考一下」,假如你是編譯器,我們知道「編譯」過(guò)程是單文件行為,那么你現(xiàn)在來(lái)編譯main.cpp,首先進(jìn)行預(yù)處理,把#include
替換成對(duì)應(yīng)頭文件內(nèi)容,那么main.cpp就變成了:
templatevoid f(const T &t);
int main() {f(1); // ff(1.0); // freturn 0;
}
上面的f
是函數(shù)聲明,下面編譯主函數(shù)的時(shí)候,根據(jù)參數(shù)推導(dǎo)出了f
和f
,于是,通過(guò)上面的「模板函數(shù)聲明」生成了2條實(shí)際的「函數(shù)聲明」語(yǔ)句,也就是:
void f(const int &);
void f(const double &);
調(diào)用都是符合聲明的,OK,結(jié)束編譯,我們得到了main.o。
好了,下面我們來(lái)編譯tmp.cpp。同理,先做預(yù)處理,得到了:
templatevoid f(const T &t);
templatevoid f(const T &t) {}
這時(shí),問(wèn)題的關(guān)鍵點(diǎn)來(lái)了!,這個(gè)模板函數(shù)f
在當(dāng)前這個(gè)編譯單元中,并沒(méi)有任何實(shí)例化,那么你自然就不知道應(yīng)當(dāng)按這個(gè)模板來(lái)生成哪些實(shí)例。所以,你只能什么都不做,很無(wú)奈地生成了一個(gè)空白的tmp.o。
最后,main.o和tmp.o鏈接,main.o中的f
和f
都找不到實(shí)現(xiàn),所以鏈接報(bào)錯(cuò)。
這就是模板的鏈接方式問(wèn)題,由于模板都是編譯期進(jìn)行實(shí)例化,因此,必須在編譯期就得知道需要哪些實(shí)例化,然后把這些實(shí)例化后的代碼編譯出來(lái),再去參與鏈接,才能保證結(jié)果正確。
所以,要保證編譯期能知道所有需要的實(shí)例,我們只能把模板實(shí)現(xiàn)放在頭文件里。這樣,每一個(gè)編譯單元都能根據(jù)自己需要的實(shí)例來(lái)生成代碼。也就是說(shuō),上面的代碼應(yīng)該改造成:
tmp.h
#pragma once
templatevoid f(const T &t);
templatevoid f(const T &t) {} // 當(dāng)然,文件內(nèi)部沒(méi)有聲明依賴關(guān)系的時(shí)候,聲明和實(shí)現(xiàn)可以合并
main.cpp
#include "tmp.h"
int main() {f(1);
f(1.0);
return 0;
}
這時(shí),在編譯main.cpp時(shí),就會(huì)把f
和f
的實(shí)例都編譯出來(lái),這樣就不會(huì)鏈接報(bào)錯(cuò)了。
但這樣會(huì)引入另一個(gè)問(wèn)題,如果多個(gè).cpp引入同一個(gè)含有模板的.h文件,并做了相同的實(shí)例化,會(huì)不會(huì)生成多份函數(shù)實(shí)現(xiàn)呢?這樣鏈接的時(shí)候不是也會(huì)報(bào)錯(cuò)嗎?
設(shè)計(jì)編譯器的大佬們自然也想到這個(gè)問(wèn)題了,那么解決方法就是,通過(guò)模板實(shí)例出的內(nèi)容,會(huì)打上一個(gè)全局標(biāo)記,最終鏈接時(shí)只使用一份(畢竟是從同一份模板生成出來(lái)的,每一份自然是相同的)。再換句更通俗易懂的說(shuō)法就是模板實(shí)例一定是inline
的,編譯器會(huì)給每個(gè)模板實(shí)現(xiàn)自動(dòng)打上inline
標(biāo)記,確保鏈接時(shí)全局唯一。
現(xiàn)在我們?cè)倩仡^看一下全特化模板,全特化模板已經(jīng)是實(shí)例化過(guò)的了,因此并不會(huì)出現(xiàn)編譯期不知道要怎么實(shí)例化的問(wèn)題。如果這時(shí)我們還把實(shí)現(xiàn)放在頭文件中會(huì)怎么樣?
tmp.h
#pragma once
templatevoid f(T t) {} // 通用模板
template<>void f(int t) {} // 針對(duì)int的全特化
t1.cpp
#include "tmp.h"
void Demo1() {f(1); // f}
t2.cpp
#include "tmp.h"
void Demo2() {f(1); // f}
我們?cè)賮?lái)當(dāng)一次編譯期。首先編譯t1.cpp,預(yù)處理展開(kāi),得到了f
的實(shí)現(xiàn),所以把f
編譯過(guò)來(lái),輸出t1.o。同理,編譯t2.cpp后,也會(huì)有一份f
的實(shí)現(xiàn)在t2.o中。最后鏈接的時(shí)候,發(fā)現(xiàn)f
重定義了!
因此我們發(fā)現(xiàn),全特化的模板其實(shí)已經(jīng)不是模板了,在這里f
會(huì)按照普通函數(shù)一樣來(lái)進(jìn)行編譯和鏈接。所以直接把實(shí)現(xiàn)放在頭文件中,就有可能在鏈接時(shí)重定義。解決方法有兩種,第一種就是我們手動(dòng)補(bǔ)上inline
關(guān)鍵字,提示編譯期要打標(biāo)全局唯一。
tmp.h
#pragma once
templatevoid f(T t) {} // 通用模板,編譯器用通用模板生成的實(shí)例會(huì)自動(dòng)打上inline
template<>inline void f(int t) {} // 針對(duì)int的全特化,必須手動(dòng)用inline修飾后才能在編譯期打標(biāo)保證鏈接全局唯一
第二種方法就是,當(dāng)做普通函數(shù)處理,我們把實(shí)現(xiàn)單獨(dú)抽到一個(gè)編譯單元中獨(dú)立編譯,最后在鏈接時(shí)才能保證唯一:
tmp.h
#pragma once
templatevoid f(T t) {} // 通用模板
template<>void f(int t); // 針對(duì)int的全特化聲明(函數(shù)聲明)
tmp.cpp
#include "tmp.h"
template<>void f(int t) {} // 函數(shù)實(shí)現(xiàn)
之后,f
會(huì)隨著tmp.cpp的編譯,單獨(dú)存在在tmp.o中,最后鏈接時(shí)就是唯一的了。
另外,對(duì)于特化的模板函數(shù)來(lái)說(shuō),參數(shù)必須是按照通用模板的定義來(lái)寫(xiě)的(包括個(gè)數(shù)、類型和順序),但對(duì)于模板類來(lái)說(shuō),則沒(méi)有任何要求,我們可以寫(xiě)一個(gè)跟通用模板壓根沒(méi)什么關(guān)系的一種特化,比如說(shuō):
templatestruct Test {// 通用模板中有2個(gè)成員變量,1個(gè)成員函數(shù)
T a, b;
void f();
};
template<>struct Test{// 特化的內(nèi)部定義可以跟通用模板完全不同
double m;
static int ff();
}
偏特化偏特化又叫部分特化,既然是「部分」的,那么就不會(huì)像全特化那樣直接實(shí)例化了。偏特化的模板本質(zhì)上還是模板,它仍然需要編譯期來(lái)根據(jù)需要進(jìn)行實(shí)例化的,所以,在鏈接方式上來(lái)說(shuō),全特化要按普通函數(shù)/類/變量來(lái)處理,而偏特化模板要按模板來(lái)處理。
先明確一個(gè)點(diǎn):模板函數(shù)不支持偏特化,因此偏特化討論的主要是模板類。
我們先來(lái)看一個(gè)最簡(jiǎn)單的偏特化的例子:
templatestruct Test {};
templatestruct Test{};
上面代碼就是針對(duì)Test
模板類,第一個(gè)參數(shù)為int
時(shí)的「偏特化」,那么只要是第一個(gè)參數(shù)為int
的時(shí)候,就會(huì)按照偏特化模板來(lái)進(jìn)行實(shí)例化,否則會(huì)按照通用模板進(jìn)行實(shí)例化。為了方便說(shuō)明,我們?cè)谕ㄓ媚0搴推鼗0逯屑右恍┯糜隍?yàn)證性的代碼:
#includetemplatestruct Test {};
templatestruct Test{static void f();
};
templatevoid Test::f() {std::cout<< "part specialization"<< std::endl;
}
void Demo() {Test::f(); // 按照偏特化實(shí)例化,有f函數(shù)
Test::f(); // 按照偏特化實(shí)例化,有f函數(shù)
Test::f(); // 按照通用模板實(shí)例化,不存在f函數(shù),編譯報(bào)錯(cuò)
}
偏特化模板本身仍然是模板,仍然需要經(jīng)歷實(shí)例化。但偏特化模板可以指定當(dāng)一些參數(shù)滿足條件時(shí),應(yīng)當(dāng)按照指定方式進(jìn)行實(shí)例化而不是通用模板定義的方式來(lái)實(shí)例化。
那如果偏特化和全特化同時(shí)存在呢?比如下面的情況:
templatestruct Test {}; // 【0】通用模板
templatestruct Test{}; // 【1】偏特化模板
template<>struct Test{}; // 【2】全特化模板
void Demo() {Testt; // 按照哪個(gè)實(shí)例化?
}
先說(shuō)答案,上面的實(shí)例會(huì)按照【2】的方式,也就是直接調(diào)用全特化。大致上來(lái)說(shuō),全特化優(yōu)先級(jí)高于偏特化,偏特化高于通用模板。
對(duì)于函數(shù)來(lái)說(shuō),模板函數(shù)不支持偏特化,但支持重載,并且重載的優(yōu)先級(jí)高于全特化。比如說(shuō):
void f(int a, int b) {} // 重載函數(shù)
templatevoid f(T1 a, T2 b) {} // 通用模板
template<>void f(int a, int b) {} // 全特化
void Demo() {f(1, 2); // 會(huì)調(diào)用重載函數(shù)
f<>(1, 2); // 會(huì)調(diào)用全特化函數(shù)ff(2.5, 2.6); // 會(huì)用通用模板生成f}
回到模板類的偏特化上,除了上面那種制定某些參數(shù)的偏特化以外,還有一種相對(duì)復(fù)雜的偏特化,請(qǐng)看示例:
templatestruct Tool {}; // 這是另一個(gè)普通的模板類
templatestruct Test {}; // 【0】通用模板
templatestruct Test>{}; // 【1】偏特化
void Demo() {Testt1; // 使用【0】實(shí)例化TestTest>; // 使用【1】實(shí)例化Test>Test>; // 使用【1】實(shí)例化Test>}
有的資料會(huì)管上面這種特化叫做「模式特化」,用于區(qū)分普通的「部分特化」。但它們其實(shí)都屬于偏特化的一種,因?yàn)槠鼗际窍喈?dāng)于特化了參數(shù)的范圍。在上面的例子中,我們是針對(duì)于「參數(shù)是Tool
的實(shí)例類型」這種情況進(jìn)行了特化。
所以,偏特化并不一定意味著模板參數(shù)數(shù)量變小,它有可能不變,甚至有可能是增加的,比如說(shuō):
templatestruct Tool {}; // 這是另一個(gè)普通的模板類
templatestruct Test {}; // 【0】通用模板
templatestruct Test>{}; // 【1】偏特化模板
templatestruct Test>{}; // 【2】偏特化模板
void Demo() {Testt1; // 【0】
Test>t2; // 【2】
Test>t3; // 【1】
}
所以偏特化的引入,讓模板編程這件事有了爆炸性的顛覆,因?yàn)槠渲械慕M合可以隨意發(fā)揮想象。但這里就引入了另一個(gè)問(wèn)題,就比如上例中,【1】和【2】都是偏特化的一種,但為什么Test
選擇了【2】而不是【1】呢?這么說(shuō),看來(lái)不僅僅是跟全特化和通用模板存在優(yōu)先級(jí)問(wèn)題,多種偏特化之間也仍然存在優(yōu)先級(jí)問(wèn)題,那么編譯器究竟是按照什么方式來(lái)進(jìn)行偏特化匹配的呢?這就是我們下一篇要著重研究的問(wèn)題了。
這一篇我們主要介紹了模板的鏈接方式和模板的特化,重點(diǎn)希望讀者理解和掌握的是模板類的偏特化,因?yàn)镃++的模板元編程其實(shí)就是一系列模板的偏特化來(lái)實(shí)現(xiàn)各種靜態(tài)功能的。
下一篇會(huì)介紹偏特化的優(yōu)先級(jí)匹配法則和SFINAE特性。
C++模板元編程詳細(xì)教程(之四)
你是否還在尋找穩(wěn)定的海外服務(wù)器提供商?創(chuàng)新互聯(lián)www.cdcxhl.cn海外機(jī)房具備T級(jí)流量清洗系統(tǒng)配攻擊溯源,準(zhǔn)確流量調(diào)度確保服務(wù)器高可用性,企業(yè)級(jí)服務(wù)器適合批量采購(gòu),新人活動(dòng)首月15元起,快前往官網(wǎng)查看詳情吧
分享名稱:C++模板元編程詳細(xì)教程(之三)-創(chuàng)新互聯(lián)
文章起源:http://www.rwnh.cn/article46/dciieg.html
成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供品牌網(wǎng)站制作、微信小程序、Google、手機(jī)網(wǎng)站建設(shè)、網(wǎng)頁(yè)設(shè)計(jì)公司、動(dòng)態(tài)網(wǎng)站
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來(lái)源: 創(chuàng)新互聯(lián)
猜你還喜歡下面的內(nèi)容