在本章节中,我们将简要探讨针对具体的工程,在编写模板代码时会遇到什么问题。在这里我们必须接触到C++编译器的工作原理,以便于我们能对它抛出的错误做出合理的判断。

C++编译器两个最重要的组件是编译器链接器 ,几乎所有的C++编译器都包含这两个组件,前者负责将.cpp文件编译为.obj文件(实际工作流程为**.cpp->经预处理器-E->.i->经编译器-S->.s->经汇编器-c->.o(.obj)**),然后后者将所有.o文件重定位生成可执行程序(windows系统下为.exe,在linux下默认没有扩展名)。

包含模型简介

编译期错误

一般我们采用如下形式编写非模板代码:

  1. 类,结构体之类的类型全部放在头文件中,通常这类文件的扩展名为.h(或.hpp,.hxx,.H,.hh)。

  2. 对于全局函数(非内联)和全局变量(非内联),只将它们的声明放入头文件中,定义则放在一个会编译为其自身编译单元的同名文件中,这类文件的扩展名通常为.cpp(.C,.c,.cc,.cxx)。

这样做既可以很清晰地找到各种类及函数的声明与定义,也避免了链接过程中由于多重定义引起的错误。

但这种方法遇到模板时却出现了问题。我们有如下文件代码:

myfirst.hpp

1
2
3
4
5
6
7
#ifndef _MYFIRST_HPP_
#define _MYFIRST_HPP_

template <typename T>
void printTypeof(T const &);

#endif // !_MYFIRST_HPP_

myfirst.cpp

1
2
3
4
5
6
7
8
9
10
#include "myfirst.hpp"

#include <iostream>
#include <typeinfo>

template <typename T>
void printTypeof(T const &obj)
{
std::cout << typeid(obj).name();
}

main.cpp

1
2
3
4
5
6
7
8
9
10
11
#include "myfirst.hpp"

#include <iostream>
#include <string>

int main()
{
std::string s;
printTypeof(s);
return 0;
}

运行时,编译器报错,并抛出printTypeof()未定义的错误信息,这是一个链接器错误。

这是由于编译器将.cpp文件编译为.o文件的过程是相对独立的,编译器事先不知道什么类型的模板函数应该被实例化,于是就没有任何版本的实例化代码,导致链接器无法建立对特定实例的printTypeof()函数的引用。

头文件中的模板

解决上述问题的最常见的方法是使用类似于宏和内联函数相似的方法:在声明模板的头文件中包含模板定义。也就是说,将.cpp文件中的模板定义写进.h文件中,这种组织模板的方式称为**包含模型**。

值得注意的是,这种方法大大增加了包含头文件的成本:预编译时,必须将所有#预编译宏展开,对于我们自己写的代码,这点代价其实无足轻重,但是要记住,.h文件中包含你的模板定义,因此在它之前,有你需要的标准库头文件(此例中如#include <iostream>#include <typeinfo>),这两个头文件中可能包含数万行代码,它大大增加了编译复杂程序所需的时间。我们也丧失了一个很高效的修改代码的方式:如果修改了一个.cpp文件,那么只需要将它自己重新编译再链接,可节省大量编译时间。如果使用包含模型,那么必须重新编译整个main.cpp文件。

目前存在的其他方法包括预编译头文件使用模板显式实例化。尽管存在这个编译时长的问题,但在更好用的机制之前,我们强烈建议尽可能遵守这个方法来解决目前的问题。

C++20中,引入了一种全新的机制:模块(module),它能够使编译器分别编译所有声明,并在使用时有需要的引入。此后我们将使用单独的一章详细探讨它的机制。

还有一个隐藏的一个问题是,非内联函数模板相比内联函数模板和宏有一个本质的区别:它不会在调用处展开,而是创建一个实例化函数副本。如果我们在两个不同的文件中使用了该模板的相同的实例化,那么会创建两个相同的实例化函数,会出现重定义行为。在大部分情况下,C++编译器可以自行解决这个问题,之后我们将探讨实例化方案和C++编译系统文档来仔细研究这个问题。

模板和inline

我们可能了解到inline限定符是内联标志,提示编译器在使用该函数时在调用处直接展开而不发生函数调用。然而我们需要忘掉这个说法,编译器会忽略掉inline带来的强制性的在调用处展开的特性,只保留它的一个附加特性:允许一个定义在程序中多次出现

事实上,编译器对调用的函数是否内联能做出做出比程序员更优的选择,所以对某个函数是否内联完全取决于编译器,我们也可以在编译时添加编译器特定属性来禁用内联,如-fno-inline-fno-inline-functions,前者禁用所有编译器内联,后者只禁用用户定义的内联代码。

预编译头文件

预编译头文件(PCH)并未在标准库中提供,它通常是取决于供应商的选择。

如果目前有若干个文件,它们开始部分都用内容相同的$N$行预编译头文件,那我们可以取出所有的头文件,包含在一个std.hpp中:

借用万能头中的文件

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
#ifndef _STD_HPP_
#define _STD_HPP_
#include <cassert>
#endif
#include <cctype>
#include <cfloat>
#include <ciso646>
#include <climits>
#include <csetjmp>
#include <cstdarg>
#include <cstddef>
#include <cstdlib>

#if __cplusplus >= 201103L
#include <cstdint>
#endif

// C++
// #include <bitset>
// #include <complex>
#include <algorithm>
#include <bitset>
#include <functional>
#include <iterator>
#include <limits>
#include <memory>
#include <new>
#include <numeric>
#include <typeinfo>
#include <utility>

#if __cplusplus >= 201103L
#include <array>
#include <atomic>
#include <initializer_list>
#include <ratio>
#include <scoped_allocator>
#include <tuple>
#include <typeindex>
#include <type_traits>
#endif

#if __cplusplus >= 201402L
#endif

#if __cplusplus >= 201703L
#include <any>
// #include <execution>
#include <optional>
#include <variant>
#include <string_view>
#endif

#if __cplusplus >= 202002L
#include <bit>
#include <compare>
#include <concepts>
#include <numbers>
#include <ranges>
#include <span>
#include <source_location>
#include <version>
#endif

#if __cplusplus > 202002L
#include <expected>
#include <stdatomic.h>
#if __cpp_impl_coroutine
# include <coroutine>
#endif
#endif

#if _GLIBCXX_HOSTED
// C
#ifndef _GLIBCXX_NO_ASSERT
#include <cassert>
#endif
#include <cctype>
#include <cerrno>
#include <cfloat>
#include <ciso646>
#include <climits>
#include <clocale>
#include <cmath>
#include <csetjmp>
#include <csignal>
#include <cstdarg>
#include <cstddef>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <ctime>
#include <cwchar>
#include <cwctype>

#if __cplusplus >= 201103L
#include <ccomplex>
#include <cfenv>
#include <cinttypes>
#include <cstdalign>
#include <cstdbool>
#include <cstdint>
#include <ctgmath>
#include <cuchar>
#endif

// C++
#include <complex>
#include <deque>
#include <exception>
#include <fstream>
#include <functional>
#include <iomanip>
#include <ios>
#include <iosfwd>
#include <iostream>
#include <istream>
#include <iterator>
#include <limits>
#include <list>
#include <locale>
#include <map>
#include <memory>
#include <new>
#include <numeric>
#include <ostream>
#include <queue>
#include <set>
#include <sstream>
#include <stack>
#include <stdexcept>
#include <streambuf>
#include <string>
#include <typeinfo>
#include <utility>
#include <valarray>
#include <vector>

#if __cplusplus >= 201103L
#include <array>
#include <atomic>
#include <chrono>
#include <codecvt>
#include <condition_variable>
#include <forward_list>
#include <future>
#include <initializer_list>
#include <mutex>
#include <random>
#include <ratio>
#include <regex>
#include <scoped_allocator>
#include <system_error>
#include <thread>
#include <tuple>
#include <typeindex>
#include <type_traits>
#include <unordered_map>
#include <unordered_set>
#endif

#if __cplusplus >= 201402L
#include <shared_mutex>
#endif

#if __cplusplus >= 201703L
#include <any>
#include <charconv>
// #include <execution>
#include <filesystem>
#include <optional>
#include <memory_resource>
#include <variant>
#endif

#if __cplusplus >= 202002L
#include <barrier>
#include <bit>
#include <compare>
#include <concepts>
#include <format>
#include <latch>
#include <numbers>
#include <ranges>
#include <span>
#include <stop_token>
#include <semaphore>
#include <source_location>
#include <syncstream>
#include <version>
#endif

#if __cplusplus > 202002L
#include <expected>
#include <generator>
#include <print>
#include <spanstream>
#if __has_include(<stacktrace>)
# include <stacktrace>
#endif
#include <stdatomic.h>
#include <stdfloat>
#endif

#if __cplusplus > 202302L
#include <text_encoding>
#endif
#endif // !_STD_HPP_

如果我们包含一次该文件,那么它会消耗大量的编译时间,但如果我们重复多次使用,它也只会被包含一次。因此这对于头文件重复多次包含的情况很有效。