Skip to content

Commit 5dc6e23

Browse files
committed
Updated to Chapter 11, Section 1
1 parent e97faa1 commit 5dc6e23

File tree

11 files changed

+270
-65
lines changed

11 files changed

+270
-65
lines changed

Structure.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -723,23 +723,23 @@
723723

724724
我们可以忽略它的具体类型,用一套普适的方法来处理各种类型的数据。
725725

726-
#### 编程示例:`swap`函数模版
726+
#### 如何使用函数模版
727727

728728
`template`允许接收的参数可以是类型信息或数据信息。
729729

730-
#### 如何使用函数模版
731-
732730
注意:模版参数必须都是在编译时确定的。
733731

734-
可以用`constexpr`
732+
#### `constexpr`的使用
735733

736-
### 函数模版的实例化与特化
734+
### 函数模版的使用
737735

738736
#### 实例化
739737

740738
显式实例化和隐式实例化。
741739

742-
需要指出的是,函数模版不是一个预先给定的函数。编译器根据需要,会根据函数模版生成若干个对应的函数定义。
740+
#### 函数模版的重载
741+
742+
`swap`函数为例,我们可以用它来交换两个`T[]`数组的内容。这是一种重载。
743743

744744
#### 特化
745745

@@ -763,7 +763,7 @@
763763

764764
与函数模版相似,类模版的参数也是要在编译时确定的。
765765

766-
### 类模版的实例化与特化
766+
### 类模版的使用
767767

768768
#### 实例化
769769

code_in_book/11.1/swap.h

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
#pragma once
2+
#include <cstddef> //std::size_t定义在cstddef库中;不过其它库中亦有
3+
//本代码适合C++11以前标准,即通过拷贝构造/赋值来实现交换
4+
//C++11以后应使用移动构造/赋值实现交换
5+
namespace user { //定义在user命名空间中
6+
template<typename T>
7+
void swap(T &a, T &b) {
8+
T tmp {a};
9+
a = b;
10+
b = tmp;
11+
}
12+
template<typename T, std::size_t N> //模版参数N表示这个数组的长度
13+
void swap(T (&a)[N], T (&b)[N]) { //参数是两个T(&)[N]
14+
for (int i = 0; i < N; i++)
15+
user::swap(a[i], b[i]); //调用已经定义好的user::swap
16+
}
17+
};

generalized_parts/11_templates_and_fundamental_generic_programming.tex

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
\chapter{模版与泛型编程基础}
2+
{\kaishu \large ``泛型编程是关于算法和数据结构的抽象和分类。它的灵感来自于高德纳,而不是类型理论。它的目标是系统地、增量地构建有用、高效和抽象的算法与数据结构。''\footnote{原文:Generic programming is about abstracting and classifying algorithms and data structures. It gets its inspiration from Knuth and not from type theory. Its goal is the incremental construction of systematic catalogs of useful, efficient and abstract algorithms and data structures.}}
3+
\begin{flushright}——亚历山大·斯捷潘若夫\end{flushright}\par
24
本章将介绍C++的两个重要部分:\textbf{泛型编程(Generic programming)}与\textbf{标准模版库(Standard template library, STL)}。其中STL部分不会细讲,留到精讲篇再详细阐述。\par
35
我们在第四章中已简要了解过函数模版,又在之后的章节中反复用到 \lstinline@std::vector@ 等类模版。在本章,我将带领读者从函数模版到类模版,系统性地学习泛型编程的基本知识,并在这之后完成一个``指能指针''的实操练习。\par
46
而在本章的末尾,我会带读者了解一些STL的基本知识——尤其是迭代器。它是指针的延伸,但其作用远比指针更加丰富。\par
5-
\import{11_templates_and_fundamental_generic_programming/}{01_function_templates.tex}
7+
\import{11_templates_and_fundamental_generic_programming/}{01_function_templates_and_constexpr.tex}
68
\import{11_templates_and_fundamental_generic_programming/}{02_instantiation_and_specialization_of_function_templates.tex}
79
\import{11_templates_and_fundamental_generic_programming/}{03_class_templates.tex}
810
\import{11_templates_and_fundamental_generic_programming/}{04_instantiation_and_specialization_of_class_templates.tex}

generalized_parts/11_templates_and_fundamental_generic_programming/01_function_templates.tex

Whitespace-only changes.
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
\section{函数模版与\texttt{constexpr}}
2+
\subsection*{什么是泛型?}
3+
在尚未接触泛型的时候,我们为了完成某个函数/类,必须指明一种欲处理的数据类型。比如要写一个 \lstinline@max@ 函数,就要写一个 \lstinline@double@ 版本的;或者要写一个 \lstinline@valarray@ 数组,就要写一个 \lstinline@int@ 版本的。但是,作为一套通用的操作,它应该能处理不同类型的数据。比如说 \lstinline@max@ 函数,它应当可以处理各种基本数据类型的值才对。为此我们能做的就是重载。\par
4+
而类是不允许重载的。以 \lstinline@valarray@ 为例,无论 \lstinline@int@ 数据,还是 \lstinline@double@ 数据,还是 \lstinline@long long@ 数据,它们都是按照相同方式来存储的。如果我们希望有一个能处理 \lstinline@double@ 数据的版本,那么很不幸,我们必须把 \lstinline@valarri@ 的代码抄一遍,把其中的许多 \lstinline@int@ 改成 \lstinline@double@\footnote{未必全部 \lstinline@int@ 都需要改成 \lstinline@double@,因为有些 \lstinline@int@ 值可能只是用来作循环结构变量的。虽然我们统一使用了 \lstinline@std::size_t@ 类型,但是难免会因为习惯的缘故而不小心写出个别的 \lstinline@int@ 来。},然后再改一个类名,比如用 \lstinline@valarrd@。\par
5+
但无论从哪个角度上讲,这样不停地和类型打交道也太麻烦了点。泛型编程能让我们从繁琐的类型问题中解放出来,我们可以更多地关注这个函数/类具体做了什么,而不必在于类型。我们可以用一个抽象的类型名称 \lstinline@T@(或者你自定义的别的名称)来代表任何一种类型。
6+
\begin{lstlisting}
7+
\template<typename T>
8+
T max(T a, T b) {
9+
return a > b ? a : b;
10+
}
11+
\end{lstlisting}
12+
这样一来,当我们在代码当中调用 \lstinline@max(int,int)@ 时,编译器就会察觉到这个需求,并为我们生成一个 \lstinline@int max(int,int)@ 函数来。当我们在代码当中调用 \lstinline@max(double,doule)@ 时,编译器也会察觉到这个需求,并为我们生成一个 \lstinline@double max(double,double)@ 函数来。\par
13+
定义一个函数模版的基本语法是
14+
\begin{lstlisting}
15+
template<[模版参数列表]>
16+
[返回类型] [函数名]([函数参数列表]) {
17+
[函数体]
18+
}
19+
\end{lstlisting}
20+
\lstinline@template@ 块内的部分称为模版参数。模版参数可以是 \lstinline@typename@/\lstinline@class@ 关键字引出的类名\footnote{\lstinline@typename@ 和 \lstinline@class@ 关键字在这方面的作用相同,没有任何区别。但从习惯上讲,我们还是倾向于使用 \lstinline@typename@。},也可以是任何一个类的数据。
21+
至于其它的部分,都和一般的函数没什么差别。只不过我们可以使用模版参数来指代一个``待定''的类名或者数据。
22+
\begin{lstlisting}
23+
template<typename T> //T是模版参数,作为待定的类名
24+
void swap(T &a, T &b) { //交换a和b的值
25+
T tmp {a};
26+
a = b;
27+
b = tmp;
28+
}
29+
\end{lstlisting}
30+
在这里,\lstinline@T@ 就是一个待定的类名。编译器会根据我们在代码中的实际需要,生成\lstinline@T=int@ 的版本、\lstinline@T=double@ 的版本、\lstinline@T=char@ 的版本等等。我们也可以为模版参数设定默认值,不过对于大多数函数模版来说,默认值是不怎么需要的东西。\par
31+
我们当然也可以交换两个指针的值。不过请读者注意,这时我们交换的就是指针的指向(也即,指针存储的地址值)而非内容了。比如,这样调用:
32+
\begin{lstlisting}
33+
int *p1 {new int [10]}, *p2 {new int[5]};
34+
swap(p1, p2); //调用自定义的swap(T&,T&)
35+
\end{lstlisting}
36+
这样做会让 \lstinline@p1@ 与 \lstinline@p2@ 调换指向。\par
37+
\subsection*{数据模版参数}
38+
不过如果我们想要调换两个数组,问题就出现了:
39+
\begin{lstlisting}
40+
void swap(T &a, T &b){
41+
T tmp {a};
42+
//error: invalid conversion from 'int*' to 'int' [-fpermissive]
43+
a = b;
44+
//error: invalid array assignment
45+
b = tmp;
46+
//error: invalid array assignment
47+
}
48+
int main() {
49+
int a[3] {1,2,3}, b[3] {4,5,6};
50+
swap(a, b);
51+
std::cout << a[0];
52+
}
53+
\end{lstlisting}
54+
我来解释一下出现这个错误的原因:\par
55+
首先我们来看 \lstinline@main@ 函数。在这里,我们调用了 \lstinline@swap(a,b)@。因为 \lstinline@a@ 和 \lstinline@b@ 都是 \lstinline@int[3]@ 类型的,所以编译器会创建一个 \lstinline@T=int[3]@ 版本的 \lstinline@swap@ 函数,你可以理解成这样:
56+
\begin{lstlisting}
57+
void swap(int (&a)[3], int (&b)[3]) { //int(&)[3]意味着“对int[3]”数组的引用
58+
int tmp[3] {a};
59+
a = b;
60+
b = tmp;
61+
}
62+
\end{lstlisting}
63+
第一则报错信息的含义是:``\lstinline@int*@ 到 \lstinline@int@ 的类型转换是禁止的。''这是因为编译器把 \lstinline@int tmp[3] {a}@ 当作了数组的列表初始化来处理。在这个过程中 \lstinline@a@ 发生了数组到指针的隐式类型转换。然而,列表初始化所期待的是 \lstinline@int@ 类型,但 \lstinline@int*@ 类型却不能直接转换为 \lstinline@int@ 类型,所以编译器会有此报错。\par
64+
第二、三则报错信息说的是一回事。简单来说就是 \lstinline@int[3]@ 这样的数组不能直接赋值——其实这很好理解,因为我们在写 \lstinline@valarri@ 的时候也不是直接用数组赋值的方式来改写赋值运算符的啊;都是通过循环结构,一个个元素赋值的。\par
65+
总之问题的根源就出在数组类型上,编译器自动把 \lstinline@T@ 识别成了 \lstinline@int[3]@ 并生成了这个版本的函数,但它没有类似于拷贝构造的机制,也不允许使用赋值运算符来直接赋值,所以我们才遇到了这样的困难。\par
66+
解决方法就是写一个函数重载,专门处理数组类型,像这样:
67+
\begin{lstlisting}
68+
template<typename T>
69+
void swap(T& a, T& b) {
70+
T tmp {a};
71+
a = b;
72+
b = tmp;
73+
}
74+
void swap(int (&a)[3], int (&b)[3]) {
75+
int tmp[3];
76+
for (int i = 0; i < 3; i++) {
77+
tmp[i] = a[i];
78+
a[i] = b[i];
79+
b[i] = tmp[i];
80+
}
81+
}
82+
int main() {
83+
int a[3] {1,2,3}, b[3] {4,5,6};
84+
swap(a, b);
85+
std::cout << a[0];
86+
}
87+
\end{lstlisting}
88+
这个 \lstinline@swap(int(&)[3],int(&)[3])@ 函数没有 \lstinline@template@ 关键字,这意味着它不是模版函数——或者说,它是非模版函数(Non-template function)。\par
89+
不过我们发现这个非模版函数的适用范围还是太窄了。它只能接收 \lstinline@int@ 数组的,而且还只能是 \lstinline@3@ 长度的数组。如果我们需要处理一个 \lstinline@double@ 数组呢?必须重载另一个非模版函数 \lstinline@void swap(double(&)[3],double(&)[3])@——那么我们又陷入到类型的麻烦之中了。\par
90+
所以我们还是需要使用模版来把类型抽象起来,写成这样:
91+
\begin{lstlisting}
92+
template<typename T>
93+
void swap(T (&a)[3], T (&b)[3]) { //把类型抽象起来,变成T[3]
94+
T tmp[3];
95+
for (int i = 0; i < 3; i++) {
96+
tmp[i] = a[i];
97+
a[i] = b[i];
98+
b[i] = tmp[i];
99+
}
100+
}
101+
\end{lstlisting}
102+
但是这样还不够。我们只是有了 \lstinline@T[3]@ 的版本,那么 \lstinline@T[4]@ 怎么办,\lstinline@T[5]@ 乃至其它的怎么办?所以我们还需要把数组长度这则信息抽象起来,变成一个模版参数 \lstinline@N@,其类型为 \lstinline@std::size_t@。
103+
\begin{lstlisting}
104+
template<typename T, std::size_t N> //模版参数N表示这个数组的长度
105+
void swap(T (&a)[N], T (&b)[N]) { //参数是两个T(&)[N]
106+
T tmp[N];
107+
for (int i = 0; i < N; i++) {
108+
tmp[i] = a[i];
109+
a[i] = b[i];
110+
b[i] = tmp[i];
111+
}
112+
}
113+
\end{lstlisting}
114+
这样我们就完成了一个针对一般数据类型的 \lstinline@swap@ 函数模版和一个针对一维数组的 \lstinline@swap@ 函数模版。\par
115+
\subsection*{函数模版中的代码重用}
116+
不知读者有没有想过,二维数组乃至更高维数组要怎么办?
117+
\begin{lstlisting}
118+
int a[2][3] {}, b[2][3] {{1,2,3},{4,5,6}};
119+
swap(a, b); //相同的问题再次出现
120+
\end{lstlisting}\par
121+
我们想,\lstinline@a@ 和 \lstinline@b@ 都是 \lstinline@int[2][3]@ 类型的。在调用 \lstinline@swap@ 时,编译器会选中 \lstinline@swap(T(&)[N],T(&)[N])@\footnote{对于函数重载/模版来说,C++规定了一套很复杂的函数选取过程(重载决议)。总之结果是选中了它。}来调用,此时应有 \lstinline@T=int[3]@, \lstinline@N=2@。问题来了:\lstinline@int[3]@ 类型的 \lstinline@a[i]@ 和 \lstinline@b[i]@ 仍然不支持直接赋值,那么我们岂不是还要再写一个二维数组的版本咯?\par
122+
其实不必,因为我们有更好的实现方法:
123+
\lstinputlisting[caption=\texttt{swap.h}]{code_in_book/11.1/swap.h}
124+
这样就够了吗?没错,这样就够了。在实现数组交换的时候,我们对每个数组对应位置的元素调用 \lstinline@user::swap@ 函数来进行交换。试想,当我们传入两个 \lstinline@int[3]@ 参数的时候,编译器会根据我们的需要,生成一个 \lstinline@T=int@, \lstinline@N=3@ 版本的 \lstinline@user::swap@ 函数;这个函数又需要调用一个 \lstinline@T=int@ 版本的 \lstinline@user::swap@ 函数(属于另一个函数模版),所以编译器也会生成之。\par
125+
而当传入两个 \lstinline@int[2][3]@ 参数的时候,会发生什么呢?首先,编译器生成一个 \lstinline@T=int[3]@, \lstinline@N=2@ 版本的函数,而这个函数需要调用一个 \lstinline@T=int@, \lstinline@N=3@ 版本的函数,所以编译器也会生成之。同上,这个函数又会调用 \lstinline@T=int@ 版本的函数,编译器也会生成之。\par
126+
总而言之,通过这样的写法,我们就保证了:只要能交换一维数组,就能交换二维数组;只要能交换二维数组,就能交换三维数组……以至任意高维的数组。有点像数学归纳法。这也是代码重用的魅力。\par
127+
\subsection*{\texttt{constexpr}的使用}
128+
通过上述内容,读者应该发现,函数模版不是一个函数,而是一套方案。编译器会根据这套方案,按照我们在代码中调用函数的情况——也就是实际需求,来生成相应的函数。这个过程自始至终完成在编译期,所以它们全都是编译时行为。\par
129+
编译时行为就意味着,我们不能把变量等运行时才能确定的信息作为模版参数。
130+
\begin{lstlisting}
131+
int n;
132+
std::cin >> n;
133+
int a[n] {}, b[n] {}; //部分编译器支持这种语法,但它是不标准的
134+
user::swap<int,n>(a, b); //不可以用变量作为模版参数
135+
//error: the value of 'n' is not usable in a constant expression
136+
\end{lstlisting}
137+
对于模版参数来说,它们只能接收类型名和常量表达式作为模版参数,所以我们要么传入字面量,要么传入常量表达式,总之不能传入在运行时才能确定的量。\par
138+
对于一般情况来说,把字面量作为模版参数就是最好的选择了;如果出于统一的目的,可以用 \lstinline@constexpr@ 数据。\par
139+
不过有些时候我们难免要用到一点复杂的常量表达式,如果还要写成字面量的话就不太方便了——比如说,\lstinline@48@ 和 \lstinline@32@ 的最小公倍数。我们必须要人工把这个结果算出来,然后再把它写成一个 \lstinline@constexpr@ 数据。更可怕的是,如果我们需要很多组这样的最小公倍数,我们需要进行大量的人工计算——其实我们可以自己写一个函数,然而这些函数只能在运行时进行计算,但我们在编译时就需要这个值,那就不行了。\par
140+
从C++11起,我们可以用 \lstinline@constexpr@ 说明符来允许函数在编译期进行求值。简单点说,如果它的所有实参都是常量表达式,那么它将在编译期进行计算\footnote{实际的条件非常复杂,但是不会本书不打算在这里深究。另外,这个编译期计算的计算量也是有限制的,如果计算量过大,编译器将报错。};否则,它将在运行期进行计算,一切如常。以下是几个例子:
141+
\begin{lstlisting}
142+
constexpr unsigned long long factorial(unsigned long long n) { //阶乘
143+
//如果提供的实参是常量表达式,这个函数的值将在编译期求得
144+
return n ? n * factorial(n - 1) : 1;
145+
}
146+
constexpr unsigned long long fibonacci(unsigned long long N) { //斐波那契数
147+
if (N == 0)
148+
return 0;
149+
if (N == 1)
150+
return 1;
151+
return fibonacci(N - 1) + fibonacci(N - 2);
152+
}
153+
constexpr unsigned gcd(unsigned a, unsigned b) { //最大公因数
154+
return b ? gcd(b, a % b) : a; //欧几里得算法,细节就先不追究了
155+
}
156+
constexpr unsigned lcm(unsigned a, unsigned b) { //最小公倍数
157+
return a / gcd(a, b) * b; //只要a,b和gcd都是常量表达式,lcm也能在编译期求得
158+
}
159+
\end{lstlisting}
160+
如果我们提供了合适的常量表达式作为实参,那么这些结果都可以在编译期求得。
161+
\begin{lstlisting}
162+
constexpr unsigned long long F = factorial(10); //factorial(10)在编译期调用
163+
std::cout << F << '\n';
164+
int n {5};
165+
std::cout << factorial(n); //实参n不是常量表达式,这个函数将在运行期调用
166+
\end{lstlisting}\par
167+
这样一来,我们可以直接把 \lstinline@constexpr@ 函数的返回值当作模版参数了(前提是这个函数能在编译期计算)。\par

0 commit comments

Comments
 (0)