[C++ Template]基礎--類模板
3 類模板
與函式相似, 類也可以被一種或多種型別引數化。
3.1 類模板Stack的實現
template <typename T> class Stack { private: std::vector<T> elems; // 儲存元素的容器 public: void push(T const&); // 壓入元素 void pop(); // 彈出元素 T top() const; // 返回棧頂元素 bool empty() const { // 返回棧是否為空 return elems.empty(); } }; template <typename T> void Stack<T>::push(T const& elem) { elems.push_back(elem); // 把elem的拷貝附加到末尾 } template<typename T> void Stack<T>::pop() { if (elems.empty()) { throw std::out_of_range("Stack<>::pop(): empty stack"); } elems.pop_back(); //刪除最後一個元素 } template <typename T> T Stack<T>::top() const { if (elems.empty()) { throw std::out_of_range("Stack<>::top(): empty stack"); } return elems.back(); // 返回最後一個元素的拷貝 }
3.1.1 類模板宣告
類模板的宣告和函式模板的宣告很相似: 在宣告之前, 我們先(用一條語句) 宣告作為型別引數的識別符號; 我們繼續使用T作為該識別符號:
template <typename T>
class Stack {
//...
};
在此, 我們可以再次使用關鍵字class來代替typename:
template <class T>
class Stack {
//...
};
這個類的型別是Stack<T>, 其中T是模板引數。 因此, 當在宣告中需要使用該類的型別時, 你必須使用Stack<T>。例如, 如果你要宣告自己實現的拷貝建構函式和賦值運算子, 那麼應該這樣編寫:
template <typename T>
class Stack {
//...
Stack(Stack<T> const&); //拷貝建構函式
Stack<T>& operator= (Stack<T> const&); //賦值運算子
//...
};
然而, 當使用類名而不是類的型別時, 就應該只用Stack; 譬如,當你指定類的名稱、 類的建構函式、 解構函式時, 就應該使用Stack。
3.1.2 成員函式的實現
為了定義類模板的成員函式, 你必須指定該成員函式是一個函式模板, 而且你還需要使用這個類模板的完整型別限定符
template <typename T>
void Stack<T>::push(T const& elem)
{
elems.push_back(elem); //把傳入實參elem的拷貝附加到末端
}
3.2 類模板Stack的使用
為了使用類模板物件, 你必須顯式地指定模板實參。 下面的例子展示瞭如何使用類模板Stack<>:
int main()
{
try {
Stack<int> intStack; // 元素型別為int的棧
Stack<std::string> stringStack; // 元素型別為字串的棧
// 使用int棧
intStack.push(7);
std::cout << intStack.top() << std::endl;
// 使用string棧
stringStack.push("hello");
std::cout << stringStack.top() << std::endl;
stringStack.pop();
stringStack.pop();
}
catch(std::exception const& ex) {
std::cerr << "Exception: " << ex.what() << std::endl;
return EXIT_FAILURE; // 程式退出, 且帶有ERROR標記
}
}
通過宣告型別Stack<int>, 在類模板內部就可以用int例項化T。 因此, intStack是一個建立自 Stack<int>的物件, 它的元素儲存於 vector,且型別為 int。 對於所有被呼叫的成員函式, 都會例項化出基於int型別的函式程式碼。
注意, 只有那些被呼叫的成員函式, 才會產生這些函式的例項化程式碼。 對於類模板, 成員函式只有在被使用的時候才會被例項化。 顯然,這樣可以節省空間和時間; 另一個好處是: 對於那些“未能提供所有成員函式中所有操作的”型別, 你也可以使用該型別來例項化類模板, 只要對那些“未能提供某些操作的”成員函式, 模板內部不使用就可以。例如,某些類模板中的成員函式會使用operator<來排序元素; 如果不呼叫這些“使用operator<的”成員函式, 那麼對於沒有定義operator<的型別,也可以被用來例項化該類模板。另一方面, 如果類模板中含有靜態成員, 那麼用來例項化的每種型別, 都會例項化這些靜態成員。
藉助於型別定義, 你可以更方便地使用類模板:
typedef Stack<int> IntStack;
void foo(IntStack const& s) //s是一個int棧
{
IntStack istack[10]; //istack是一個含有10個int棧的陣列
//...
}
C++的型別定義只是定義了一個“類型別名”, 並沒有定義一個新型別。 因此, 在進行型別定義:
typedef Stack<int> IntStack
之後, IntSatck和Stack<int>實際上是相同的型別, 並可以用於相互賦值。
3.3 類模板的特化
你可以用模板實參來特化類模板。 和函式模板的過載類似, 通過特化類模板, 你可以優化基於某種特定型別的實現, 或者克服某種特定型別在例項化類模板時所出現的不足。 另外, 如果要特化一個類模板, 你還要特化該類模板的所有成員函式。 雖然也可以只特化某個成員函式, 但這個做法並沒有特化整個類, 也就沒有特化整個類模板。
為了特化一個類模板, 你必須在起始處宣告一個 template<>, 接下來宣告用來特化類模板的型別。 這個型別被用作模板實參, 且必須在類名的後面直接指定:
template<>
class Stack<std::string>
{
...
}
...
進行類模板的特化時, 每個成員函式都必須重新定義為普通函式,原來模板函式中的每個T也相應地被進行特化的型別取代:
void Stack<std::string>::push(std::string const& elem)
{
elems.push_back(elem); //附加傳入實參elem的拷貝
}
3.4 區域性特化
類模板可以被區域性特化。 你可以在特定的環境下指定類模板的特定實現, 並且要求某些模板引數仍然必須由使用者來定義。 例如類模板:
template <typename T1, typename T2>
class MyClass
{
//...
};
就可以有下面幾種區域性特化:
//區域性特化: 兩個模板引數具有相同的型別
template <typename T>
class MyClass < T, T >
{
...
};
//區域性特化: 第2個模板引數的型別是int
template<typename T>
class MyClass < T, int >
{
...
};
//區域性特化: 兩個模板引數都是指標型別。
template<typename T1, typename T2>
class MyClass < T1*, T2* >
{
...
};
下面的例子展示各種宣告會使用哪個模板:
Myclass<int, float> mif; //使用MyClass<T1,T2>
MyClass<float, float> mff; //使用MyClass<T,T>
MyClass<float, int> mfi; //使用MyClass<T,int>
MyClass<int*, float*> mp; //使用MyClass<T1*,T2*>
如果有多個區域性特化同等程度地匹配某個宣告, 那麼就稱該宣告具有二義性:
MyClass<int,int> m; //錯誤:同等程度地匹配MyClass<T,T>
// 和MyClass<T,int>
MyClass<int*,int*> m; //錯誤:同等程度地匹配MyClass<T,T>
// 和MyClass<T1*,T2*>
為了解決第2種二義性, 你可以另外提供一個指向相同型別指標的特化:
template<typename T>
class MyClass < T*, T* >
{
...
};
3.5 預設模板引數
對於類模板, 你還可以為模板引數定義預設值; 這些值就被稱為預設模板實參; 而且, 它們還可以引用之前的模板引數。 例如, 在類Stack<>中, 你可以把用於管理元素的容器定義為第2個模板引數, 並且使用std::vector<>作為它的預設值:
template <typename T, typename CONT = std::vector<T> >
class Stack
{
private:
CONT elems; // 包含元素的容器
public:
void push(T const&); // 壓入元素
void pop(); // 彈出元素
T top() const; // 返回棧頂元素
bool empty() const
{ // 返回棧是否為空
return elems.empty();
}
};
template <typename T, typename CONT>
void Stack<T, CONT>::push(T const& elem)
{
elems.push_back(elem); // 把傳入實參elem附加到末端
}
template <typename T, typename CONT>
void Stack<T, CONT>::pop()
{
if (elems.empty()) {
throw std::out_of_range("Stack<>::pop(): empty stack");
}
elems.pop_back(); // 刪除末端元素
}
template <typename T, typename CONT>
T Stack<T, CONT>::top() const
{
if (elems.empty()) {
throw std::out_of_range("Stack<>::top(): empty stack");
}
return elems.back(); // 返回末端元素的拷貝
}
可以看到: 我們的類模板含有兩個模板引數, 因此每個成員函式的定義都必須具有這兩個引數。如果你只傳遞第一個型別實參給這個類模板, 那麼將會利用vector來管理stack的元素;另外, 當在程式中宣告Stack物件的時候, 你還可以指定容器的型別;
// int棧:
Stack<int> intStack;
// double棧, 它使用std::deque來管理元素
Stack<double,std::deque<double> > dblStack;
3.6 小結
•類模板是具有如下性質的類: 在類的實現中, 可以有一個或多個型別還沒有被指定。
•為了使用類模板, 你可以傳入某個具體型別作為模板實參; 然後編譯器將會基於該型別來例項化類模板。
•對於類模板而言, 只有那些被呼叫的成員函式才會被例項化。
•你可以用某種特定型別特化類模板。
•你可以用某種特定型別區域性特化類模板。
•你可以為類模板的引數定義預設值, 這些值還可以引用之前的模板引數。