本章講解高級的基礎概念,包括關鍵字typename的另外一種使用,將成員函數和嵌套類定義爲模板,模板模板參數(template template parameters),0值初始化和在類模板中使用字符串常量的一些細節等等。
1、關鍵字typename
關鍵字typename目的是向編譯說明它所有修飾的標識符是一個類型而不是其它的什麼東西。如下:
template <typename T>
class MyClass {
typename T::SubType * ptr;
…
};
上述例子中的第二個關鍵字typename的意思是,SubType是類型T的一個類型,因此ptr就是一個執行類型T::SubType的指針。
如果沒有關鍵字typename,編譯會理解爲SubType是類型T的一個靜態成員,那麼代碼”T::SubType * ptr”就會被編譯成T::SubType與ptr的乘積。
通常如果某個與模板參數(tmplate parameter)相關的名稱是類型的時候就必須加上關鍵字typename。
如下,一個典型的應用是使用STL容器的迭代器:
// print elements of an STL container
template <typename T>
void printcoll (T const& coll)
{
typename T::const_iterator pos; // iterator to iterate over coll
typename T::const_iterator end(coll.end()); // end position
for (pos=coll.begin(); pos!=end; ++pos) {
std::cout << *pos << ' ';
}
std::cout << std::endl;
在上述的例子中,模板函數的參數T是個容器,使用容器的迭代器訪問容器的元素。迭代器類型是STL容器的const_iterator,STL的聲明如下:
class stlcontainer {
…
typedef … iterator; // iterator for read/write access
typedef … const_iterator; // iterator for read access
…
};
因此,使用容器T的類型const_iterator的時候要加關鍵字typename。
.template 的使用,
如下:
template<int N>
void printBitset (std::bitset<N> const& bs)
{
std::cout << bs.template to_string<char,char_traits<char>,
allocator<char> >();
}
上述中.template看起來很奇怪,但是如果沒有.template編譯器就無法知道緊跟其後的“<”是模板參數列表的起始符,而不是小於號。
注:只有當“.”之前的構件取決於模板參數的時候,這個問題纔會發生。如上述的例子中bs是取決於模板參數N。
結論:“.template”或者“->template”只在模板中使用,並且它們必須緊跟着與模板參數相關的構件。
2、使用this->
如果類模板有基類,那麼在類中出現的成員x並不是等價於this->x,即使x是繼承而來的。
template <typename T>
class Base {
public:
void exit();
};
template <typename T>
class Derived : Base<T> {
public:
void foo() {
exit(); // calls external exit() or error
}
};
上述例子,foo()函數中調用了exit()函數,雖然基類中有exit()函數但是會被編譯器忽略。使用定義在基類模板中的符號時,未來避免不確定性,最好使用“this->” 或者 “Base::” 來修飾。
3、成員模板(Member Templates)
類成員也可是模板,既可以是嵌套類模板,也可以是成員函數模板。
還是以之前章節的類模板Stack<>舉例,通常只有類型相同的類Stack<>才能相互賦值,而不同的類型之間不能賦值,即使兩種類型之間是可以隱式轉換的。
Stack<int> intStack1, intStack2; // stacks for ints
Stack<float> floatStack; // stack for floats
…
intStack1 = intStack2; // OK: stacks have same type
floatStack = intStack1; // ERROR: stacks have different types
默認賦值操作要求左右兩邊的類型相同。上述例子中參數是float和參數是int的Stack是兩個不同的類型,因此不能使用默認賦值操作。
如果要使得不同類型的stack可以相互賦值,就要將賦值操作定義爲一個模板,如下:
template <typename T>
class Stack {
private:
std::deque<T> elems; // elements
public:
void push(T const&); // push element
void pop(); // pop element
T top() const; // return top element
bool empty() const { // return whether the stack is empty
return elems.empty();
}
// assign stack of elements of type T2
template <typename T2>
Stack<T>& operator= (Stack<T2> const&);
};
上述例子與原來的Stack有兩次不同:
1、聲明瞭一個賦值操作,此操作使Stack被不元素數類型T2的Stack賦值。
2、使用dque作爲Stack的內部容器。
賦值操作的定義:
template <typename T>
template <typename T2>
Stack<T>& Stack<T>::operator= (Stack<T2> const& op2)
{
Stack<T2> tmp(op2); // create a copy of the assigned stack
elems.clear(); // remove existing elements
while (!tmp.empty()) { // copy all elements
elems.push_front(tmp.top());
tmp.pop();
}
return *this;
}
上述例子中,在模板參數爲T的模板中定義了一個模板參數爲T2的成員模板。
template <typename T>
template <typename T2>
…
有了這個成員模板,就可以把int的Stack賦值給了float的Statck,如下:
Stack<int> intStack; // stack for ints
Stack<float> floatStack; // stack for floats
…
floatStack = intStack; // OK: stacks have different types,
// but int converts to float
當然上面的賦值並沒有改變Stack中的元素類,float的Stack中元素仍然是float。
上述例子可能會讓人認爲,這麼做會讓類型檢查失效,但實際上是不會的。
必要的類型檢查會在源Stack的元素copy到目的Stack中時進行:
elems.push_front(tmp.top());
例如,將一個string的Stack賦值到float的Stack在編譯的時候就會產生錯誤,如下 :
Stack<std::string> stringStack; // stack of ints
Stack<float> floatStack; // stack of floats
…
floatStack = stringStack; // ERROR: std::string doesn't convert to float
注:前面定義的模板賦值操作並不會取代默認的賦值操作,對於同元素類型的Stack賦值調用的仍然是默認的賦值操作。
同樣,內部容器的類型也可以參數化,如下:
template <typename T, typename CONT = std::deque<T> >
class Stack {
private:
CONT elems; // elements
public:
void push(T const&); // push element
void pop(); // pop element
T top() const; // return top element
bool empty() const { // return whether the stack is empty
return elems.empty();
}
// assign stack of elements of type T2
template <typename T2, typename CONT2>
Stack<T,CONT>& operator= (Stack<T2,CONT2> const&);
};
template <typename T, typename CONT>
template <typename T2, typename CONT2>
Stack<T,CONT>&
Stack<T,CONT>::operator= (Stack<T2,CONT2> const& op2)
{
Stack<T2> tmp(op2); // create a copy of the assigned stack
elems.clear(); // remove existing elements
while (!tmp.empty()) { // copy all elements
elems.push_front(tmp.top());
tmp.pop();
}
return *this;
}
對於類模板,只有被調用的成員纔會被實例化。如果不使用不同類型的Stack的賦值操作,那麼內部容器也可以是vector。
Stack<int,std::vector<int> > vStack;
…
vStack.push(42);
vStack.push(7);
std::cout << vStack.pop() << std::endl;
由於上面定義的vStack沒有調用到賦值操作,所以不會產生錯誤。
4、模板模板參數(Template Template Parameters)
模板參數本身也可是是一個類模板。
爲了使用其他的類型的元素類型,stack使用的時候必須兩次指定元素類型:一次是元素類型本身,另外一次是榮幸的類型。如下:
Stack<int,std::vector<int> > vStack; // integer stack that uses a vector
上述,int指定了兩次。如果,使用模板模板參數(template template parameters ),那麼上述中vector就不需要指定了,如下:
stack<int,std::vector> vStack; // integer stack that uses a vector
爲了實現上述的定義,需要將第二個目標參數聲明爲模板模板參數,如下:
template <typename T,
template <typename ELEM> class CONT = std::deque >
class Stack {
private:
CONT<T> elems; // elements
public:
void push(T const&); // push element
void pop(); // pop element
T top() const; // return top element
bool empty() const { // return whether the stack is empty
return elems.empty();
}
};
上述例子,與之前的差別是第二個模板參數被聲明爲了類模板。
template <typename ELEM> class CONT
其默認值有原來的 std::deque改爲了 std::deque,這個參數必須是類模板,並使用第一個參數實例化。
CONT<T> elems;
此例子比較特殊,使用第一個模板參數實例化了第二個模板參數,實際使用的時候可以使用類模板內的任何的類型實例化模板模板參數。
由於有模板模板參數中的模板參數“ELEM”在此例中並沒有被使用,因此此例中可以省略,如下:
template <typename T,
template <typename> class CONT = std::deque >
class Stack {
…
};
類模板中的成員函數也必須按照原則修改:必須將第二個模板參數改爲模板模板參數,如下:
template <typename T, template <typename> class CONT>
void Stack<T,CONT>::push (T const& elem)
{
elems.push_back(elem); // append copy of passed elem
}
注意:函數模板是不允許使用模板模板參數了。
模板模板參數的匹配
如果用上面模板模板參數的例子編譯的化,會產生編譯錯誤。這是因爲模板模板參數不但要求是一個模板,還要滿足其參數必須嚴格匹配它所替換的模板模板參數的參數。模板模板參數的默認值是不被考慮的,因此如果不給默認值參數編譯就會出錯。
本例子中的標準模板庫中的std::deque實際有兩個參數,第二個參數有默認值是個配置器(allocator),但是當它用來匹配CONT的參數時編譯器會忽略這個默認值,如下:
我們可以聲明CONT包含兩個模板參數,如下:
template <typename T,
template <typename ELEM,
typename ALLOC = std::allocator<ELEM> >
class CONT = std::deque>
class Stack {
private:
CONT<T> elems; // elements
…
};
由於ALLOC沒有使用,所有也可是省略,如下:
template <typename T,
template <typename ELEM,
typename = std::allocator<ELEM> >
class CONT = std::deque>
5、零值初始化(Zero Initialization)
對於基本的類型,如int、float、指針類型,沒有默認的構造函數將它們初始化爲有意義上的值。任何一個沒有初始化的局部變量,其值都是未定義的。
在模板中聲明一個變量並打算初始化,但是如果變量是內建類型(built-in type),就不能確保變量會被正確初始化,如下:
template <typename T>
void foo()
{
T x; // x has undefined value if T is built-in type
}
爲解決這個問題,可以在定義變量的時顯示調用默認構造函數,使其值爲0(bool值是false),如下:
template <typename T>
void foo()
{
T x = T(); // x is zero (or false)ifT is a built-in type
}
類模板的各個成員其類型可能被參數化。爲確保初始化這樣的程序,必須定義一個構造函數,在程序初始化列表中對每一個成員進行初始化,如下:
template <typename T>
class MyClass {
private:
T x;
public:
MyClass() : x() { // ensures that x is initialized even for built-in types
}
…
};
6、使用字符串常量(String Literals)作爲函數模板參數
以引用(by reference)的方式將字符串常量作爲函數模板參數,有時會出現意想不到的錯誤:
#include <string>
// note: reference parameters
template <typename T>
inline T const& max (T const& a, T const& b)
{
return a < b ? b : a;
}
int main()
{
std::string s;
::max("apple","peach"); // OK: same type
::max("apple","tomato"); // ERROR: different types
::max("apple",s); // ERROR: different types
}
上述出現的問題是,由於字符串常量長度不同,底層的array的類型也是不相同的。
“apple” 和 “peach”的array類型都是char const[6],而”tomato“的array類型是char const[7].
如果是以傳值(by value)的方式就可以傳遞不同的字符串常量,如下:
#include <string>
// note: nonreference parameters
template <typename T>
inline T max (T a, T b)
{
return a < b ? b : a;
}
int main()
{
std::string s;
::max("apple","peach"); // OK: same type
::max("apple","tomato"); // OK: decays to same type
::max("apple",s); // ERROR: different types
}
上述例子不同字符串傳遞可行,是因爲採用傳值得方式會將數組轉換爲指針(array-to-pointer 經常被稱爲退化)。
如下:
#include <typeinfo>
#include <iostream>
template <typename T>
void ref (T const& x)
{
std::cout << "x in ref(T const&): "
<< typeid(x).name() << '\n';
}
template <typename T>
void nonref (T x)
{
std::cout << "x in nonref(T): "
<< typeid(x).name() << '\n';
}
int main()
{
ref("hello");
nonref("hello");
}
上述例子中分別使用傳值和傳引用的方式,傳遞字符串常量,結果如下:
x in ref(T const&): char [6]
x in nonref(T): const char *
7、總結
1、當訪問一個依賴於模板參數的類型名時,必須在前面加上關鍵字typename
2、嵌套類和成員函數也可以是模板。應用之一是可以實現對不同類型但可以相互隱式轉換的模板類相互轉換,隱式轉換時會發生類型檢查。