目录
5 技巧性基础知识
本章给出模板的一些更深入的基础知识, 它们都是和模板的实际应用密切相关的, 包括关键字 typename 的另一种用法、 把成员函数和嵌套类也定义成模板、 模板的模板参数(template template parameters)、 零初始化和使用字符串作为模板实参时所要注意的一些细节。
5.1 关键字typename
在C++标准化过程中, 引入关键字typename是为了说明: 模板内部的标识符可以是一个类型。 譬如下面的例子:
template <typename T>
class MyClass
{
typename T::SubType * ptr;
...
};
上面程序中, 第2个typename被用来说明: SubType是定义于类T内部的一种类型。 因此, ptr是一个指向T::SubType类型的指针。
如果不使用typename, SubType就会被认为是一个静态成员, 那么它应该是一个具体的变量或对象, 于是, 下面表达式:
T::SubType * ptr
会被看作是类T的静态成员SubType和ptr的乘积。通常而言, 当某个依赖于模板参数的名称是一个类型时, 就应该使用typename。
.template构造
我们在引入typename之后, 发现了一个很相似的问题。 考虑下面这个使用标准bitset类型的例子:
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.template后面的小于号并不是数学中的小于号, 而是模板实参列表的起始符号; 那么只有在编辑器判断小于号之前, 存在依赖于模板参数的构造, 才会出现这种问题。 在这个例子中, 传入参数bs就是依赖于模板参数N的构造。(通常而言, C++编译器会把模板名称后面的 < 看作模板参数列表的开始; 但如果该 < 不是位于模板名称后面, 那么编译器将会把它当作小于号处理。)
总之, 只有当该前面存在依赖于模板参数的对象时, 我们才需要在模板内部使用.template标记(和类似的诸如->template的标记) , 而且这些标记也只能在模板中才能使用。
5.2 使用this->
对于具有基类的类模板, 自身使用名称x并不一定等同于this->x。即使该x是从基类继承获得的, 也是如此。 例如:
template <typename T>
class Base
{
public:
void exit();
};
class Derived:Base<T>
{
public:
void foo()
{
exit(); //调用外部的exit()或者出现错误
}
};
在这个例子中, 在foo()内部决定要调用哪一个exit()时, 并不会考虑基类Base中定义的exit()。 因此, 你如果不是获得一个错误, 就是调用了另一个exit()。
现在建议你记住一条规则:对于那些在基类中声明, 并且依赖于模板参数的符号(函数或者变量等) , 你应该在它们前面使用 this->或者Base<T>::。 如果希望完全避免不确定性, 你可以(使用诸如this->和Base<T>::等) 限定(模板中) 所
有的成员访问。
5.3 成员模板
类成员也可以是模板。 嵌套类和成员函数都可以作为模板。我们可以通过一个 Stack<>类模板来说明这种(作为模板的) 能力的优点和应用方法。 通常而言, 栈之间只有在类型完全相同时才能互相赋值, 其中类型指的是元素的类型。 就是说, 对于元素类型不同的栈, 你不能对它们进行相互赋值, 即使这两种(元素的) 类型之间存在隐式类型转换。譬如:
Stack<int> intStack1, intStack2; //int栈
Stack<float> floatStack; //float栈
...
intStack1 = intStack2; //OK:具有相同类型的栈
floatStack = intStack1; //ERROR:两边栈的类型不同
缺省赋值运算符要求两边具有相同的类型, 当元素类型不同时, 两个栈的类型显然不同, 不能符合缺省赋值运算符的要求。
然而, 通过定义一个身为模板的赋值运算符, 针对元素类型可以转换的两个栈就可以进行相互赋值。 为了达到这个目的, 你需要这样声明Stack<>:
template <typename T>
class Stack
{
private:
std::deque<T> elems; // 存储元素的容器
public:
void push(T const&); // 压入元素
void pop(); // 弹出元素
T top() const; // 返回栈顶元素
bool empty() const
{ // 返回栈是否为空
return elems.empty();
}
//使用元素类型为T2的栈进行赋值
template <typename T2>
Stack<T>& operator= (Stack<T2> const&);
};
新赋值运算符的实现大致如下:
template <typename T>
template <typename T2>
Stack<T>& Stack<T>::operator= (Stack<T2> const& op2)
{
if ((void*)this == (void*)&op2) { //赋值给自身吗
return *this;
}
Stack<T2> tmp(op2); // 产生一个赋值栈的拷贝
elems.clear(); //删除现存的元素
while (!tmp.empty())
{ // 拷贝所有的元素
elems.push_front(tmp.top());
tmp.pop();
}
return *this;
}
让我们先来看定义成员模板的语法, 在定义有模板参数T的模板内部, 还定义了一个含有模板参数T2的内部模板:
template <typename T>
template <typename T2>
...
实现了上面的成员模板之后, 现在你就可以把一个int栈赋值给一个float栈:
Stack<int> intStack; //int栈
Stack<float> floatStack; //float栈
...
floatStack = intStack; //OK:虽然是具有不同类型的栈,
// 但int可以转换为float
这个赋值函数好像屏蔽了类型检查, 看起来你可以用任意类型的栈来对目标栈进行赋值; 但实际情况并非如此, 类型检查仍然存在。当源栈(的拷贝) 的元素被移入到目标栈的时候, 就要执行必要的类型检查, 即类型检查发生在如下语句执行时:
elems.push_front(tmp.top());
例如, 如果把一个字符串栈赋值给一个浮点数栈, 那么编译器在这一行将会报告一个错误信息, 说明tmp.top()返回的字符串不能作为elems.push_front()的实参:
Stack<std::string> stringStack; //std::string栈
Stack<float> floatStack; //float栈
...
floatStack = stringStack; //ERROR:std::string并不能转换为float
可以看到, 模板赋值运算符并没有取代缺省赋值运算符。 对于相同类型栈之间的赋值, 仍然会调用缺省赋值运算符。
5.4 模板的模板参数
有时, 让模板参数本身成为模板是很有用的, 我们将继续以 stack类模板作为例子, 来说明模板的模板参数的用途。
借助于模板的模板参数, 你可以只指定容器的类型而不需要指定所含元素的类型, 就可以声明这个Stack类模板:
Stack<int, std::vector> vStack; //使用vector的int栈
为了获得这个特性,你必须把第二个参数设定为模板的模板参数,那么stack的声明应该如下:
template <typename T, template <typename ELEM> class CONT = std::deque >
class Stack {
private:
CONT<T> elems; // 保存元素的容器
public:
void push(T const&); // 压入元素
void pop(); // 弹出元素
T top() const; // 返回栈顶元素
bool empty() const { // 返回栈是否为空return elems.empty();
}
};
不同之处在于, 第2个模板参数现在被声明为一个类模板:
template <typename ELEM> class CONT
其中template <typename ELEM> class是类型,CONT是参数。这就意味着第二个参数必须是一个类模板, 并且由第一个模板参数传递进来的类型进行实例化:
CONT<T> elems;
我们前面提过: 作为模板参数的声明, 通常可以使用typename来替换关键字class。 然而, 上面的CONT是为了定义一个类, 因此只能使用关键字class。
由于在这里我们并不会用到“模板的模板参数”的模板参数(即上面的ELEM) , 所以你可以把该名称省略不写:
template <typename T,
template <typename> class CONT = std::deque >
class Stack
{
...
};
另外, 还必须对成员函数的声明进行相应的修改。 你必须把第2个模板参数指定为模板的模板参数; 这同样适用于成员函数的实现。 例如, 成员函数push()的实现如下:
template <typename T, template <typename> class CONT>
void Stack<T, CONT>::push(T const& elem)
{
elems.push_back(elem); //把elem的拷贝附加到末端
}
还有一点需要知道:函数模板并不支持模板的模板参数。
模板的模板实参匹配
如果你尝试使用新的Stack,你会获得一个错误信息:缺省值std::deque和模板的模板参数CONT不匹配。问题在于标准库中的std::deque模板还具有另一个参数: 即第2个参数(也就是所谓的内存分配器allocator) , 它有一个缺省值, 但在匹配std::deque的参数和CONT的参数时, 我们并没有考虑这个缺省值
然而, 解决办法总是有的。 我们可以重写类的声明, 让CONT的参数期待的是具有两个模板参数的容器:
template <typename T,
template <typename ELEM,typename ALLOC = std::allocator<ELEM> >class CONT = std::deque>
class Stack
{
private:
CONT<T> elems; //保存元素的容器
...
};
同样, 你可以略去ALLOC不写, 因为实现中不会用到它:
template <typename T,
template <typename ELEM,typename = std::allocator<ELEM> >class CONT = std::deque>
class Stack
{
private:
CONT<T> elems; //保存元素的容器
...
};
5.5 零初始化
现在, 假如你在编写模板, 并且希望模板类型的变量都已经用缺省值初始化完毕, 那么这时你会遇到问题, 内建类型并不能满足你的要求:
template <typename T>
void foo()
{
T x;//如果T是内建类型, 那么x本身是一个不确定值
}
于这个原因, 我们就应该显式地调用内建类型的缺省构造函数,并把缺省值设为0(或者false, 对于bool类型而言) 。 譬如调用int()我们将获得缺省值0。 于是, 借助如下代码, 我们可以确保对象已经执行了适当的缺省初始化, 即便对内建类型对象也是如此:
template <typename T>
void foo()
{
T x = T(); //如果T是内建类型, x是零或者false
}
对于类模板, 在用某种类型实例化该模板后, 为了确认它所有的成员都已经初始化完毕, 你需要定义一个缺省构造函数, 通过一个初始化列表来初始化类模板的成员:
template <typename T>
class MyClass {
private:
T x;
public:
MyClass() : x()
{//确认x已被初始化, 内建类型对象也是如此
}
...
};
5.6 使用字符串作为函数模板的实参
有时, 把字符串传递给函数模板的引用参数会导致出人意料的运行结果。 考虑下面的程序:
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: 相同类型的实参
::max("apple", "tomato"); // ERROR: 不同类型的实参
::max("apple", s); // ERROR: 不同类型的实参
}
问题在于: 由于长度的区别, 这些字符串属于不同的数组类型。 也就是说, ‘apple’和‘peach’具有相同的类型char const; 然而‘tomato’的类型则是: char const。 因此, 只有第一个调用是合法的, 因为该max()模板期望的是类型完全相同的参数。 然而, 如果声明的是非引用参数, 你就可以使用长度不同的字符串来作为max()的参数:
template <typename T>
inline T max(T a, T b)
{
return a < b ? b : a;
}
int main()
{
std::string s;
::max("apple", "peach"); // OK: 相同的类型
::max("apple", "tomato"); // OK: 退化(decay) 为相同的类型
::max("apple", s); // ERROR: 不同的类型
}
产生这种调用结果的原因是: 对于非引用类型的参数, 在实参演绎的过程中, 会出现数组到指针的类型转换(这种转型通常也被称为decay)。我们可以通过下面的程序来说明这一点:
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*
然而遗憾的是, 对于这个问题并没有通用的解决方法。 根据不同的情况, 你可以:
•使用非引用参数, 取代引用参数(然而, 这可能会导致无用的拷贝) 。
•进行重载, 编写接收引用参数和非引用参数的两个重载函数(然而, 这可能会导致二义性) 。
•对具体类型进行重载(譬如对std::string进行重载) 。
•重载数组类型, 譬如:
template <typename T, int N, int M>
T const* max(T const (&a)[N], T const (&b)[M])
{
return a < b ? b : a;
}
•强制要求应用程序程序员使用显式类型转换。
对于我们讨论的例子, 最好的方法是为字符串重载max()(见[C++ Template]基础--函数模板2.4节) 。 无论如何, 为字符串提供重载都是有必要的; 因为如果不提供重载, 当我们调用max()来比较两个字符串时, 操作a<b执行的是指针比较, 就是说a<b比较的是两个字符串的地址, 而不是它们的字典顺序。
5.7 小结
•如果要访问依赖于模板参数的类型名称, 你应该在类型名称前添加关键字typename。
•嵌套类和成员函数也可以是模板。 在本章的例子中, 针对元素类型可以进行隐式类型转换的2个栈, 我们实现了通用的赋值操作。 然而, 在这种情况下, 类型检查依然是存在的。
•赋值运算符的模板版本并没有取代缺省赋值运算符。
•类模板也可以作为模板参数, 我们称之为模板的模板参数。
•模板的模板实参必须精确地匹配。 匹配时并不会考虑“模板的模板实参”的缺省模板实参(如std::deque的allocator) 。
•通过显式调用缺省构造函数, 可以确保模板的变量和成员都已经用一个缺省值完成初始化, 这种方法对内建类型的变量和成员也适用。
•对于字符串, 在实参演绎过程中, 当且仅当参数不是引用时, 才会出现数组到指针(array-to-pointer) 的类型转换(称为decay)。