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

前言

该系列是《C++Primer第五版》的笔记,包含本人认为值得记录和整理的主要的知识点,并不是全部内容,也不是具体的内容。
该系列文章的作用应该是作为复习或预习的参考,有哪些知识点忘记或想学,可以大致浏览下该文章,然后再去书中寻找详细解答。(本系列文章基本是按书本顺序罗列的知识点,便于大家去书中寻找)
所以看该文章前,需要有一定的C++基础,否则阅读起来可能有困难。

本文大致整理了第十六章的知识点,涉及到C++关于模板与泛型编程的知识,有一定难度。

链接目录

函数模板

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

//在调用时会实例化一个特定版本的函数
cout << compare(1, 0) << endl;//T为int

模板类型参数:

template <typename T> T foo(T* p){
	T tmp = *p;
	...
	return tmp;
}
//类型参数前必须加上typename
template <typename T, typename U> T calc(const T&, const U&);

非类型模板参数:非类型参数必须是一个常量值,在编译时就可确定

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

inlineconstexpr的函数模板:

template <typename T> inline T min(const T&, const T&);

模板编译:模板的头文件通常既包括声明也包括定义。

类模板

定义类模板:

template <typename T> class Blob{
public:
	typedef T value_type;
	//typename是必须的,他显式告诉这是一个类型名
	typedef typename std::vector<T>::size_type size_type;
	Blob();
	Blob(std::initializer_list<T> il);
	size_type size() const {return data->size();}
	bool empty() const {return data->empty();}
	void push_back(const T &t){data->push_back(t);}
	void push_back(T &&t){data->push_back(std::move(t));}
	void pop_back;

	T& back();
	T& operator[](size_type i);
private:
	std::shared_ptr<std::vector<T>> data;
	void check(size_type i, const string &msg) const;
};

//构造函数
template <typename T>
Blob<T>::Blob():data(std::make_shared<std::vector<T>>()){}
template <typename T>
Blob<T>::Blob(std::initializer_list<T> il)
	:data(std::make_shared<std::vector<T>>(il)){}

实例化类模板:

Blob<int> ia;
Blob<int> ia2 = {0,1,2,3,4};

//此时会生成一个特定版本的Blob,将T替换为int

类模板的成员函数:定义在类模板之外的成员函数必须以关键字template开始,类模板的成员函数也只有在被使用时才会实例化。

template<typename T>
return_type Blob<T>::member_name(list){
	...
}

在类代码内简化模板类名的使用:在类模板的作用域中,可以省略模板实参

template <typename T> class BlobPtr{
public:
	...
	BlobPtr& operator++();//等价于BlobPtr<T>& operator++()
	...
}

在类模板外使用类模板名:在类模板外则不在作用域中,所以不能省略模板实参,除非遇到BlobPtr<T>::之后才可以省略。

类模板和友元:

  • 一对一友好关系:
//声明
template <typename> class BlobPtr;
template <typename> class Blob;
template <typename T>
bool operator==(const Blob<T>&, const Blob<T>&);

template <typename T> class Blob{
	//每个Blob实例将同类型的BlobPtr和相等运算符视为友元
	friend class BlobPtr<T>;
	friend bool operator==<T>(const Blob<T>&, const Blob<T>&);
};
  • 通用和特定的模板友好关系:
template <typename> class Pal;
class C{
	friend class Pal<C>;//只有Pal<C>才是友元
	//Pal2的所有实例都是C的友元,无需前置声明
	template <typename T> friend class Pal2;
};
template <typename T> class C2{
	friend class Pal<T>;//相同类型的实例化才是友元
	//Pal2所有实例化都是C2的友元,无需前置声明
	//必须额外声明个不同名的模板参数
	template <typename X> friend class Pal2;
	//Pal3是非模板类,它是C2所有实例的友元,无需前置声明
	friend class Pal3;
};

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

//例如,Foo将成为Bar<Foo>的友元
template <typename Type> class Bar{
	friend Type;
};

模板类型别名:提供的必须是实例化的版本

typedef Blob<string> StrBlob;
//C++11允许非实例化的别名
template<typename T> using twin = pair<T, T>;
twin<string> authors;//等价pair<string, string> authors
//也可以固定模板参数
template<typename T> using book = pair<string, int>;
book<string> books;//等价于pair<string, int>

类模板的static成员:对于任意Foo<X>类型,所有Foo<X>类型的对象共享相同的static成员,即每种实例化的类有一个独立的static成员,所以static成员也可以包含模板参数

template <typename T> class Foo{
public:
	static T num;
};

//必须包含此条,可以不必初始化,但必须声明
template <typename T> T Foo<T>::num = 0;

//使用
int i = Test<int>::num;

模板参数

模板参数名可用于声明之后,定义结束之前,会隐藏外层作用域的相同名字。
模板内也不能重用模板参数名,模板声明还必须包含模板参数

//即使是个函数声明,模板参数也要给定,但可以不同名
template <typename T> void f(T t);//声明
template <typename A> void f(A a){
	double A;//错误,重声明
}

使用类的类型成员:在模板中对于T::mem无法确定mem是类型成员还是static数据成员,因为此时并没有类的实例化代码,编译器无法判断。C++默认情况作用域运算符访问的是名字而不是类型,所以我们要显式告诉编译器是一个类型。

typename T::value_type val;//val是一个T中的value_type类型

模板默认实参:

template <class T = int> class Numbers{//T默认为int
public:
	Numbers(T v = 0):val(v){}
private:
	T val;
};

Numbers<> e;//空<>表示默认类型

成员模板

成员模板:本身是模板的成员函数

class DebugDelete{
public:
	DebugDelete(ostream &s = cerr):os(s){}
	//成员模板
	template <typename T> void operator()(T *p) const{
		os << "deleting unique_ptr" << endl;
		delete p;
	}
private:
	ostream &os;
};

double* p = new double;
DebugDelete d;
d(p);//调用DebugDelete::operator()(double*)
int* ip = new int;
DebugDelete()(ip);//在临时DebugDelete的对象上调用operator()(int*)

//new int参数为初始化智能指针指向,DebugDelete()创建删除器对象
unique_ptr<int, DebugDelete> p(new int, DebugDelete());

类模板的成员模板:

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

template <typename T>//类的类型参数
template <typename It>//构造函数的类型参数
Blob<T>::Blob(It b, It e):data(std::make_shared<vector<T>>(b, e)){}

//进行实例化
int ia[] = {0,1,2};
Blob<int> a1(begin(ia), end(ia));
vector<string> v1 = {"a", "b", "c"};
Blob<string> a2(v1.begin(), v1.end());

控制实例化

当模板被使用时才会实例化,因此相同的实例可能出现在多个对象文件中,造成额外的开销。可以通过显式实例化来避免这种开销:

//declaration是一个类或函数声明,其中所有模板参数被替换成模板实参
extern template declaration;//声明,可以有多处
template declaration;//定义,只能有一次

当遇到extern模板声明时,不会在本文件中生成实例化代码,在编译时,将实例化代码和本文件链接在一起。

一个类模板的实例化定义会实例化该模板的所有成员,包括内联函数。生成一个实例化的代码,即使某些部分使用不到。

效率与灵活性

在运行时绑定删除器:shared_ptr在其生存期内可以随时改变删除器的类型,其删除功能通过删除器指针执行。在进行删除操作时才实例化。

del ? del(p) : delete p;//del(p)运行时跳转至del的地址

在编译时绑定删除器:unique_ptr的删除器成员在编译时就确定,直接进行实例化,没有运行时绑定的额外开销。

模板实参推断

编译器通过函数实参来确定模板实参,这一过程叫做模板实参推断。

类型转换与模板类型参数

对于模板类型参数的类型转换有特殊性,编译器能够自动完成以下的转换:

  • const转换:将一个非const对象的引用或指针传递给const引用或指针形参
  • 数组或函数指针转换:即使传入的不是引用类型,也会自动转换成指针

其他类型转换,算术转换、派生类向基类的转换、用户定义的转换都不能引用于函数模板。

普通函数实参执行正常的类型转换,不进行特殊处理。

函数模板显式实参

template <typename T1, typename T2, typename T3>
T1 sum(T2, T3);

auto val = sum<long long>(i, lng);//返回类型由尖括号指定,参数类型由i和lng的类型推断

尾置返回类型与类型转换

当需要的返回类型需要用参数表达时,只能使用尾置返回(在确定返回类型时已经读取过参数列表,从而得知返回类型)。

template <typename It>
auto fcn(It beg, It end) -> decltype(*beg){
	...
	return *beg;
}

进行类型转_换的标准库模板类:定义在头文件type_traits中,可以通过remove_reference来获得元素类型。

//返回的是一个值,而非引用
template <typename It>
auto fcn(It beg, It end) -> typename remove_reference<decltype(*beg)>::type{
	...
	return *beg;//返回一个拷贝
}

函数指针和实参推断

template <typename T> int compare(const T&, const T&);
//指向实例化的int compare(const int&, const int&)的函数指针
int (*pf1)(const int&, const int&) = compare;//compare<int>也行

void func(int(*)(const string&, const string&));
void func(int(*)(const int&, const int&));
func(compare<int>);//指出哪个重载版本

模板实参推断和引用

从左值引用推断类型:

  • 参数非const,但传递是const,则自动转换成const
  • 参数是const,则所有都是const,且可以传递右值
template <typename T> void f1(T&);

f1(i);//i是一个int,类型T是int
f1(ci);//ci是一个const int,类型T是const int
f1(5);//参数必须是左值

template <typename T> void f2(const T&);
f2(i);//类型T是int
f2(ci);//类型T是int
f2(5);//可以传递右值,类型T是int

从右值引用推断类型:类似左值引用推断类型

引用折叠:间接创建了引用的引用,有两种实现方式

  • 模板类型参数
  • 类型别名
    此时会发生引用折叠:
  • X& &X& &&X&& &都折叠成类型X&
  • 类型X&& &&折叠成X&&
template <typename T> void f3(T&&);
//当我们给参数列表为右值的函数传入左值,会发生引用折叠
f3(i);//折叠成void f3(int& &&)等价于void f3(int&)

如果一个函数参数是模板参数类型的右值引用,可以传递任意类型的实参,如果传递的是左值,即将被实例化成一个普通的左值引用函数。

于是可能会出现令人困惑的代码异常:

//当val是一个右值传入时,代码正常执行
//当val是一个左值传入时,此时val成为一个引用,t为val为相同的引用,对t的改变同样对val生效
template <typename T> void f3(T&& val){
	T t = val;
	t = fcn(t);
	if(val == t){//若T是引用类型,出现死循环
		...
	}
}

此时可以采用函数重载的方式避免出现异常,定义右值和左值两个版本的函数。

理解move

std::move的定义:

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

string s1("hi"), s2;
s2 = std::move(string("bye"));//正确
s2 = std::move(s1);//正确,但赋值后s1的值不确定

std::move(string("bye"));的执行过程:string&& move(string &&t)

  • 推断T的类型为string
  • remoeve_referencestring进行实例化
  • remoeve_referencetype成员是string
  • move的返回类型是string&&
  • move函数参数t的类型是string&&
    std::move(s1);的执行过程:string&& move(string &t)将一个右值引用绑定到左值上
  • 推断T类型为string&
  • remove_referencestring&进行实例化
  • remove_referencetype成员是string
  • move的返回类型是string&&
  • move函数参数t的类型是string& &&折叠为string&

转发

我们希望实参列表能不变地转发给其他函数(实参可能会发生某些隐式的类型转换)包括类似是否是const以及是左值还是右值,可以使用std::forward来保持类型信息。

重载与模板

函数匹配:有多个函数提供同样好的匹配

  • 如果同样好的函数只有一个非模板,则选择此函数
  • 如果同样好的函数没有非模板函数,且有多个模板,则其中一个模板比其他模板更特例化,则选择此模板
  • 否则调用有歧义

缺少声明可能导致程序行为异常:
对于重载函数模板的函数,如果忘记声明,编译器可以从模板实例化出一个匹配的版本,导致与预期不符的行为。

可变参数模板

参数包:可变数目的参数

  • 模板参数包:零个或多个模板参数
  • 函数参数包:零个或多个函数参数
//Args为模板参数包名,rest为函数参数包名
template <typename T, typename... Args>
void foo(const T &t, const Args& ... rest);

int i;
double d;
string s;
foo(i, s, d);//包中有两个参数
//实例化void foo(const int&, const string&, const double&)
foo("s", d);//包中有一个参数
//实例化void foo(const char[2]&, const double&);

获取参数个数:sizeof...()

template <typename ... Args> void g(Args ... args){
	cout << sizeof...(Args) << endl;//类型参数数目
	cout << sizeof...(args) << endl;//函数参数数目
}

编写可变参数函数模板

可变参数模板通常是递归的:

//用来终结递归,非可变参数模板比可变参数模板更特例化
template <typename T>
ostream &print(ostream &os, const T &t){
	return os << t;
}
//递归调用,每递归一次参数少一个
template <typename T, typename... Args>
ostream &print(ostream &os, const T &t, const Args&... rest){
	os << t << ",";
	return print(os, rest...);
}

包扩展

在模式右边放...进行扩展操作:

//第一个...扩展模板参数包,生成函数参数列表,第二个为print调用生成实参列表
template <typename T, typename... Args>
ostream &print(ostream &os, const T &t, const Args&... rest){
	os << t << ",";
	return print(os, rest...);
}

//更复杂的包扩展
print(os, debug_rep(rest)...);
//等价于print(os, debug_rep(a1), debug_rep(a2), ...)
print(os, debug_rep(rest...));
//等价于print(os, debug_rep(a1, a2, ...))

转发参数包

//通过右值引用,将参数完整保持地传递给work函数
template <typename... Args>
void fun(Args&&... args){
	work(std::forward<Args>(args)...);
}

模板特例化

模板特例化是一个模板的实例,并不是一个重载版本的函数,不会影响到函数匹配。

//函数模板
template <typename T> int compare(const T&, const T&);
//函数模板特例化,需要保持参数列表一致,只能对T进行替换
template <> int compare(const char* const &p1, const char* const &p2){
	...
}

如果在使用模板实例之前,特例化版本没有声明,编译器会通过模板生成代码,很容易产生模板和特例化版本的声明顺序错误,这种错误很难查找。所以模板及其特例化版本应该声明在同一个头文件中。

特例化类模板:与函数模板特例化类似,替换T,尖括号为空。而且类模板可以部分特例化,还可以特例化某个成员。

上一篇 下一篇

评论 | 0条评论