C++ 学习笔记
C++ tepmlate
简介
- Template所代表的 泛型编程 是C++语言中的重要的组成部分,本文是基础篇的第一部分。
为什么要有泛型编程
C++是一门强类型语言,所以无法做到像动态语言(python javascript)那样子,编写一段通用的逻辑,可以把任意类型的变量传进去处理。泛型编程弥补了这个缺点,通过把通用逻辑设计为模板,摆脱了类型的限制,提供了继承机制以外的另一种抽象机制,极大地提升了代码的可重用性。
注意:模板定义本身不参与编译,而是编译器根据模板的用户使用模板时提供的类型参数生成代码,再进行编译,这一过程被称为模板实例化。用户提供不同的类型参数,就会实例化出不同的代码。
函数模版
函数模板定义
- 把处理不同类型的公共逻辑抽象成函数,就得到了函数模板。
1
函数模板可以声明为inline或者constexpr的,将它们放在template之后,返回值之前即可。
普通函数模板
- 下面定义了一个名叫compare的函数模板,支持多种类型的通用比较逻辑。
1
2
3
4
5
6
7
8
9
10
11
12template<typename T>
int compare(const T& left, const T& right) {
if (left < right) {
return -1;
}
if (right < left) {
return 1;
}
return 0;
}
compare<int>(1, 2); //使用模板函数
成员函数模板
不仅普通函数可以定义为模板,类的成员函数也可以定义为模板。
1
2
3
4
5
6
7
8
9
10class Printer {
public:
template<typename T>
void print(const T& t) {
cout << t <<endl;
}
};
Printer p;
p.print<const char*>("abc"); //打印abctemplate 中不仅仅可以是一个
typename
, 也可以是具体类型的变量1
2
3
4
5
6
7
8
9
10
11
12
13
14
15template<typename T, int N>
class Array {
private:
T m_Array[N];
public:
void ShowSize()
{
std::cout << N << std::endl;
}
};
Array<int, 2> p;
// Array<std::string, 2> p;
p.ShowSize(); //打印 2为什么成员函数模板不能是虚函数(virtual)?
1
这是因为c++ compiler在parse一个类的时候就要确定vtable的大小,如果允许一个虚函数是模板函数,那么compiler就需要在parse这个类之前扫描所有的代码,找出这个模板成员函数的调用(实例化),然后才能确定vtable的大小,而显然这是不可行的,除非改变当前compiler的工作机制。
实参推断
- 为了方便使用,除了直接为函数模板指定类型参数之外,我们还可以让编译器从传递给函数的实参推断类型参数,这一功能被称为模板实参推断。
1 | compare(1, 2); //推断T的类型为int |
- 有意思的是,还可以通过把函数模板赋值给一个指定类型的函数指针,让编译器根据这个指针的类型,对模板实参进行推断。
1
int (*pf) (const int&, const int&) = compare; //推断T的类型为int
当返回值类型也是参数时
当一个模板函数的返回值类型需要用另外一个模板参数表示时,你无法利用实参推断获取全部的类型参数,这时有两种解决办法:
返回值类型与参数类型完全无关,那么就需要显示的指定返回值类型,其他的类型交给实参推断。
1
注意:此行为与函数的默认实参相同,我们必须从左向右逐一指定。
1 | template<typename T1, typename T2, typename T3> |
- 返回值类型可以从参数类型中获得,那么把函数写成尾置返回类型的形式,就可以愉快的使用实参推断了。
1 | template<typename It> |
实参推断时的自动类型转换
- 编译器进行模板实参推断时通常不会对实参进行类型转换,只有以下几种情况例外:
- 普通对象赋值给const引用 int a = 0; -> const T&
- 数组名转换为头指针 int a[10] = {0}; -> T*
- 函数名转换为函数指针 void func(int a){…} -> T*
函数模板重载
函数模板之间,函数模板与普通函数之间可以重载。编译器会根据调用时提供的函数参数,调用能够处理这一类型的最特殊的版本。在特殊性上,一般按照如下顺序考虑:
- 1.普通函数
- 2.特殊模板(限定了T的形式的,指针、引用、容器等)
- 3.普通模板(对T没有任何限制的)
对于如何判断某个模板更加特殊,原则如下:如果模板B的所有实例都可以实例化模板A,而反过来则不行,那么B就比A特殊。
1 | template<typename T> |
模板函数特化
- 有时通用的函数模板不能解决个别类型的问题,我们必须对此进行定制,这就是函数模板的特化。函数模板的特化必须把所有的模版参数全部指定。
1
2
3
4
5
6
7template<>
void func(int i) {
cout << "In special version for int "<< i << endl;
}
int i = 10;
func(i); //调用特化版本
类模板
函数模板定义
- 类模板也是公共逻辑的抽象,通常用来作为容器(例如:vector)或者行为(例如:clonable)的封装。
类模板
下面定义了一个Printer类模板,负责打印以及转化为string。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25template<typename T>
class Printer {
public:
// explicit 修饰单个形式参数的构造函数,指定改函数只能显示的调用
explicit Printer(const T& param):t(param){}
string&& to_string();
//定义在内部
void print() {
cout << t << endl;
}
private:
T t;
};
//定义在外部
template<typename T>
string&& Printer<T>::to_string() {
strstream ss;
ss << t;
return std::move(string(ss.str()));
}
Printer p(1); //error
Printer<int> p = 1; //error, 因为构造函数有explicit进行修饰
Printer<int> p(1); //ok与函数模板不同,类模板不能推断实例化。所以你只能显示指定类型参数使用Printer
p(1),而不能让编译器自行推断Printer p(1)。
1 | 类模板的成员函数既可以定义在内部,也可以定义在外部。定义在内部的被隐式声明为inline,定义在外部的类名之前必须加上template的相关声明。 |
类模板中的成员函数模板
- 我们还可以把类模板和函数模板结合起来,定义一个含有成员函数模板的类模板。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21template<typename T>
class Printer {
public:
// explicit 修饰单个形式参数的构造函数,指定改函数只能显示的调用
explicit Printer(const T& param):t(param){}
//成员函数模板
template<typename U>
void add_and_print(const U& u);
private:
T t;
};
//注意这里要有两层template的说明
template<typename T>
template<typename U>
void Printer<T>::add_and_print(const U& u) {
cout << t + u << endl;
}
Printer<int> p(42);
p.add_and_print(1.1); //自动推断U为double,打印出43.1
类模板Tips
类模板中的static成员
类模板中可以声明static成员,在类外定义的时候要增加template相关的关键词。另外,需要注意的是:每个不同的模板实例都会有一个独有的static成员对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16template<typename T>
class Printer {
public:
explicit Printer(const T& param):t(param){}
static int s_value;
private:
T t;
};
template<typename T> //注意这里的定义方式
int Printer<T>::s_value = 1;
Printer<int> pi(1);
Printer<int> pi2(1);
Printer<double> pd(1.0);
pi.s_value += 1; //pi和pi2中的改变了,pd的没改变其实这个结论是显然的,static成员属于实例化后的类,不同的实例化当然有不同static成员。就像上面的例子一样,pi.s_value += 1只影响到了Printer
,而不会影响到Printer 。
1 | 函数模板中的static局部变量也有类似的工作方式。 |
类模板成员函数实例化
- 为了节省资源,类模板实例化时并不是每个成员函数都实例化了,而是使用到了哪个成员函数,那个成员函数才实例化。
1 | template<typename T> |
- 虽然成员函数print无法通过编译,但是因为没有使用到,也就没有实例化print,所以没有触发编译错误。
类模板别名
- 为了简化代码,我们可以使用typedef为类模板的某个实例定义一个别名,也可以使用using语句固定一个或多个类型参数(这有点偏特化的意思了)。
1
2
3
4
5
6typedef std::pair<int, int> PairOfInt; //ok,为std::pair<int, int>定义了一个别名
template <typename T> using WithNum = std::pair<T, int>; //ok,相当于定义了一个std::pair的偏特化
PairOfInt poi; //实际类型,std::pair<int, int>
WithNum<std::string> strs; //实际类型,std::pair<string, int>
WithNum<int> ints; //实际类型,std::pair<int, int>
特化与偏特化
类模板的特化与偏特化
- 就像函数模板重载那样,你可以通过特化(偏特化)类模板来为特定的类型指定你想要的行为。类模板的特化(偏特化)只需要模板名称相同并且特化列表<>中的参数个数与原始模板对应上即可,模板参数列表不必与原始模板相同模板名称相同。一个类模板可以有多个特化,与函数模板相同,编译器会自动实例化那个最特殊的版本。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44template<typename T> //基本模板
class S {
public:
void info() {
printf("In base template\n");
}
};
template<> //特化
class S<int> {
public:
void info() {
printf("In int specialization\n");
}
};
template<typename T> //偏特化
class S<T*> {
public:
void info() {
printf("In pointer specialization\n");
}
};
template<typename T, typename U> //另外一个偏特化
class S<T(U)> {
public:
void info() {
printf("In function specialization\n");
}
};
int func(int i) {
return 2 * i;
}
S<float> s1;
s1.info(); //调用base模板
S<int> s2;
s2.info(); //调用int特化版本
S<float*> s3;
s3.info(); //调用T*特化版本
S<decltype(func)> s4;
s4.info(); //调用函数特化版本 - 提供了所有类型实参的特化是完全特化,只提供了部分类型实参或者T的类型受限(例如:T)的特化被认为是不完整的,所以也被称为偏特化。完全特化的结果是一个实际的class,而偏特化的结果是另外一个同名的模板。*
类模板成员特化
- 除了可以特化类模板之外,还可以对类模板中的成员函数和普通静态成员变量进行特化。
1 | template<typename T> |