[C++ Template]基础--技巧性基础知识

目录

5 技巧性基础知识

5.1 关键字typename

5.2 使用this->

5.3 成员模板

5.4 模板的模板参数

5.5 零初始化 

5.6 使用字符串作为函数模板的实参

5.7 小结


 

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)。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值