C++Primer笔记——模板与泛型编程

泛型编程,在编译时获知类型。标准库中的容器、迭代器和算法都是泛型编程的例子。

16.1 定义模板

当多个函数体完全一样,唯一的差异是参数类型时,可以使用模板而不是对该函数进行多次重载。

16.1 函数模板

一个函数模板就是一个公式,用来生成针对特定类型的函数版本。以比较大小函数compare为例

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

模板定义:
template <typename T1, Typename T2, … >(模板参数列表)

实例化函数模板
在调用函数模板时,编译器通常用函数实参来自动推断模板实参。编译器用推断出的模板参数来实例化一个特定版本的函数,称作模板的实例

模板类型参数
模板类型参数可以看作类型说明符,就像内置类型或者类类型说明符,可以用来指定返回类型或者函数的参数类型,以及在函数体内用于变量声明或者类型转换。
注意:类型参数前必须使用关键字 class或typename(二者相同且可同时使用)

非类型模板参数
模板中定义非类型参数,用于表示一个值。使用特定的类型名进行声明。
一个非类型参数可以是一个整型,或者是一个指向对象或函数类型的指针或左值引用。
其中绑定到非类型整型参数的实参必须是一个常量表达式,绑定到指针或引用非类型参数的实参必须具有静态的生存期。(模板定义内,模板非类型参数是一个常量值,可以在需要常量表达式的地方使用非类型参数,例如指定数组大小)

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

//调用 compare("hello","world")

inline和constexpr的函数模板 关键字放在模板参数列表之后。

编写类型无关的代码

编写泛型代码的重要原则(类型无关性与可移植性)

  • 模板中的函数参数是const的引用:保证函数用于不能拷贝的类型;
  • 尽可能少地使用运算符的种类,降低了函数对要处理的类型的要求。

——模板程序应当尽量减少对实参类型的要求。

模板编译
编译器遇到模板定义时并不生成代码,只有当我们实例化模板的一个特定版本时,编译器才会生成代码。
为了生成一个实例化版本,编译器需要掌握函数模板或类模板成员函数的定义。因此,模板的头文件通常既有声明也有定义。
模板的用户必须包含模板的头文件以及用来实例化模板的任何类型的头文件。

模板编译错误
通常,编译器会在三个阶段报告错误:

  • 编译模板本身时:检查语法错误。
  • 编译器遇到模板使用时:检查函数模板实参数目;检查类模板是否被用户提供了正确数目的模板实参。
  • 模板实例化时:发现类型相关的错误。

16.1.2 类模板

类模板用于生成类的蓝图。

定义类模板
类模板提供对不同类型的相同的一组功能,当使用类时,用户需要指出元素类型。

template <typename T> class A{
public:
	T get_val(){
		return member;
	}
	//...
private:
	T member;
}

实例化类模板
对类模版进行实例化时,如果只声明了类模版但未定义,则实例化会产生不完整的类型,报错。

A<int> a;
a.get_val();

类模版的实例化包含两部分:隐式实例化和显式实例化。
隐式实例化:

  1. 当代码使用类模版定义对象时,需要在上下文中引用完全定义类型;构造指向此类型指针时不会实例化;
  2. 当类型的完整性影响代码时,且该特定类型尚未进行显式实例化时,就会发生隐式实例化;

显式实例化

  1. template class-key template-name < argument-list >
    显式实例化定义强制实例化它们所引用的类。 它可以出现在模板定义之后的程序中的任何位置,并且对于给定的参数列表,在整个程序中只允许出现一次,不需要诊断。
  2. extern template class-key template-name < argument-list > (since C++11)
    显式实例化声明(外部模板)跳过隐式实例化步骤:否则会导致隐式实例化的代码改为使用其他地方提供的显式实例化定义(如果不存在此类实例化,则会导致链接错误)。这可用于通过在除使用它的源文件之一之外的所有源文件中显式声明模板实例化并在其余文件中显式定义它来减少编译时间。

在模板作用域中引用模板类型
类模板的名字不是一个类型名。类模板用来实例化类型,而一个实例化的类型总是包含模板参数的,且可嵌套。

类模板的成员函数
定义在类模板内的成员函数被隐式声明为内联函数,而在类模板外定义一个成员时,必须说明属于哪个类模板:

template <typename T> (inline/constexpr) T A<T>::get_val();

类模板成员函数的实例化
默认情况下,一个类模板的成员函数只有当程序用到它时才会进行实例化。如果一个成员函数没有被使用,则不会被实例化。

在类代码内简化模板类名的使用
类模板内使用类模板类型时,可以省略< T >部分。
在类模板外使用类模板名
这时则必须使用模板参数< T >。

类模板和友元
如果友元是非模板友元,则友元可以访问所有模板实例。如果友元自身是模板,类可以授权给所有友元模板实例,也可以只授权给特定实例。

一对一友好关系
类模板与另一个(类或函数)模板间友好关系:建立对应实例及其友元之间的友好关系。
为了引用类或函数模板的一个特定实例(作为友元),我们必须首先声明模板自身(模板参数列表+模板名)

template <typename> class A; 
template <typename> class B; //operator==中参数所需要的
template <typename T> bool operator==(const B<T>&,const B<T>&);
template <typename T> class B{
	friend class A<T>;
	friend bool operator==<T>(const B<T>&, const B<T>&);//只有相同类型的A和==运算符可以访问B中成员
};

通用和特定的模板友好关系
为了让所有实例成为友元,友元声明中必须使用与类模板本身不同的模板参数。如果相同,则友好关
系被限定在用相同类型实例化的模板之间。

令模板自己的类型参数成为友元

新标准中可以将模板类型参数声明为友元:

template <typename Type> class A{
friend Type; //将访问权限授予用来实例化A的类型
};

模板类型别名

类模板的一个实例定义一个类类型,因此我们可以定义一个typedef来引用实例化的类:

typedef A< int > A;

由于模板本身不是类型,因此不可以使用typedef来进行引用。新标准允许为类模板定义一个类型别名

template<typename T> using twin=pair<T,T>;
twin<string> authors; 

当我们定义一个模板类型别名时,可以固定一个或多个模板参数;

类模板的static成员
每个类模板的实例都有自己独有的static对象,模板类的每个static数据成员必须有且仅有一个定义。

16.1.3 模板参数

模板参数与作用域
模板参数遵循普通的作用域规则。
作用域:模板参数名声明之后,模板声明结束或定义结束之前。
模板参数隐藏外层作用域中声明的相同的名字。
(模板作用域内不能重用模板参数名,且模板参数名在一个特定模板列表中只能出现一次)

模板声明
模板声明必须包含模板参数,声明中的模板参数名字不必与定义中的相同。一个给定模板的每个声明和定义必须有相同数量和种类的参数。

使用类的类型成员
使用模板类型参数的类型成员,必须显式告诉编译器改名字是一个类型,使用关键字typename实现:

template<typename T> typename T::value_type top(const T& c){
	//...
}

默认模板实参
旧标准:只允许为类模板提供默认实参
新标准:可以为函数和类模板提供默认实参

//重写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(v2,v1)) return 1;
	return 0;
}

模板默认实参与类模板
无论何时使用一个类模板,都必须在模板名之后接上尖括号。如果类模板声明时使用到默认实参,在类实例化时如果利用默认实参则在类模板后加上空尖括号。

16.1.4 成员模板

一个类可以包含本身是模板的成员函数(成员模板)。

普通(非模板)类的成员模板

class A{
public:
	A(){}
	template<typename T> void func(T *p) const{
		//do something with *p
	}
};

类模板的成员模板

template <typename T> class A{
	template<typename It> A(It b, It e);
};

//类外定义
template <typename T> template <typename It> A<T>::A(It b,It e){
}

实例化与成员模板

为了实例化一个类模板的成员模板我们必须同时提供类和函数模板的实参。

16.1.5 控制实例化

模版被使用时才会进行实例化,相同的实例可能会出现在多个不同的源文件中。(每个文件中都有该模版的一个实例)

新标准中可以通过显式实例化避免这种开销。

extern template declaration; //实例化声明
template declaration; //实例化定义

declaration是一个类或函数声明,其中所有模版参数已经被替换为模版实参,例如

extern template class A<string>; //声明
template int compare(const int&,const int&) //定义

当编译器遇到extern模版声明时,他不会在本文件中生成实例化代码 ,代表程序员应当在程序其他位置有该实例化的一个非extern声明或定义。一个给定的实例化版本可以有多个extern声明,但只有一个定义。extern声明必须出现在任何使用此实例化版本的代码之前

实例化定义会实例化所有成员
类模版的实例化定义会实例化该模版的所有成员,包括内联的成员函数。当我们显式实例化一个类模版的类型时,必须能用于模版的所有成员。

16.1.6 效率与灵活性

以shared_ptr和unique_ptr为例。

16.2 模版实参推断

从函数实参来确定模版实参的过程被称为模版实参推断

16.2.1 类型转换与模版类型参数

编译器通常不是对实参进行类型转换,而是产生一个新的模版实例。

  • 顶层const无论是在形参和实参中都会被忽略。
  • const转换:可以将一个非const对象的引用或指针传递给一个const的引用或指针的形参。
  • 数组或函数指针转换:如果函数形参不是引用类型,则可以对数组或函数类型的实参应用正常的指针转换。注意:数组大小不同时的类型也不同。
  • 算数转换、派生类向基类的转换以及用户定义的转换,都不能应用于函数模版。

使用相同模版参数类型的函数形参
一个模版类型参数可以用作多个函数形参的类型,由于只允许有限的几种类型转换,因此传递给这些形参的实参必须有相同的类型。如果推断出的类型不匹配,则调用就是错误的。

正常类型转换应用于普通函数实参
当函数模版形参的类型中存在普通类型定义时,可以对传入的实参进行正常的类型转换。

16.2.2 函数模版显式实参

当编译器无法推断出模版实参的类型时,我们希望允许用户控制模版实例化:例如,当函数返回类型和参数列表任何类型都不相同时。

指定显式模版实参

template<typename T1,typename T2> T1 example(T2);
auto v3=example<long long>(1); //等价于 long long example(int)

当我们需要对模版参数列表中其中某一个进行显式指定类型时,则其之前的所有模版参数也都必须显式指定类型。
且正常的类型转换可以应用于显式指定的实参。

16.2.3 尾置返回类型与类型转换

当我们不确定返回结果的准确类型是,可以使用尾置返回类型对函数定义:

//这里不知道迭代器begin指向元素的类型
template <typename It> auto fcn(It begin,It end)->decltype(*begin){
	//处理序列
	return *begin;
}

进行类型转换的标准库模版类

#include <type_traits>

Mod< T >

ModTMod< T >::type
remove_referenceX&或X&&X
add_const
add_lvalue_reference
add_rvalue_reference
remove_pointer
add_pointer
make_signed
make_unsigned
remove_extent
remove_all_extents

16.2.4 函数指针和实参推断

当我们使用一个函数模版初始化一个函数指针或为一个函数指针赋值,编译器使用指针的类型来推断模版实参。

template <typename T> int compare(const T&,const T&);
int (*pf1)(const int&, const int&)=compare;

func的重载

void func(int(*)(const string&, const string&));
void func(int(*)(const int&, const int&));
//显式实例化compare的版本
func(compare<int>);

16.2.5 模版实参推断和引用

template <typename T> void f(T &p);
template <typename T> void f2(const T &p);
template <typename T> void f3(T&&);

从左值引用函数参数推断类型
如果函数模版的参数只是一个普通左值引用,那么只能传递给它一个左值,如果实参是const类型,则T也会推断为const类型;
如果函数参数的类型是const T&,那么可以传给他任何类型的实参——一个对象、临时对象或一个字面常量值。且推断出的T类型也不会包含const部分。

从右值引用函数参数推断类型
当一个函数参数是一个右值引用,推断出的T是该右值实参的类型。

引用折叠和右值引用参数
通常右值引用无法绑定到一个左值上,但C++存在两个例外规则允许这种绑定。

  • 当我们将一个左值传递给函数的右值引用参数,且此右值引用指向模版类型参数时,编译器推断模版类型参数为实参的左值引用类型。
  • 如果我们间接创建一个引用的引用,则这些引用形成了“折叠”:
    X& &/ X& &&/ X&& & 折叠为类型X&
    X&& && 折叠为X&&

编写接受右值引用参数的模版函数

template<typename T> void f3(T&& val){
	T t=val;
	t=fcn(t);
	if(val==t){/* ...  */}
}

16.2.6 理解std::move

template <typename T> typename remove_reference<T>::type && move(T&& t){
	return static_cast<typename remove_reference<T>::type&&>(t);
}

注意:从一个左值static_cast到右值引用是允许的。

16.2.7 转发

某些函数需要将其一个或多个实参连通类型不变地转发给其他函数,我们需要保持被转发实参的所有性质,包括实参类型是否为const以及实参是左值还是右值。

定义能保持类型的函数参数

  • 使用引用可以保持参数的const属性
  • 将函数参数定义为右值引用,可以通过引用折叠,保持实参的左值/右值属性。

在调用中使用std::forward保持类型信息

template <typename Type> intermediary(Type &&arg){
	finalFcn(std::forward<Type>(arg));
}

16.3 重载与模版

函数模版可以被另一个模版或一个普通非模版函数重载。必须具有不同数量或类型的参数。

多个可行模版

当多个模版都是可行的而且都是精确匹配时,根据重载函数模版的特殊规则,进行选择版本:

  • 当有多个重载模版对一个调用提供同样好的匹配时,应该选择最特例化的版本。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值