1. 程式人生 > >[C++ Template]基礎--類模板

[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 成員函式的實現

為了定義類模板的成員函式, 你必須指定該成員函式是一個函式模板, 而且你還需要使用這個類模板的完整型別限定符

。 因此, 型別Stack<T>的成員函式push()的實現如下:

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 小結

•類模板是具有如下性質的類: 在類的實現中, 可以有一個或多個型別還沒有被指定。
•為了使用類模板, 你可以傳入某個具體型別作為模板實參; 然後編譯器將會基於該型別來例項化類模板。
•對於類模板而言, 只有那些被呼叫的成員函式才會被例項化。
•你可以用某種特定型別特化類模板。
•你可以用某種特定型別區域性特化類模板。
•你可以為類模板的引數定義預設值, 這些值還可以引用之前的模板引數。