前言
该系列是《C++Primer第五版》的笔记,包含本人认为值得记录和整理的主要的知识点,并不是全部内容,也不是具体的内容。
该系列文章的作用应该是作为复习或预习的参考,有哪些知识点忘记或想学,可以大致浏览下该文章,然后再去书中寻找详细解答。(本系列文章基本是按书本顺序罗列的知识点,便于大家去书中寻找)
所以看该文章前,需要有一定的C++基础,否则阅读起来可能有困难。
本文大致整理了第十六章的知识点,涉及到C++关于模板与泛型编程的知识,有一定难度。
链接目录
- 第二章:变量与基本类型
- 第三章:字符串、向量和数组
- 第四章:表达式
- 第五章:语句
- 第六章:函数
- 第七章:类
- 第八章:IO库
- 第九章:顺序容器
- 第十章:泛型算法
- 第十一章:关联容器
- 第十二章:动态内存
- 第十三章:拷贝控制
- 第十四章:重载运算与类型转换
- 第十五章:面向对象程序设计
- 第十六章:模板与泛型编程
- 第十七章:标准库特殊设施
- 第十八章:用于大型程序的工具
- 第十九章:特殊工具与技术
函数模板
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");
inline
和constexpr
的函数模板:
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_reference
用string
进行实例化remoeve_reference
的type
成员是string
move
的返回类型是string&&
move
函数参数t
的类型是string&&
std::move(s1);
的执行过程:string&& move(string &t)
将一个右值引用绑定到左值上- 推断
T
类型为string&
remove_reference
用string&
进行实例化remove_reference
的type
成员是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
,尖括号为空。而且类模板可以部分特例化,还可以特例化某个成员。