在本章节中,我们将简要探讨针对具体的工程,在编写模板代码时会遇到什么问题。在这里我们必须接触到C++编译器的工作原理,以便于我们能对它抛出的错误做出合理的判断。
C++编译器两个最重要的组件是编译器和链接器 ,几乎所有的C++编译器都包含这两个组件,前者负责将.cpp文件编译为.obj文件(实际工作流程为**.cpp->经预处理器-E->.i->经编译器-S->.s->经汇编器-c->.o(.obj)**),然后后者将所有.o文件重定位生成可执行程序(windows系统下为.exe,在linux下默认没有扩展名)。
包含模型简介
编译期错误
一般我们采用如下形式编写非模板代码:
类,结构体之类的类型全部放在头文件中,通常这类文件的扩展名为.h(或.hpp,.hxx,.H,.hh)。
对于全局函数(非内联)和全局变量(非内联),只将它们的声明放入头文件中,定义则放在一个会编译为其自身编译单元的同名文件中,这类文件的扩展名通常为.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.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
#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 <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
#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
#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 <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
|
如果我们包含一次该文件,那么它会消耗大量的编译时间,但如果我们重复多次使用,它也只会被包含一次。因此这对于头文件重复多次包含的情况很有效。