C++ 模板偏特化-來自STL的思考


之前學習STL時接觸過一段時間的模板,模板是C++泛型編程編程的基礎
STL從頭到尾都是模板泛型編程,我覺得用的最巧妙的就是在traits萃取技巧時用到的模板偏特化


  • 先簡要回顧一下模板吧,模板主要分為函數模板與類模板

函數模板

template<class T>
T add(T a, T b)	{ return a + b;}

int main()
{
	int a = 1, b = 2;
	std::cout << add(a + b) << std::endl;
	return 0;
}

如上就是最簡單的函數模板,當實例化函數模板時,編譯器會自動進行實參類型推導
上面類型T就被自動推導為int類型

類模板

templete<class T>
class A
{
public:
	explicit A(T val) : t(val) { }
	T add(T x) { return t + y; }	
privete:
	T t;
};

int main()
{
	A<int> a(10);
	std::cout << a.add(5) << std::endl;
	return 0;
}

如上就是最簡單的類模板,實例化類模板必須要指定類型,編譯器無法為類模板自動推導類型

幾個需要注意的點
1.類模板的和函數模板都必須定義在.h頭文件中
2.模板的實例化類型確定是在編譯期間
3.只是模板寫好了,編譯一般不會很多出錯,出錯一般會在實例化編譯之后
4.模板實例化只會實例化用到的部分,沒有用到的部分將不會被實例化


  • 我覺得模板的特例化是模板中比較精髓的東西
    有函數模板特例化,類模板特例化,其中又分為全特化與偏特化
    主要的用途都是對於特定的類型,指定特定的處理方式
    就相當於普通編程中if-else if - else這樣的方式
    編譯階段確定如果是某個特化類型,就用特化的模板
    如果都不是,就用最一般的模板

函數模板特例化

函數模板只能全特化,不能偏特化,如果要偏特化的話只能重載

函數模板全特化

template< >                                  // 全特化 注意語法
double add(double a, double b)  { return a + b; }

int main()
{
	int x = 10, y = 20;
	double z = 1.1, w = 2.2;
	std::cout << add(x, y) << std::endl;   // 調用普通版本
	std::cout << add(z, w) << std::endl;   // 調用全特化版本
	return 0;
}

如果有與實參更加匹配的特例化版本,編譯器將會選擇特例化版本

函數模板重載(不存在偏特化)

因為偏特化版本本質上仍然是模板,所以如果需要的話,可以重載一個函數模板

template<class T1>  // 重載版本,接收參數為指針
T1 add(T1* a, T1* b) { return *a + *b; }   
int main()
{
	int a = 10, b = 20;
	int *x = &a, *y = &b;
	add(a, b);    // 調用普通模板
	add(x, y);   // 調用重載的模板
	return 0;
}

如上,如果需要一個接收指針的偏特化版本,那么可以用重載模板實現
函數模板不存在偏特化

類模板特例化

類模板既有全特化,又有偏特化
這里重新寫一個更一般的模板類來說明類模板的特例化

類模板全特化

類模板全特化比較好理解,跟函數模板一樣,全特化是一個實例,當編譯器匹配時會優先匹配參數一致的實例

template< >        // 注意語法
class A<char*>       // 一個全特化的模板類A
{                    // 當用char*類型來實例化類模板A時,將會優先調用這個全特化實例
public:
	explicit A(char* val) : t(val) { }
	char* add(char* a, char* b) { return strcat(a, b); }
private:
	char* t;
};

類模板的偏特化

類模板的偏特化會稍微復雜一點點,它有多種形式
類模板偏特化本質上都是指定部分類型,讓偏特化版本稱為普通版本的子集,若實例化時參數類型為指定的類型,則優先調用特例化版本
第一種形式

template<class T1, class T2>      // 普通版本,有兩個模板參數
class B { ..... };

template<class T2>            // 偏特化版本,指定其中一個參數,即指定了部分類型
class B<int , T2> { ..... };  // 當實例化時的第一個參數為int 則會優先調用這個版本

第二種形式,也是最重要的版本

template<class T>     // 普通版本
class B { ..... };

template<class T>   //這個偏特化版本只接收指針類型的模板實參 
class B<T*> { ..... }; 

template<class T>
class B<T&> { ..... };     // 這個偏特化版本只接受引用類型的模板實參

第三種形式

template<class T>    //普通版本
class B { ..... };

template<class T>   // 這種只接受用T實例化的vector的模板實參.也是一種偏特化
class B<vector<T>> { ......  };  

幾個值得注意的地方
1.特例化本質上是我們頂替了編譯器的工作,我們幫編譯器做了類型推導
2.全特化本質上是一個實例,而偏特化本質上還是一個模板,只是原來模板的一個子集
3.所以全特化的函數模板,本質上是實例,從而不會與函數模板產生二義性
4.若想讓用戶能使用特例化版本,特例化版本必須與模板定義在同一個.h頭文件中


  • STL中的迭代器實現與高效實現與模板偏特化息息相關.

類模板偏特化與STL

偏特化在STL中最重要的兩個應用

1.應用在迭代器設計中,為了使迭代器既可以萃取出值類型,又可以包容原生指針
如果要通過一個迭代器就能知道它的值類型,那么一般會使用iterator_traits
迭代器萃取技術的兩個核心是:
1)在每個迭代器類中定義value_type值類型的類型成員,這樣直接通過迭代器的value_type類型成員就可以知道值類型
2)問題就在於,迭代器必須兼容原生指針,而原生指針很難被重新定義,即要在原生指針的類中添加value_type的值類型的類型成員.這時候,靠的就是類模板的偏特化了.新添加一層iterator_traits類,專門萃取迭代器的屬性,然后再對iterator_traits類設計原生指針與原生引用的偏特化版本,就解決了這個棘手的問題

2.type_traits類型萃取,對待特殊類型,特殊處理,提高效率
對於沒有構造函數,析構函數等的內置類型,如果與復雜類型一樣,執行同樣的操作,顯然是效率不高的
先實現一個對所有類型都設置一個最保守值的type_traits模板類,然后再對每個內置類型設置偏特化版本,內置類型設置一個更為激進的值,表明可以采取更為高效的操作來提高效率
比如copy函數,如果傳遞的對象是一個復雜類型,那么可能只能采取最保守的處理方式,一個一個的構造;如果是內置類型,這樣顯然太低效,使用memcpy()可能會好一些

其實iterator_traits也不止是處理兼容原生指針的問題,它也可以提高效率.
迭代器分為很多種,有可以隨機訪問的(vector),有只能前后一個一個移動的(list),也有只能單向移動的(slist),所以一般把迭代器分為五種:
InputIterator       輸入迭代器
OutputIterator        輸出迭代器
ForwardIterator      單向迭代器
BidirectionIterator     雙向迭代器
RandomAccessIterator   隨機訪問迭代器
比如一個advance(n)函數,對於單向迭代器只能一個一個移動過去,但是這種實現對於隨機訪問迭代器顯然不是理想的處理方式
處理的方式就是先實現這五個類,用作標記用,在每個迭代器里面都定義迭代器類型的類型成員iterator_catagory,再對不同版本的迭代器實現不同的advance(n)處理方式


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM