C++ 模板与泛型编程 《C++Primer》第16章(上)———— 读书笔记

C++ 模板与泛型编程 《C++Primer》第16章(上)———— 读书笔记,第1张

1、定义模板 1.1 怎么定义模板

以函数模板为例:

template <typename T, typename U> T func_name(const T& x, const U& y)
{
	// ...... //
} 

模板定义以关键字 template 开始,后跟一个模板参数列表。


模板参数列表不能为空。



使用模板时,我们(隐式或显示的)指定模板实参,把他绑定到模板参数上。


当调用函数模板时,编译器会推断模板参数,并用推断出的模板参数实例化一个特定版本的函数。


这种被编译器生成的版本被称为模板的实例


1.2 模板 类型参数

上面例子中的TU就是模板类型参数,模板类型参数前必须使用关键字 classtypename ,在模板参数列表中这两个关键字含义相同,可互换使用,一个模板参数列表中可混合使用这两个。


1.3 非类型的模板参数

非类型模板参数 的 模板实参 必须是常量表达式。


当模板的参数是类型参数时,我们传入的实参是一个类型。


不过,除了定义类型参数外,还可以定义非类型参数,此时传入的实参就是个具体的值;这个值必须是个常量表达式,也就是 其值必须能在编译时就能计算出来。


从而允许编译器在编译时实例化模板。


举个例子。


我们编写一个函数模板,比较两个字符串字面常量。


这种字面常量是const char数组。


由于不能拷贝一个数组,又由于“字面常量”告诉我们字符串长度编译时可知,所以我们把函数参数定义为数组的引用,非类型模板参数定义为 unsigned类型 ,表示字符串长度。


故函数模板定义如下:

template< unsigned N, unsigned M >
int compare(const char(&p1)[N], const char (&p2)[M])
{
	return strcmp(p1, p2);
}

compare("hi", "mom");		调用函数模板,编译器使用字面常量的大小代替N 和 M,从而实例化模板

编译器会自动在一个字符串字面常量末尾插入一个空字符作为终结符。


所以,上面的调用实际实例化出如下的版本:

int compare(const char (&p1)[3], const char (&p2)[4]);
1.4 模板程序应尽量减少对实参类型的要求

以一个compqre函数模板说明这条原则:

template< typename T >
T compare(const T& r1, const int& r2)
{
	if (r1 < r2) return 1;
	if (r2 < r1) return -1;
	return 0;
}

① 上面代码中,模板的函数参数是 const的引用 ,这就保证了模板可用于不能拷贝的类型。



② 函数体的条件判断只用了<,这样就不必担心传入的类型不支持>了。


提高了模板的泛用性。


1.5 模板编译

模板直到实例化时,才生成代码。



当编译器遇到一个模板定义时,不生成代码,只有当实例化出模板的一个特定版本时,编译器才生成代码。


模板应当定义在头文件中,且 在模板的定义中,所有不依赖于模板参数的名字都必须是可见的。


1.6 编译器对模板的错误检查

大多数编译错误是在实例化期间报告的。


因为模板直到实例化时,才生成代码,只有此时才会传入具体的类型,发现类型相关的错误。


引用《C++ Primer》

2、类模板 2.1 显式模板实参

在使用一个类模板时,我们必须提供显式模板实参,作为额外信息。


vector里的int就是显式模板实参,这种时候编译器无从推断出模板参数的类型,所以要人为提供。


当编译器从类模板实例化出一个类时,会重写类模板,将里面的模板参数全部替换为我们传入的显式模板实参。



因此,一个类模板的每个实例都形成一个独立的类。


每个类和其它类之间没有任何关联。


也不会对别人有特殊的访问权限。


2.2 类模板的成员函数

成员函数与模板共享模板参数。



成员函数可以定义在类内部,也可定义在类外部。


类外部的成员函数要以关键字template开始,后接模板参数列表;然后才是返回值类型,函数名,参数列表。


2.3 类模板成员函数的实例化

默认情况下,类模板的成员函数只有在被使用的时候,才实例化。


没被使用,就不会被实例化。



这一特性使得,即使某类型不能完全符合模板 *** 作的要求,依然能用该类型实例化类。


2.4 在类作用域内简化模板类名的使用

当使用一个类模板类型时必须提供模板实参,这个规则有个例外。


在类模板自己的作用域中,可以直接使用模板名,而不提供模板实参,编译器会假定我们使用的类型与当前实例化所用类型一致。



如下例所示:

template<typename T> Name
{
	Name& operator++();		在类内声明一个++运算符,该运算符要返回自身类型的引用
	vector<T> vec;
};

template<typename T> Name<T>& Name<T>::operator++()		定义++运算符
{
	Name ret = *this;
	// ...逻辑就不管了,只是举个例子... //
	return ret;
}

从上往下看,在类内,++运算符的返回值类型是Name&而不是Name&,这就是在类内,可以直接使用模板名。


往下看,++运算符的定义,返回值类型是Name&而不是Name&,因为此时还不在类的作用域中,直到遇到作用域运算符才表示进入类模板的作用域。



可以看到,返回值类型之后,紧接着的就是类模板的作用域。


因此可以看到,函数体内定义ret时无需重复模板实参。


ret的定义与Name ret = *this等价。


2.5 类模板和友元

试想,类模板里声明了友元会怎样?共有如下四条

  • ① 若一个类模板包含一个非模板友元,则该友元可以访问所有类模板实例。


  • ② 若一个非模板类包含一个模板友元,则所有友元实例可以访问该类。


  • ③ 若一个类模板和友元模板有相同的类型参数,则类与友元为 一对一 的友好关系。


  • ④ 若一个类模板与友元模板有不同的类型参数,则类的每一个实例授权给所有友元模板实例。


下面列出具体的例子。


模板类 和 友元模板 一对一 友好关系
template<typename> class BlobPtr;	 前置声明,在Blob中声明为友元所需要的

template<typename> calss Blob;		 前置声明,声明 operator== 函数所必要的

template<typename T> 				 前置声明,在Blob中声明为友元所必要的 *** 作
	bool operator==(const Blob<T>&, const Blob<T>);

template<typename T> class Blob
{
	friend class BlobPtr<T>;		 引用类模板的特定实例
	friend bool operator==<T>(const Blob&, const Blob&);	引用函数模板的特定实例
	// ...其他成员省略... //
};

声明友元本身并不需要友元的前置声明,但如果友元是一个模板 且 在声明友元时引用了他的某个实例,就必须前置声明了。



上面代码中,为了在Blob中引用特定实例,我们必须先声明类模板BlobPtr和函数模板operator==,由于operator==中含有Blob的实例,所以还要在operator==声明前,先声明Blob类模板。


然后再看Blob的友元声明,BlobPtroperator==都使用Blob的模板形参引用各自的特定实例,所以友元关系被限定在用相同类型实例化的Bloboperator==


普通类 与 友元模板

有一对一 和 一对多两种情况:

template<typename T> class Pal;		下面要引用他的特定实例,所以要前置声明

class C
{
	friend class Pal<C>;		用类 C 实例化的 Pal 是C的友元
	template<typename T> friend class Pal2;		Pal2的 所有 实例化都是C的友元
};

上面的Pal2没有引用实例,所以不用前置声明。


模板类 和 普通友元
template<typename T> class C2
{
	template<typename X> friend class Pal2;		Pal2的所有实例都是C2所有实例的友元,多对多
	friend class Pal3;		Pal3是所有 C2 实例的友元,多对一
};
模板自己的类型参数成为友元
template<typename T> class Bar
{
	friend T;	把访问权限授予用来实例化Bar的类型
};
2.6 模板 与 typedefusing typedef

定义一个typedef引用实例化的类。


typedef Blob<string> StrBlob;

使用StrBlob就相当于使用一个用string实例化的Blob



由于模板不是一个类型,所以不能定义一个typedef引用模板。


但是,新标准允许用using为类模板定义类型别名。


using
template<typename T> using twin = pair<T, T>;
twin<double> t1;	t1 是一个 pair<double, double>

template<typename T, typename X> using twin2 = pair<T, X>;
twin2<double, int> t2;		这样就跟直接使用 pair 没啥区别了

template<typename T> using twin3 = pair<double, T>;
twin3<int> t3;		也可以这样,自己固定一个类型,t3 是一个 pair<double, int>
					用户可以指定second成员的类型,但不能指定first成员的类型
2.7 类模板的stiatic成员

每个类模板的实例 都有各自的 static成员实例。



与普通类的static数据成员一样,类模板的static数据成员仍要在类外初始化。


不同的是,类模板的static数据成员应该定义为模板:

template<typename T> class Foo
{
public:
	static int count() { return cnt; }
private:
	static int cnt;
};

template<typename T> int Foo<T>::cnt = 10;		这里就不用加 static

注意上面cnt的初始化格式。


与类外定义类模板的成员函数格式类似,模板参数列表起手。


别忘记加上类模板的作用域。


和其他模板成员函数一样,static成员函数也是只有在使用到的时候,才会实例化。


3、模板参数 3.1 模板参数 和 作用域

模板参数遵循普通的作用域规则。


一个模板参数名的可用范围在其声明之后,直到模板声明或定义结束之前。


同样的,模板参数也会隐藏外部作用域中声明的同名名字。


不同的是,在模板作用域内不能重用模板参数名。


如下代码所示:

typedef double A;
template<typename A, typename B> void func1(A a, B b)
{
	A tmp = a;		tmp 的类型为模板参数 A 的类型,而不是 double,A 的typedef被类型参数 A 隐藏
	double B;		错误,重声明模板参数 B
}

template<typename A, typename A> func2(A a);	错误,重用模板参数名 A
3.2 使用类的类型成员

前面我们用作用域运算符 :: 访问类的static成员 或是 类型成员,如下代码所示:

string::size_type		访问 string 的类型成员,这是一条不完整的代码,访问static成员就不再举例

在普通类的代码中,编译器掌握类的定义,编译器清楚的知道::前面的类里面定义了什么东西,因此,他知道通过作用域运算符访问的名字是类型还是static数据成员。


编译器有string的定义,所以知道 size_type 是一个类型。


但同样的 *** 作对于模板代码就存在困难。


例如,假定T是一个模板类型参数(注意,是模板类型参数而不是模板名),T可能是很多种类型,编译器不知道T里面定义了什么东西,自然也就不知道::后面的是什么东西了,当编译器遇到T::mem这样的代码时,他不知道mem是一个类型成员还是static数据成员,直到模板实例化后才知道,但为了实例化这个模板,编译器还必须知道名字是否表示一个类型。



看下面这个例子:

template<typename T> class X
{
	T::size_type * p;		这表示的是 p 是一个指针变量呢?
};								还是 T 里名为 size_type 的static数据成员与变量 p 相乘呢?

默认情况下,C++语言认为通过作用域运算符访问的名字不是类型。


如果希望使用模板参数的类型成员,必须显式告诉编译器改名字是一个类型。


使用关键字typename实现这一点:

template<typename T> typename T::value_type func(const T& t)
{
	return typename T::value_type();		默认初始化一个value_type类型的变量,并返回
}

3.3 默认模板实参

C++新标准中,可以为函数模板和类模板提供默认模板实参,以前只能为类模板提供默认模板实参。


函数模板 的默认模板实参

例如,使用标准库的less函数对象模板实现compare函数模板:

template<typename T, typename F = less<T>> int compare(const T& v1, const T& v2, F f = F())
{
	if (f(v1, v2)) return 1;
	if (f(v1, v1)) return -1;
	return 0;
}

这段代码比较绕,先说模板类型参数列表中的默认实参less



首先要明确,less是一个 函数对象 模板,它接受 要比较的对象的类型,作为自己的模板类型参数,实例化出 比较某种类型对象的 函数对象类型。



也就是说,less是一个类型,该类型的对象能比较T类型的对象。



F是一个模板类型参数,它的初始值,正是实例化出的某个less类型。


再看函数的默认实参F()


它是调用了F的默认构造函数,默认初始化了一个函数对象,赋给f


所以f是一个函数对象,他的调用运算符可以比较两个对象的大小。


类模板 的默认模板实参

这就好理解多了。


template<typename T = int> class Numbers
{
private:
	T value;
};

Numbers<> num1;<>,表示使用默认类型,int 类型的 Numbers
Numbers<double> num2;	double 类型的 Numbers

不过要注意的是,任何时候使用一个类模板,都必须在模板名之后接上尖括号。


尖括号指出:类从一个模板实例化而来。


即使你想使用默认模板类型实参,也要写一对空的尖括号。


4、成员模板

任何一个类可以包含本身是模板的成员函数,这叫做 成员模板。


成员模板不能是虚函数。


4.1 普通类 和 成员模板

以下面的 “自定义delete” 为例:

class DebugDelete
{
public:
	DebugDelete(ostream& s = cerr) : os(s) { }
	template<typename T> void operator()(T* p)
	{
		cerr << "deleteing unique_ptr" << endl;
		delete p;
	}
private:
	ostream& os;
};

double* p1 = new double(3.14);
DebugDelete d;
d(p1);		销毁 p1 指向的动态内存

unique_ptr<int, DebugDelete> p2(new int, DebugDelete());	
这是 unique_ptr 的一个构造函数,接受自定义的删除器对象
这里是分配了一个临时的匿名int,然后立即销毁
4.2 类模板 和 成员模板

对于类模板 及其 成员模板,他们各自 有自己的、独立的模板参数。


template<typename T> class Blob
{
public:
	template<typename Iter> Blob(Iter begin, Iter end);
	// ...
};

与类模板的普通成员函数不同,成员模板是函数模板,当在类外定义一个成员模板时,必须同时 为类模板和成员模板提供参模板数列表。


类模板的参数列表在前,成员模板的参数列表在后。


template<typename T> template<typename Iter> Blob<T>::Blob(Iter begin, Iter end)
{
	// ......
}

上面的定义中,类模板有一个模板类型参数,命名为T


而成员自身是一个函数模板,有一个名为Iter的类型参数。


4.3 成员模板的 实例化

从代码上来看,成员模板实例化时仍然遵循普通函数模板的规则,只不过是要和类模板的实例化结合起来。



接上一小节的代码:

int arr[] = { 1, 2, 3, 4, 5 };
vector<double> v = { 1.1, 2.2, 3.3, 4.4, 5.5 };
list<const char*> li = { "aa", "ww", "ee", "xx", "dd" };

Blob<int> b1( begin(arr), end(arr) );
Blob<double> b2( v.begin(), v.end() );
Blob<string> b3( li.begin(), li.end() );

对于类模板的类型参数,仍然需要显式提供,而函数模板的模板实参,编译器根据函数实参自己推断。


5、控制实例化

当模板被使用时才会实例化,这会产生一个问题:当两个或多个独立变异的源文件使用了相同的模板,并提供了相同的模板参数时,相同的实例就出现在了多个对象文件中。


这样的开销在大系统中是经受不起的。


在新标准中,通过显式实例化来避免这种开销。


5.1 什么是显式实例化

显式实例化就是两个 *** 作,结合使用这两个 *** 作就能避免额外的开销。


实例化定义

(看清主语,是 实例化定义,不是模板定义,是有区别的)

template declaration;	这比较抽象

相比于模板的声明或定义,实例化定义就是:省略模板参数列表,但走一个template,还要给模板参数提供具体的实参。



既然都提供了实参,所以会在当前文件内 生成一个对应实例


实例化声明
extern template declaration;

就是在实例化定义前面加一个extern


所以,也是要显式的提供模板类型实参的。



但并不会生成实例,extern表示:承诺了 在程序其他位置有该实例化的一个实例化声明,也就是有代码了,不用再生成了。



由于编译器使用模板时自动对其实例化,所以要提前写好extern声明。


对于某个给定的实例化版本,可以有多个extern声明,但只能有一个实例化定义。



对于每个实例化声明,程序中某个位置必有其显式的实例化定义。


5.2 怎么用?看例子

现有Application.cc文件,如下所示:

extern template class Blob<double>;						实例化声明
extern template int compare(const int&, const int&);	实例化声明

Blob<double> b1;		实例化出现在其他文件

Blob<int> b3 = { 0, 1, 2, 3 };	  Blob<int>及其接受initializer_list的构造函数在本文件中实例化
Blob<int> b4(b3);				  Blob<int>的拷贝构造函数也在本文件中实例化

int i = compare(b3[0], b4[0]);	  实例化出现在其他文件

还有一个templateBuild.cc文件:

template int compare(const int&, const int&);	
template class Blob<string>;	实例化该类型模板的所有成员

当编译整个应用程序时,需要把templateBuild.oApplication.o连接到一起。


当然这个工作不需要我们完成。


5.3 实例化定义 会实例化所有成员

预处理类模板的普通实例化不同,实例化定义会实例化该模板的所有成员。


因为,实例化定义的时候,编译器并不知道哪些成员函数会被用到,所以会全部实例化。


书上一个比较好的例题:

第二个例题:

这题告诉了我:什么叫用到的时候?
只要模板中出现了实参,就算用到,就要实例化。


欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/567303.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-04-09
下一篇 2022-04-09

发表评论

登录后才能评论

评论列表(0条)

保存