C++ day29 代码重用(五)类模板(篇三:成员模板, 把模板用作参数)

模板果然内容丰富功能强大用法灵活,用法真tm多,很期待阅读STL源码的那一天,打好基础,希望不要太陌生。写本文的过程中,感觉离STL越来越近了,本文模板的特性都是主要用于实现STL的。

成员模板(把模板用作类或结构的成员,对于STL的完全实现特别重要)

之前只接触过成员数据,成员函数,即把数据和函数做为类(包括模板类)或者结构的成员。现在STL说,还可以把模板也作为类(包括模板类)或者结构的成员,并且这种特性对于STL的完全实现极为关键。

示例

这个示例还是挺晕乎的,用了三个泛型名,在一个模板类中把另一个模板类作为成员(感觉很神奇吧,毕竟模板类并不是实际的可以生成对象的类,只是通用描述性代码,居然也可以做成员。。),再用这个成员模板类的对象作为自己的私有成员(这是包含)。

尤其是成员模板函数blab中用了一个泛型名U,让我好晕乎,实际上不难,这个U是独立于T和Type的,和他俩没关系,不过是函数模板自己需要用一个泛型名罢了。

//tempmember.h
#ifndef TEMPMEMBER_H_
#define TEMPMEMBER_H_
#include <iostream>
template <typename T>
class beta
{
private:
	//Hold模板类在beta模板类的私有部分声明,因此只能在beta类访问Hold类
	template<typename Type>
	class Hold
	{
	private:
		Type val;
	public:
		Hold(Type v = 0):val(v){}
		Type Value() const {return val;}
		void show() const {std::cout << "value: " << val << '\n';}
	};
	Hold<T> q;//模板类Hold<T>的对象,q是基于T(beta模板的类型参数)的Hold对象
	//如果T是double,则q的类型是Hold<double>
	Hold<int> n;//模板类Hold<int>的对象,n是基于int的Hold对象,n的类型是Hold<int>
public:
	beta(T t, int i):q(t), n(i){}//q(t)和int(i)用t,i的类型隐式实例化,再调用构造函数生成对象q,n
	void show() const {q.show();n.show();}
	template <typename U>
	U blab(U u, T t){return (n.Value() + q.Value()) * u / t;}//U的类型由该方法被调用时的参数值显式确定
	//T类型由对象的实例化类型确定
};
#endif
#include <iostream>
#include "tempmember.h"

int main()
{
	using std::cout;

	beta<double> guy(3.5, 2);//	T实现为double
	guy.show();

	cout << "U is set to int: " << guy.blab(10, 2.5) << std::endl;
	cout << "U is set to double: " << guy.blab(2.5, 5) << std::endl;
	cout << "Done!\n";
    return 0;
}

注意程序中U设为int时, blab方法会先按照double计算,因为计算元素有double(算术表达式中混合类型的自动转换),计算完成由于返回类型是int,才截断为int。注意这个顺序,不是一开始就截断为int再计算。
int((3.5 + 2) * 10 / 2.5)= 22, double((3.5 + 2) * 2.5 / 5) = 2.75

value: 3.5
value: 2
U is set to int: 22
U is set to double: 2.75
Done!

另一种写法

上面的代码在beta模板类的定义中嵌套定义了(即不只是声明,还给了定义)模板类Hold,也在beta类定义中给出了成员模板函数blab的定义。但是我们还有另一种写法:在beta类中只给出模板成员们(包括模板类和模板函数)的声明,即原型;然后在beta类外部(但还是在同一个文件中)给出他们的定义代码:

这样写,程序的组织性会更强,看起来结构也更清晰。但是要注意定义成员模板类和成员函数时要加类名限定符以指出Hold类和blab函数是beta类的成员!!!否则报错。且模板头也是嵌套的。

//tempmember.h
#ifndef TEMPMEMBER_H_
#define TEMPMEMBER_H_
#include <iostream>
template <typename T>
class beta
{
private:
    //beta类声明中只声明模板类成员和模板函数成员,而在类外部提供定义
	template<typename Type>
	class Hold;
	Hold<T> q;//模板类Hold<T>的对象
	Hold<int> n;//模板类Hold<int>的对象
public:
	beta(T t, int i):q(t), n(i){}//q(t)和int(i)用t,i的类型隐式实例化,再调用构造函数生成对象q,n
	void show() const {q.show();n.show();}
	template <typename U>
	U blab(U u, T t);
};

//嵌套模板头,定义成员模板类
template <typename T>
    template <typename Type>
        class beta<T>::Hold//必须用前缀beta<T>::
        {
        private:
            Type val;
        public:
            Hold(Type v = 0):val(v){}
            Type Value() const {return val;}
            void show() const {std::cout << "value: " << val << '\n';}
        };

//嵌套模板头,定义成员模板函数
template <typename T>
    template <typename U>
        U beta<T>::blab(U u, T t)//必须用前缀beta<T>::
        {
            return (n.Value() + q.Value()) * u / t;
        }
#endif

把模板用作参数(新增特性,专用于实现STL)

模板有类型参数和非类型参数。

我个人觉得模板参数既属于类型参数又属于非类型参数:前者理由是,模板类就是一种类型。后者理由是,模板类参数不需要使用关键字typename或者class。

所以就不要尝试将它归类,可以把它看作是第三类参数

示例 使用Stack类

//tempparm.h -- templates as parameters把模板类当做模板类的非类型参数
#include <iostream>
#include "stacktp.h"

template <template <typename T> class Thing>
class Crab
{
private:
	Thing<int> s1;//T替换为int
	Thing<double> s2;//T替换为double
public:
	Crab(){}
	bool push(int a, double x){return (s1.push(a) && s2.push(x));}
	bool pop(int & a, double & x){return (s1.pop(a) && s2.pop(x));}
};
//stacktp.h  -- a stack template
#ifndef STACKTP_H_
#define STACKTP_H_
#include <iostream>
using std::cout;

template <typename Type>
class Stack//这里不需要写为Stack<Type>,只是写模板函数代码时需要,在类外部也是必须使用Stack<Type>
{
private:
	int top;
	int stacksize;
	enum {defaultSize = 10};
	Type * items;//在构造函数中分配空间;指针成员,所以必须自己写深复制的复制构造函数等
public:
	Stack(int ss = defaultSize);
	Stack(const Stack & st);//复制构造函数(自写深复制版)
	~Stack(){delete [] items;}
	bool isfull() const{return    top == stacksize;}
	bool isempty() const{return top == 0;}
	bool push(const Type & item);
	bool pop(Type & item);
	Stack & operator=(const Stack & st);//赋值运算符成员函数(自写深复制版)
};

template <typename Type>
Stack<Type>::Stack(int ss):top(0),stacksize(ss)
{
    items = new Type[stacksize];
}

template <typename Type>
Stack<Type>::Stack(const Stack & st)
{
	stacksize = st.stacksize;
	top = st.top;
	items = new Type[stacksize];
	int i;
	for (i = 0; i < stacksize; ++i)
	{
		items[i] = st.items[i];
	}
}

template <typename Type>
bool Stack<Type>::push(const Type & item)
{
	if (isfull())
		return false;
	items[top++] = item;
	return true;
}

template <typename Type>
bool Stack<Type>::pop(Type & item)
{
	if (isempty())
		return false;
	item = items[--top];
	return true;
}

//运算符成员函数的核心代码和复制构造函数一模一样
//只是要先判断是否赋值两端是同一个对象,以及有返回值
template <typename Type>
Stack<Type> & Stack<Type>::operator=(const Stack<Type> & st)
{
	if (this == &st)
		return *this;
	stacksize = st.stacksize;
	top = st.top;
	items = new Type[stacksize];
	int i;
	for (i = 0; i < stacksize; ++i)
		items[i] = st.items[i];
	return *this;
}
#endif
#include <iostream>
#include "tempparm.h"
#include "stacktp.h"

int main()
{
	using std::cout;
	using std::cin;
	
	//Stack模板必须匹配template <typename T> class Thing
	Crab<Stack> nebula;//把Stack模板类作为参数传入,nebula对象有两个私有成员,一个是int栈,一个是double栈
	int i;
	double d;
	cout << "Enter int double pairs, such as 4 3.5 (0 0 to end):\n";
	while (cin >> i >> d && i > 0 && d > 0)
	{
		if (!nebula.push(i, d))
			break;
			
	}
	while (nebula.pop(i, d))
		cout << i << ", " << d << std::endl;
	
	cout << "Done!\n";
    return 0;
}

输出

Enter int double pairs, such as 4 3.5 (0 0 to end):
5 2.5
6 3.2
8 2.4
6 2.8
8 3.5
0 0
8, 3.5
6, 2.8
8, 2.4
6, 3.2
5, 2.5
Done!

但是这句代码while (cin >> i >> d && i > 0 && d > 0)不允许用户输入错误,比如用户把int输入为了double,程序就会出错:

因为cin根据插入运算符后面的变量的数据类型决定读取到哪里,由于i是int类型,所以一遇到小数点就停止读取并保存int,然后再读取double,由于小数点前面没有数子则默认为0 所以double读取也能成功,程序运行不会出错,但是运行结果却不是我们想要的。

Enter int double pairs, such as 4 3.5 (0 0 to end):
2.1 5.2
0 0
5, 0.2
2, 0.1
Done!

可以改程序以检查每一个输入的正确性,这里就不赘述了。

混合使用模板参数和类型参数

上面的模板类示例中,使用了int和double两个具体类型,这限制了模板的通用性,我们可以通过混合使用模板参数和常规的类型参数,把这两个类型也写成泛型:

//tempparm.h -- templates as parameters把模板类当做模板类的非类型参数
#include <iostream>
#include "stacktp.h"

template <template <typename T> class Thing, typename T1, typename T2>
class Crab
{
private:
	Thing<T1> s1;//T替换为int
	Thing<T2> s2;//T替换为double
public:
	Crab(){}
	bool push(T1 a, T2 x){return (s1.push(a) && s2.push(x));}
	bool pop(T1 & a, T2 & x){return (s1.pop(a) && s2.pop(x));}
};

主程序修改:

Crab<Stack, int, double> nebula;

类模板的三种友元函数

先说点小事:我突然发现,我之前一直随意混用类模板和模板类这两个词语,这是不对的,应该用类模板,不要再用模板类了,虽然书上也这么写过,但我觉得还是类模板更严谨。因为这样才强调了模板的身份,并不是一个类,只有实例化才会生成类。同样的,不要再讲模板函数,而要说函数模板,把模板这个身份词放在后面,强调清楚身份。

可以在类模板的代码中给类声明和定义友元函数。类模板有三种友元:

一,非模板友元函数

即把一个常规函数声明为友元,由于这种友元函数不是函数模板,所以没有泛型,是完全具体的,因此会成为类模板的所有实例化的友元函数。

和之前的友元函数一样,由于不是成员函数,所以没有默认的隐藏的对象参数,也不可以被对象调用。但是它可以:

  1. 访问类的全局对象(全局是为了保证友元函数在类作用域中),或者用全局指针访问非全局对象
  2. 也可以创建类的对象
  3. 还可以访问对象的模板类的静态数据成员

情况1:常规函数

template <typename T>
class HasFriend
{
public:
	friend void counts();//会成为所有实例化类的友元函数
};

情况2:把类模板作为参数的常规函数,需要提供显式具体化定义

这是第二种情况,友元函数本身也不是模板,但是有参数是类模板。

如果把类模板作为友元函数的参数,必须要指明具体化的类型,注意!!下面例子的report函数本身并不是一个函数模板哦,也只是普通函数,只是把类模板作为了一个参数

template <typename T>
class HasFriend
{
public:
	//friend void report(HasFriend & hf);//错误,因为HasFriend根本不是一个类
	//只是个模板,没有HasFriend的对象
	friend void report(HasFriend<T> & hf);//正确,例如当类模板被具体化为HasFriend<int>类时
	//则hf是HasFriend<int>类的对象
};

这时候,参数为HasFriend<int>的引用的report函数成为了HasFriend<int>类的友元函数

由于report函数只是常规函数,却又用了类模板作为参数,所以实例化的时候编译器也要为他生成代码,所以我们必须要为这个友元函数提供显式具体化的定义代码:

//可能的类型都要写
void report(HasFriend<short> & hf){···}//专门用于short类型的显式具体化定义
void report(HasFriend<double> & hf){···}
void report(HasFriend<int> & hf){···}
···

示例

这个示例包含了第一类友元函数的两种情况,即完全常规的函数和代类模板参数的常规函数

//frnd2tmp.h -- 有非函数模板友元的类模板(class template with non-template friends)
#include <iostream>
using std::cout;
using std::endl;

template <typename T>
class HasFriend
{
private:
	T item;
	static int ct;//类模板的静态数据成员,每个具体类都有一个自己的静态成员
public:
	HasFriend(const T & i):item(i){++ct;}//ct是具体类的对象个数;由于T是泛型,所以item不好设置默认参数
	~HasFriend(){--ct;}
	friend void counts();//第一类友元函数,非模板友元函数
	friend void report(HasFriend<T> & hf);//第二类友元函数,约束模板友元函数
};

//静态成员的初始值赋值
template <typename T>//每次单独使用泛型名都必须加模板头
int HasFriend<T>::ct = 0;//这里还要用int!!!

void counts()
{
	cout << "int count: " << HasFriend<int>::ct << endl;
	cout << "double count: " << HasFriend<double>::ct << endl;
}

void report(HasFriend<int> & hf)
{
	cout << "HasFriend<int>::item = " << hf.item << endl;//访问全局对象
}

void report(HasFriend<double> & hf)
{
	cout << "HasFriend<double>::item = " << hf.item << endl;//访问全局对象
}
#include <iostream>
#include "frnd2tmp.h"

int main()
{
	using std::cout;

	counts();//友元函数无需对象调用
	HasFriend<int> hfint1(10);
	counts();
	HasFriend<int> hfint2(20);
	counts();

	HasFriend<double> hfd1(2.5);
	counts();
	HasFriend<double> hfd2(5.5);
	counts();

	report(hfint1);
	report(hfint2);
	report(hfd1);
	report(hfd2);



	cout << "Done!\n";
    return 0;
}
int count: 0
double count: 0
int count: 1
double count: 0
int count: 2
double count: 0
int count: 2
double count: 1
int count: 2
double count: 2
HasFriend<int>::item = 10
HasFriend<int>::item = 20
HasFriend<double>::item = 2.5
HasFriend<double>::item = 5.5
Done!

二,约束模板友元函数(bound,实际是在类外面声明的模板的具体化)

友元函数是函数模板。而不是具体化的函数。第一类友元函数都是具体化的函数,即使第一类的第二种情况出现了泛型,但也是提供了所有需要的显式实例化定义的。

接下来这两类友元函数都是函数模板。要复杂一些,一共有三点需要注意:

  1. 必须在类定义前面声明模板友元函数,注意要用模板头
  2. 在类模板中再次声明模板友元函数(总共要声明两次),注意这里要用尖括号表示具体化
  3. 为模板友元函数提供定义,不需要尖括号,但需要模板头

示例

//tmp2tmp.h -- 约束模板友元函数
#ifndef TMP2TMP_H_
#define TMP2TMP_H_
#include <iostream>
using std::cout;

//必须在类定义前面声明模板友元函数
template <typename T> void counts();
template <typename T> void report(T &);

//类模板的声明
template <typename TT>
class HasFriendT
{
private:
	TT item;
	static int ct;
public:
	HasFriendT(const TT & i):item(i){++ct;}//必须有const,因为传入的可能是常量,比如10,就是右值,只能赋给const变量
	~HasFriendT(){--ct;}
	//在类模板中再次声明模板友元函数
	friend void counts<TT>();//<TT>表示是模板具体化,counts方法没有参数,必须用<TT>指明其具体化,TT是HasFriendT类的参数的类型
	friend void report<>(HasFriendT<TT> &);//<>表示模板具体化,为空是因为函数代码中的TT可以由参数推断出
	//但你也可以写<TT>
};

//初始化静态数据成员
template <typename T>
int HasFriendT<T>::ct = 0;

//模板友元函数的定义
template <typename T>
void counts()//这里并不需要写<T>
{
	cout << "template size: " << sizeof(HasFriendT<T>) << ";";
	cout << "template counts: " << HasFriendT<T>::ct << '\n';//友元函数可以用类名限定符访问静态成员
}

template <typename T>
void report(T & t)//不需要写<>,不写HasFriendT<T> & t
{
	cout << t.item << std::endl;
}
#endif
#include <iostream>
#include "tmp2tmp.h"

int main()
{
	using std::cout;
	
	counts<int>();
	HasFriendT<int> hft1(10);
	HasFriendT<int> hft2(30);
	HasFriendT<double> hft3(5.5);
	
	report(hft1);
	report(hft2);
	report(hft3);
	
	counts<int>();
	counts<double>();

	cout << "Done!\n";
    return 0;
}

输出

template size: 4;template counts: 0
10
30
5.5
template size: 4;template counts: 2
template size: 8;template counts: 1
Done!

三,非约束模板友元函数(unbound,在类内部声明模板)

这样做,友元函数模板的所有具体化版本全部都是每一个具体类的友元函数。即每一个具体类都有友元模板的所有的具体化版本作为自己的友元函数。

这个比第二种简单一些。

示例

//manyfrnd.h -- 模板类的非约束模板友元函数
#ifndef MANYFRND_H_
#define MANYFRND_H_
#include <iostream>
using std::cout;

template <typename T>
class ManyFrnd
{
private:
	T item;
public:
	ManyFrnd(const T & t):item(t){}
	//在模板类声明内声明友元函数模板
	template <typename C, typename D> friend void show2(C &, D &);
};

template <typename C, typename D>
void show2(C & c, D & d)//C,D是ManyFrnd模板的具体类
{
	cout << c.item << ", " << d.item << std::endl;
}
#endif
#include <iostream>
#include "manyfrnd.h"

int main()
{
	using std::cout;

	ManyFrnd<int> mf1(10);
	ManyFrnd<int> mf2(30);
	ManyFrnd<double> mf3(20.5);
	show2(mf1, mf2);//show2<int, int>是ManyFrnd<int>的非约束友元函数
	show2(mf1, mf3);//show2<int, double>是ManyFrnd<int>,也是ManyFrnd<double>的非约束友元函数

	cout << "Done!\n";
    return 0;
}
10, 30
10, 20.5
Done!

用using = 给模板指定别名(C++11新增,非常方便)

以前起别名这事儿只有typedef会干

之前只知道可以用typedef给模板具体化指定别名

之前用typedef给类型指定别名,也可以给类模板的具体化,即具体类指定别名。因为具体类就是一种类型,是最高级的复合类型。

比如:

typedef std::array<double, 12> arrd;
typedef std::array<int, 12> arri;
typedef std::array<std::string, 12> arrst;

arrd gallons;
arri days;
arrst months;//months的类型:std::array<std::string, 12>

现在using = 来抢饭吃了!

不管怎样,typedef只可以给具体的类型起名字

现在!!!using = 来抢饭吃了!抢的还是牛逼的模板的范围。

可以给模板起新名字了!!就是不那么具体的,涉及泛型的。使用using指令:

template <typename T>
using arrtype = std:array<T, 12>;//arrtype是类模板std:array<T, 12>的别名,arrtype<T>就表示std:array<T, 12>

arrtype<double> gallons;
arrtype<int> years;
arrtype<std::string> months;

二者实际上等价

但using=的可读性更高

typedef const char * pc1;
using pc2 = const char *;

typedef const int *(*pa1)[10];
using pa2 = const int *(*)[10];
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值