├── .gitignore ├── C++-Templates-The-Complete-Guide.tex ├── LICENSE ├── README.md ├── content ├── 1 │ ├── Section.tex │ ├── chapter1 │ │ ├── 0.tex │ │ ├── 1.tex │ │ ├── 2.tex │ │ ├── 3.tex │ │ ├── 4.tex │ │ ├── 5.tex │ │ ├── 6.tex │ │ └── 7.tex │ ├── chapter10 │ │ ├── 0.tex │ │ ├── 1.tex │ │ ├── 2.tex │ │ ├── 3.tex │ │ ├── 4.tex │ │ ├── 5.tex │ │ └── 6.tex │ ├── chapter11 │ │ ├── 0.tex │ │ ├── 1.tex │ │ ├── 2.tex │ │ ├── 3.tex │ │ ├── 4.tex │ │ ├── 5.tex │ │ ├── 6.tex │ │ └── 7.tex │ ├── chapter2 │ │ ├── 0.tex │ │ ├── 1.tex │ │ ├── 10.tex │ │ ├── 11.tex │ │ ├── 2.tex │ │ ├── 3.tex │ │ ├── 4.tex │ │ ├── 5.tex │ │ ├── 6.tex │ │ ├── 7.tex │ │ ├── 8.tex │ │ └── 9.tex │ ├── chapter3 │ │ ├── 0.tex │ │ ├── 1.tex │ │ ├── 2.tex │ │ ├── 3.tex │ │ ├── 4.tex │ │ └── 5.tex │ ├── chapter4 │ │ ├── 0.tex │ │ ├── 1.tex │ │ ├── 2.tex │ │ ├── 3.tex │ │ ├── 4.tex │ │ └── 5.tex │ ├── chapter5 │ │ ├── 0.tex │ │ ├── 1.tex │ │ ├── 2.tex │ │ ├── 3.tex │ │ ├── 4.tex │ │ ├── 5.tex │ │ ├── 6.tex │ │ ├── 7.tex │ │ └── 8.tex │ ├── chapter6 │ │ ├── 0.tex │ │ ├── 1.tex │ │ ├── 2.tex │ │ ├── 3.tex │ │ ├── 4.tex │ │ ├── 5.tex │ │ └── 6.tex │ ├── chapter7 │ │ ├── 0.tex │ │ ├── 1.tex │ │ ├── 2.tex │ │ ├── 3.tex │ │ ├── 4.tex │ │ ├── 5.tex │ │ ├── 6.tex │ │ └── 7.tex │ ├── chapter8 │ │ ├── 0.tex │ │ ├── 1.tex │ │ ├── 2.tex │ │ ├── 3.tex │ │ ├── 4.tex │ │ ├── 5.tex │ │ └── 6.tex │ └── chapter9 │ │ ├── 0.tex │ │ ├── 1.tex │ │ ├── 2.tex │ │ ├── 3.tex │ │ ├── 4.tex │ │ ├── 5.tex │ │ └── 6.tex ├── 2 │ ├── Section.tex │ ├── chapter12 │ │ ├── 0.tex │ │ ├── 1.tex │ │ ├── 2.tex │ │ ├── 3.tex │ │ ├── 4.tex │ │ ├── 5.tex │ │ └── 6.tex │ ├── chapter13 │ │ ├── 0.tex │ │ ├── 1.tex │ │ ├── 2.tex │ │ ├── 3.tex │ │ ├── 4.tex │ │ ├── 5.tex │ │ └── images │ │ │ └── 1.png │ ├── chapter14 │ │ ├── 0.tex │ │ ├── 1.tex │ │ ├── 2.tex │ │ ├── 3.tex │ │ ├── 4.tex │ │ ├── 5.tex │ │ ├── 6.tex │ │ ├── 7.tex │ │ └── 8.tex │ ├── chapter15 │ │ ├── 0.tex │ │ ├── 1.tex │ │ ├── 10.tex │ │ ├── 11.tex │ │ ├── 12.tex │ │ ├── 13.tex │ │ ├── 2.tex │ │ ├── 3.tex │ │ ├── 4.tex │ │ ├── 5.tex │ │ ├── 6.tex │ │ ├── 7.tex │ │ ├── 8.tex │ │ ├── 9.tex │ │ └── images │ │ │ └── 1.png │ ├── chapter16 │ │ ├── 0.tex │ │ ├── 1.tex │ │ ├── 2.tex │ │ ├── 3.tex │ │ ├── 4.tex │ │ ├── 5.tex │ │ └── 6.tex │ └── chapter17 │ │ ├── 0.tex │ │ ├── 1.tex │ │ ├── 10.tex │ │ ├── 11.tex │ │ ├── 2.tex │ │ ├── 3.tex │ │ ├── 4.tex │ │ ├── 5.tex │ │ ├── 6.tex │ │ ├── 7.tex │ │ ├── 8.tex │ │ └── 9.tex ├── 3 │ ├── Section.tex │ ├── chapter18 │ │ ├── 0.tex │ │ ├── 1.tex │ │ ├── 2.tex │ │ ├── 3.tex │ │ ├── 4.tex │ │ ├── 5.tex │ │ ├── 6.tex │ │ ├── 7.tex │ │ └── images │ │ │ ├── 1.png │ │ │ ├── 2.png │ │ │ ├── 3.png │ │ │ ├── 4.png │ │ │ └── 5.png │ ├── chapter19 │ │ ├── 0.tex │ │ ├── 1.tex │ │ ├── 10.tex │ │ ├── 11.tex │ │ ├── 2.tex │ │ ├── 3.tex │ │ ├── 4.tex │ │ ├── 5.tex │ │ ├── 6.tex │ │ ├── 7.tex │ │ ├── 8.tex │ │ └── 9.tex │ ├── chapter20 │ │ ├── 0.tex │ │ ├── 1.tex │ │ ├── 2.tex │ │ ├── 3.tex │ │ ├── 4.tex │ │ ├── 5.tex │ │ ├── 6.tex │ │ └── 7.tex │ ├── chapter21 │ │ ├── 0.tex │ │ ├── 1.tex │ │ ├── 2.tex │ │ ├── 3.tex │ │ ├── 4.tex │ │ ├── 5.tex │ │ └── images │ │ │ ├── 1.png │ │ │ ├── 2.png │ │ │ ├── 3.png │ │ │ └── 4.png │ ├── chapter22 │ │ ├── 0.tex │ │ ├── 1.tex │ │ ├── 2.tex │ │ ├── 3.tex │ │ ├── 4.tex │ │ ├── 5.tex │ │ ├── 6.tex │ │ └── 7.tex │ ├── chapter23 │ │ ├── 0.tex │ │ ├── 1.tex │ │ ├── 2.tex │ │ ├── 3.tex │ │ ├── 4.tex │ │ ├── 5.tex │ │ ├── 6.tex │ │ └── 7.tex │ ├── chapter24 │ │ ├── 0.tex │ │ ├── 1.tex │ │ ├── 2.tex │ │ ├── 3.tex │ │ ├── 4.tex │ │ ├── 5.tex │ │ └── 6.tex │ ├── chapter25 │ │ ├── 0.tex │ │ ├── 1.tex │ │ ├── 2.tex │ │ ├── 3.tex │ │ ├── 4.tex │ │ ├── 5.tex │ │ ├── 6.tex │ │ └── 7.tex │ ├── chapter26 │ │ ├── 0.tex │ │ ├── 1.tex │ │ ├── 2.tex │ │ ├── 3.tex │ │ ├── 4.tex │ │ ├── 5.tex │ │ ├── 6.tex │ │ └── 7.tex │ ├── chapter27 │ │ ├── 0.tex │ │ ├── 1.tex │ │ ├── 2.tex │ │ ├── 3.tex │ │ ├── 4.tex │ │ └── images │ │ │ └── 1.png │ └── chapter28 │ │ ├── 0.tex │ │ ├── 1.tex │ │ ├── 2.tex │ │ ├── 3.tex │ │ ├── 4.tex │ │ ├── 5.tex │ │ └── 6.tex ├── About-This-Book.tex ├── Acknowledgments-for-the-First.tex ├── Acknowledgments-for-the-Second.tex ├── Appendix │ ├── A │ │ ├── 0.tex │ │ ├── 1.tex │ │ ├── 2.tex │ │ └── 3.tex │ ├── B │ │ ├── 0.tex │ │ ├── 1.tex │ │ ├── 2.tex │ │ ├── 3.tex │ │ ├── 4.tex │ │ └── images │ │ │ └── 1.png │ ├── C │ │ ├── 0.tex │ │ ├── 1.tex │ │ ├── 2.tex │ │ └── 3.tex │ ├── D │ │ ├── 0.tex │ │ ├── 1.tex │ │ ├── 2.tex │ │ ├── 3.tex │ │ ├── 4.tex │ │ ├── 5.tex │ │ ├── 6.tex │ │ ├── 7.tex │ │ └── images │ │ │ └── 1.png │ └── E │ │ ├── 0.tex │ │ ├── 1.tex │ │ ├── 2.tex │ │ ├── 3.tex │ │ └── 4.tex ├── Bibliography.tex ├── Glossary.tex └── preface.tex └── cover.jpg /.gitignore: -------------------------------------------------------------------------------- 1 | *.pdf 2 | *.aux 3 | *.log 4 | *.out 5 | *.gz 6 | *toc 7 | *.listing 8 | *.synctex(busy) 9 | /CPP-Templates-2nd---master/ 10 | /cpp-templates-2nd-master/ 11 | *.zip 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # C++ Templates 2 | 3 | *Second Edition* 4 | 5 | 6 | 7 | * 作者:David Vandevoorde,Nicolai M. Josuttis,Douglas Gregor 8 | * 译者:陈晓伟 9 | * 首次发布时间:2017年9月8日([来源](https://www.amazon.com/C-Templates-Complete-Guide-2nd/dp/0321714121)) 10 | 11 | > 翻译是译者用自己的思想,换一种语言,对原作者想法的重新阐释。鉴于我的学识所限,误解和错译在所难免。如果你能买到本书的原版,且有能力阅读英文,请直接去读原文。因为与之相较,我的译文可能根本不值得一读。 12 | > 13 | >

— 云风,程序员修炼之道第2版译者

14 | 15 | PDF可在本库的[Release页面](https://github.com/xiaoweiChen/Cpp-Templates-2nd/releases)获取。 16 | 17 | ## 本书概述 18 | 19 | 模板是C++中一个强大的特性,但对模板的误解,并未随着C++语言和开发社区的发展而消弭,从而无法使模板无法发挥其全力。本书的三位作者,同时作为C++专家,展示了如何使用现代模板来构建干净、快捷、高效、容易维护的软件。 20 | 21 | 第二版对C++11、C++14和C++17标准进行了更新,对改进模板或与模板交互的特性进行了解释,包括可变参数模板、泛型Lambda、类模板参数演绎、编译时if、转发引用和用户定义文字。还深入研究了一些基本的语言概念(比如值类别),并包含了所有标准类型特征。 22 | 23 | 本书从基本概念和相关语言特征开始,其余部分作为参考。先关注语言,再是编码、高级应用程序和复杂的惯用法。过程中,示例清楚地说明了抽象概念,并演示了模板的最佳实践。 24 | 25 | #### 关键特性 26 | 27 | - 准确理解模板的行为,避免陷阱 28 | 29 | - 使用模板编写有效、灵活、可维护的软件 30 | 31 | - 掌握有效的习语和技巧 32 | 33 | - 保持性能或安全的情况下重用源码 34 | 35 | - C++标准库中的泛型编程 36 | 37 | - 预览即将发布的“概念"特性 38 | 39 | 40 | 41 | ## 适读人群 42 | 43 | 如果您是使用C++的开发人员,想要学习或复习模板,请仔细阅读第1部分。即使已经非常熟悉模板,快速浏览这一部分也有助于熟悉本书使用的编程方式和术语。该部分也涵盖了如何组织模板相关代码的内容。 44 | 45 | 可以按自己喜欢方式学习。第2部分中有模板更多的许多细节信息,也可以在第3部分中阅读实用的编码技术(并参考第2部分了解相关的语言问题)。如果阅读这本书是为了应对开发中的具体问题,那么后一种方法可能有助于问题的解决。 46 | 47 | 附录包含了许多在正文中经常提到的信息,我们也试图让其变得更有趣。 48 | 49 | 根据经验,学习新东西的最好方法是看例子。因此,可以在本书中找到大量的例子。有些只是用几行代码解释一个抽象概念,而另一些是具体应用的完整源码。后一种示例将通过注释来说明需要包含程序代码的文件。可以在这本书的网站上找到这些文件。 50 | 51 | ## 作者简介 52 | 53 | **David Vandevoorde**在20世纪80年代后期开始用C++编程。从伦斯勒理工学院获得博士学位后,成为惠普C++编译器团队的技术负责人。1999年,加入了爱迪生设计集团(EDG),该集团的C++编译器技术是业界领先的。他是C++标准委员会的活跃成员,也是comp.lang.c++新闻组的主持人(参与创办)。也是《C++ Solutions》的作者,该书是《C++ Programming Language, 3rd Edition》的配套书籍。 54 | 55 | **Nicolai M. Josuttis**因其畅销的标准书籍《The C++ Standard Library - A Tutorial and Reference》而闻名于世,是一名独立技术顾问,为电信、交通、金融和制造业设计面向对象的软件。也是C++标准委员会的活跃成员,也是System Bauhaus的合伙人,System Bauhaus是一个由面向对象系统开发专家组成的德国团体。Josuttis还写过其他几本关于面向对象编程和C++的书。 56 | 57 | **Douglas Gregor**是苹果公司的高级Swift/C++/Objective-C编译工程师,拥有伦斯勒理工学院的计算机科学博士学位,并在印第安纳大学从事博士后工作。 58 | 59 | 60 | 61 | ## 本书相关 62 | 63 | * github地址: 64 | * 译文的LaTeX 环境配置: 65 | * vscode中配置latex: 66 | * 开源示例: 67 | * 开源翻译: 68 | 69 | * 70 | * 71 | * 72 | 73 | -------------------------------------------------------------------------------- /content/1/Section.tex: -------------------------------------------------------------------------------- 1 | 2 | 3 | 介绍C++模板的基础概念和语言特性,通过函数模板和类模板讨论其目的和概念。再介绍其他的模板特性,比如:非类型模板参数,可变参模板,typename关键字和成员模板。并且,讨论如何处理移动语义,如何声明模板参数,以及如何使用泛型进行编译时编程。本节最后会对一些术语和模板在实际中的应用,给开发工程师和泛型库的开发者们提供一些建议。 4 | 5 | \begin{flushleft} 6 | \zihao{3} 为何需要模板? 7 | \end{flushleft} 8 | 9 | C++要求使用特定类型声明变量、函数和大多数其他类型的实体。但是,对于不同的类型,很多代码看起来一样。例如,算法快速排序的实现对于不同的数据结构(比如array或vector)在结构上看就是相同的,只要所包含的类型可以相互比较。 10 | 11 | 如果编程语言不支持这种泛型特性,就只有这些(糟糕的)替代方案了: 12 | 13 | \begin{enumerate} 14 | \item 15 | 对不同类型重复实现相同的算法。 16 | 17 | \item 18 | 公共基类(比如\texttt{Object}和\texttt{void*})里面实现通用算法。 19 | 20 | \item 21 | 使用预处理。 22 | \end{enumerate} 23 | 24 | 若从其它语言转投C++,可能已经使用过以上的方法了。这里来说说他们的缺点: 25 | 26 | \begin{enumerate} 27 | \item 28 | 重复实现相同算法,就是重复地造轮子!并且会犯相同的错误。为了避免犯更多的错误,不会使用复杂但高效的算法。 29 | 30 | \item 31 | 公共基类里实现统一的代码,就等于放弃了类型检查。而且,有时候某些类必须要从特殊的基类派生出来,这会增加代码维护的成本。 32 | 33 | \item 34 | 采用预处理的方式,就需要实现一些“笨拙的文本替换”,这很难兼顾作用域和类型检查,更容易引发奇怪的错误。 35 | \end{enumerate} 36 | 37 | 而模板就不会有这些问题,它就是为了一种或多种未明确定义的类型而定义的函数或者类。使用模板时,需要显式地或隐式地指定模板参数。由于模板是C++的特性,肯定会检查类型和作用域。 38 | 39 | 目前模板使用的很广,在C++标准库中,几乎所有的代码都用到了模板。标准库提供了一些针对某种特定类型的值或对象的排序算法,也提供一些数据结构(也叫容器)来维护某种特定类型的元素,对于字符串而言,“特定类型”就是“字符”。当然,这只是最基础的功能。模板还允许参数化函数或类的行为,优化代码以及参数化其他信息。这些高级特性会在后面介绍,先从简单的模板开始吧。 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /content/1/chapter1/0.tex: -------------------------------------------------------------------------------- 1 | 2 | 3 | 本章将介绍函数模板。函数模板是参数化的函数,代表一组具有相似行为的函数。 4 | 5 | 6 | -------------------------------------------------------------------------------- /content/1/chapter1/2.tex: -------------------------------------------------------------------------------- 1 | 当使用函数模板(如max())时,模板参数由传入的参数决定。如果类型T传递两个int型参数,那编译器就会认为T是int型。 2 | 3 | 然而,T可能只是类型的“一部分”。若声明max()使用常量引用: 4 | 5 | \begin{lstlisting}[style=styleCXX] 6 | template 7 | T max (T const& a, T const& b) 8 | { 9 | return b < a ? a : b; 10 | } 11 | \end{lstlisting} 12 | 13 | 并传递int,同样T会推导为int,因为函数形参匹配的是int const\&。 14 | 15 | \hspace*{\fill} \\ %插入空行 16 | \noindent 17 | \textbf{类型推导时的类型转换} 18 | 19 | 注意,自动类型转换在类型推导时有一些限制: 20 | 21 | \begin{itemize} 22 | \item 23 | 当通过引用声明参数时,简单的转换也不适用于类型推导。用同一个模板参数T声明的两个实参必须完全匹配。 24 | 25 | \item 26 | 当按值声明参数时,只支持简单的转换:忽略const或volatile的限定符,引用转换为引用的类型,原始数组或函数转换为相应的指针类型。对于使用相同模板参数T声明的两个参数,转换类型必须匹配。 27 | \end{itemize} 28 | 29 | 例如: 30 | 31 | \begin{lstlisting}[style=styleCXX] 32 | template 33 | T max (T a, T b); 34 | ... 35 | int i = 17; 36 | int const c = 42; 37 | max(i, c); // OK: T推导为int 38 | max(c, c); // OK: T推导为int 39 | int& ir = i; 40 | max(i, ir); // OK: T推导为int 41 | int arr[4]; 42 | max(&i, arr); // OK: T推导为int* 43 | \end{lstlisting} 44 | 45 | 下面就是反面案例了: 46 | 47 | \begin{lstlisting}[style=styleCXX] 48 | max(4, 7.2); // ERROR: T推导为int或double 49 | std::string s; 50 | max("hello", s); // ERROR: T推导为char const*或std::string 51 | \end{lstlisting} 52 | 53 | 有三种方法可以处理此类错误: 54 | 55 | \begin{enumerate} 56 | \item 57 | 强制转换: 58 | \begin{lstlisting}[style=styleCXX] 59 | max(static_cast(4), 7.2); // OK 60 | \end{lstlisting} 61 | 62 | \item 63 | 显式指定(或限定)T的类型: 64 | \begin{lstlisting}[style=styleCXX] 65 | max(4, 7.2); // OK 66 | \end{lstlisting} 67 | 68 | \item 69 | 指定参数可以有不同的类型。 70 | \end{enumerate} 71 | 72 | 第1.3节将进行详细的说明。第7.2节和第15章将详细讨论类型推导时的类型转换规则。 73 | 74 | \hspace*{\fill} \\ %插入空行 75 | \noindent 76 | \textbf{默认参数的类型推导} 77 | 78 | 类型推导不适用于默认调用参数。例如: 79 | 80 | \begin{lstlisting}[style=styleCXX] 81 | template 82 | void f(T = ""); 83 | ... 84 | f(1); // OK: 推导出T为int,因此其可以调用f(1) 85 | f(); // ERROR: 不能推断出T的类型 86 | \end{lstlisting} 87 | 88 | 为了支持这种情况,必须为模板形参声明一个默认实参,这将在1.4节中进行详述: 89 | 90 | \begin{lstlisting}[style=styleCXX] 91 | template 92 | void f(T = ""); 93 | ... 94 | f(); // OK 95 | \end{lstlisting} 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /content/1/chapter1/4.tex: -------------------------------------------------------------------------------- 1 | 还可以为模板参数定义默认值。这些值称为默认模板参数,可以用于任何类型的模板。 2 | 3 | \begin{tcolorbox}[colback=webgreen!5!white,colframe=webgreen!75!black] 4 | \hspace*{0.75cm}C++11前,因为历史原因,默认模板参数只允许在类模板中使用。 5 | \end{tcolorbox} 6 | 7 | 甚至可能引用前面的模板参数。 8 | 9 | 若希望定义返回类型的方法与具有多个参数类型的能力结合起来(如前一节所述),那么可以为返回类型引入模板参数RT,将两个参数的公共类型作为默认类型。同样,有多种选择: 10 | 11 | \begin{enumerate} 12 | \item 13 | 可以直接使用三元操作符。但在使用三元操作符之前,只能使用参数的类型: 14 | 15 | \hspace*{\fill} \\ %插入空行 16 | \noindent 17 | \textit{basics/maxdefault1.hpp} 18 | \begin{lstlisting}[style=styleCXX] 19 | #include 20 | 21 | template> 23 | RT max (T1 a, T2 b) 24 | { 25 | return b < a ? a : b; 26 | } 27 | \end{lstlisting} 28 | 29 | 注意std::decay\_t<>的用法以确保返回的不是引用类型。 30 | 31 | \begin{tcolorbox}[colback=webgreen!5!white,colframe=webgreen!75!black] 32 | \hspace*{0.75cm}C++11中,必须使用typename std::decay<…>::type,而非std::decay\_t<…>(见第2.8节)。 33 | \end{tcolorbox} 34 | 35 | 此实现要求能够为传递的类型调用默认构造函数。还有另一种解决方案,使用std::declval,但是这会使声明更加复杂。参见第11.2.3节中的示例。 36 | 37 | \item 38 | 也可以使用std::common\_type<>来指定返回类型的默认值: 39 | 40 | \hspace*{\fill} \\ %插入空行 41 | \noindent 42 | \textit{basics/maxdefault3.hpp} 43 | \begin{lstlisting}[style=styleCXX] 44 | #include 45 | 46 | template> 48 | RT max (T1 a, T2 b) 49 | { 50 | return b < a ? a : b; 51 | } 52 | \end{lstlisting} 53 | 54 | 注意std::common\_type<>的衰变,因此返回值不会是引用。 55 | \end{enumerate} 56 | 57 | 可以使用返回类型的默认值: 58 | 59 | \begin{lstlisting}[style=styleCXX] 60 | auto a = ::max(4, 7.2); 61 | \end{lstlisting} 62 | 63 | 或者显式地指定返回的类型: 64 | 65 | \begin{lstlisting}[style=styleCXX] 66 | auto b = ::max(7.2, 4); 67 | \end{lstlisting} 68 | 69 | 不过,目前必须指定三种类型才能只明确指定返回类型。所以,将返回类型移动到第一个模板参数,就可以对其类型进行推导。原则上,即使后面没有默认参数,推导的函数模板参数也可以有默认类型: 70 | 71 | \begin{lstlisting}[style=styleCXX] 72 | template 73 | RT max (T1 a, T2 b) 74 | { 75 | return b < a ? a : b; 76 | } 77 | \end{lstlisting} 78 | 79 | 通过定义,可以这样使用: 80 | 81 | \begin{lstlisting}[style=styleCXX] 82 | int i; 83 | long l; 84 | ... 85 | max(i, l); // 返回long(返回类型的模板默认类型) 86 | max(4, 42); // 显式返回int 87 | \end{lstlisting} 88 | 89 | 但这种方法只在模板参数有默认值时才有意义。这里,需要模板参数的默认值依赖于之前的模板参数。原理上这可行(在26.5.1节中会进行讨论),但是这种技术依赖于类型特征,会使定义复杂化。 90 | 91 | 由于所有这些原因,最好和最简单的解决方案是让编译器推导出第1.3.2节中提出的返回类型。 92 | -------------------------------------------------------------------------------- /content/1/chapter1/6.tex: -------------------------------------------------------------------------------- 1 | 即使是这些简单的函数模板示例,也可能引发进一步的问题。有三个常见问题,在这里简单的讨论下。 2 | 3 | \subsubsubsection{1.6.1\hspace{0.2cm}使用值,还是引用传递参数?} 4 | 5 | 为什么通常声明函数按值传递参数,而不是使用引用。一般来说,除了简单类型(比如基本类型或\texttt{std::string\_view}),因为不会创建副本,所以推荐使用引用传递。 6 | 7 | 然而,按值传递在下面的情况下会更好: 8 | 9 | \begin{itemize} 10 | \item 11 | 语法简单。 12 | 13 | \item 14 | 编译器会进行很好的优化。 15 | 16 | \item 17 | 移动语义会使复制成本降低。 18 | 19 | \item 20 | 没有复制或移动操作。 21 | \end{itemize} 22 | 23 | 此外,对于模板来说: 24 | 25 | \begin{itemize} 26 | \item 27 | 模板可能同时用于简单类型和复杂类型,因此为复杂类型选择这种方法时,可能会对简单类型产生反效果。 28 | 29 | \item 30 | 作为调用者,可以通过引用来传递参数,可以使用\texttt{std::ref()}和\texttt{std::cref()}(参见7.3节)。 31 | 32 | \item 33 | 虽然传递字符串字面值或原始数组可能会产生问题,但通过引用传递通常会有更多的问题。 34 | \end{itemize} 35 | 36 | 这些将在第7章中详细讨论。目前,我们使用值传递参数(除非某些功能只有在使用引用时才使用引用)。 37 | 38 | \subsubsubsection{1.6.2\hspace{0.2cm}为什么不用内联?} 39 | 40 | 通常,函数模板不必使用内联声明。与普通的非内联函数不同,我们可以在头文件中定义非内联函数模板,并在多个翻译单元中包含该头文件。 41 | 42 | 该规则的唯一例外是,对特定类型的模板进行完全特化,从而产生的代码不再是泛型(定义了所有模板参数)。参见9.2节了解更多细节。 43 | 44 | 从严格的语言定义角度来看,内联意味着函数的定义可以在程序中出现多次。也表示编译器对该函数的调用应该“内联展开”:某些情况下可以产生更有效的代码,但在许多其他情况下反而会降低代码的效率。现在,编译器通常能够更好地决定是否采纳使用inline关键字的提示。但是,编译器仍然要考虑在该决策中是否存在内联。 45 | 46 | \subsubsubsection{1.6.3\hspace{0.2cm}为什么不用constexpr?} 47 | 48 | C++11后,可以使用constexpr提供在编译时计算某些值的能力。对于很多模板来说,这很有意义。 49 | 50 | 例如,为了能够在编译时使用maximum函数,必须声明它: 51 | 52 | \hspace*{\fill} \\ %插入空行 53 | \noindent 54 | \textit{basics/maxconstexpr.hpp} 55 | \begin{lstlisting}[style=styleCXX] 56 | template 57 | constexpr auto max (T1 a, T2 b) 58 | { 59 | return b < a ? a : b; 60 | } 61 | \end{lstlisting} 62 | 63 | 这样,就可以在有编译时使用maximum函数模板,比如在声明原始数组的大小时: 64 | 65 | \begin{lstlisting}[style=styleCXX] 66 | int a[::max(sizeof(char),1000u)]; 67 | \end{lstlisting} 68 | 69 | 或者定义\texttt{std::array<>}的大小: 70 | 71 | \begin{lstlisting}[style=styleCXX] 72 | std::array arr; 73 | \end{lstlisting} 74 | 75 | 注意,将1000作为unsigned int传递是为了避免在模板中比较有符号值和无符号值时,编译器发出的警告。 76 | 77 | 第8.2节将讨论使用constexpr的例子。为了让我们的注意力集中在基本特性上,在讨论其他模板特性时,我们通常会跳过constexpr。 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /content/1/chapter1/7.tex: -------------------------------------------------------------------------------- 1 | 2 | \begin{itemize} 3 | \item 4 | 函数模板为不同的模板参数定义了一个函数族。 5 | 6 | \item 7 | 当根据模板参数向函数传递参数时,函数模板会根据相应的参数类型推导出要实例化的模板参数。 8 | 9 | \item 10 | 可以显式给定模板参数。 11 | 12 | \item 13 | 可以为模板参数定义默认参数。这些参数可以使用前面的模板参数,后面跟着没有默认参数的参数。 14 | 15 | \item 16 | 函数模板可以重载。 17 | 18 | \item 19 | 函数模板与其他函数模板重载时,应确保只有一个函数模板与调用匹配。 20 | 21 | \item 22 | 重载函数模板时,应将更改限制为显式指定模板参数。 23 | 24 | \item 25 | 确保编译器在调用函数模板前,了解函数模板所有的重载版本。 26 | \end{itemize} -------------------------------------------------------------------------------- /content/1/chapter10/0.tex: -------------------------------------------------------------------------------- 1 | 我们已经介绍了C++中模板的基本概念。了解细节之前,来看看会使用到的术语。在C++社区中(甚至在标准的早期版本中),有时术语缺乏准确性。 -------------------------------------------------------------------------------- /content/1/chapter10/1.tex: -------------------------------------------------------------------------------- 1 | C++中,结构体、类和联合体统称为类类型。如果没有附加的限定条件,纯文本类型中的单词“class”包含通过关键字class或关键字struct引入的类类型。 2 | 3 | \begin{tcolorbox}[colback=webgreen!5!white,colframe=webgreen!75!black] 4 | \hspace*{0.75cm}C++中,class和struct之间的唯一区别是,class的默认访问权限是private,而struct的默认访问权限是public。然而,对于使用新的C++特性的类型,我们更倾向使用class,而对于普通的C数据结构,使用struct,这些数据结构可以用作“普通数据”(POD)。 5 | \end{tcolorbox} 6 | 7 | 注意,“类类型”包括联合,但“类”不包括。 8 | 9 | 对于模板类的调用方式有一些混乱: 10 | 11 | \begin{itemize} 12 | \item 13 | 术语“类模板”表示类是一个模板。也就是说,它是一族类的参数化描述。 14 | 15 | \item 16 | 另一方面,已经使用了术语模板类 17 | \begin{itemize} 18 | \item[-] 19 | 类模板的同义词。 20 | 21 | \item[-] 22 | 引用从模板生成的类。 23 | 24 | \item[-] 25 | 使用模板id(模板名称后跟在<和>之间指定的模板参数的组合)来引用类。 26 | \end{itemize} 27 | 28 | 第二和第三个之间的区别有些微妙,不过这并不重要。 29 | \end{itemize} 30 | 31 | 由于这种确定性,所以在本书中会避免使用模板类这个术语。 32 | 33 | 类似地,使用函数模板、成员模板、成员函数模板和变量模板,但不使用模板函数、模板成员、模板成员函数和模板变量。 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /content/1/chapter10/2.tex: -------------------------------------------------------------------------------- 1 | 处理使用模板的源代码时,C++编译器必须用具体的模板实参替换模板中的模板形参。有时,这种替换是暂时的:编译器可能需要检查替换是否有效(参见8.4节和15.7节)。 2 | 3 | 通过替换模板的具体参数,为模板中的常规类、类型别名、函数、成员函数或变量实际创建定义的过程,称为模板实例化。 4 | 5 | 但目前还没有标准或公认的术语,来表示通过模板参数替换来创建非定义声明的过程。我们已经看到了一些团队使用的部分实例化或声明的实例化,但这不通用。也许更直观的术语是不完全实例化(类模板的情况下,会生成不完整的类)。 6 | 7 | 由实例化或不完全实例化(即类、函数、成员函数或变量)产生的实体一般称为特化。 8 | 9 | C++中实例化过程并不是产生特化的唯一方法。替代机制允许开发者显式地指定与模板参数的特殊替换绑定的声明。如在2.5节中看到的,这样的特化是通过前缀template<>引入的: 10 | 11 | \begin{lstlisting}[style=styleCXX] 12 | template // primary class template 13 | class MyClass { 14 | ... 15 | }; 16 | 17 | template<> // explicit specialization 18 | class MyClass { 19 | ... 20 | }; 21 | \end{lstlisting} 22 | 23 | 严格地说,这称为显式特化(与实例化或生成的特化相对)。 24 | 25 | 如2.6节所述,仍有模板参数的特化称为偏特化: 26 | 27 | \begin{lstlisting}[style=styleCXX] 28 | template // partial specialization 29 | class MyClass { 30 | ... 31 | }; 32 | 33 | template // partial specialization 34 | class MyClass { 35 | ... 36 | }; 37 | \end{lstlisting} 38 | 39 | 当说到(显式或部分)特化时,通用模板也称为主模板。 40 | -------------------------------------------------------------------------------- /content/1/chapter10/3.tex: -------------------------------------------------------------------------------- 1 | 2 | 目前,“声明”和“定义”这两个词在本书中只出现过几次。然而,这些词在标准C++中有相当精确的含义。 3 | 4 | 声明是一种C++构造,在C++作用域中引入或重新引入一个名称。此介绍会包含该名称的部分类别,但进行有效声明时不需要详细信息。例如: 5 | 6 | \begin{lstlisting}[style=styleCXX] 7 | class C; // a declaration of C as a class 8 | void f(int p); // a declaration of f() as a function and p as a named parameter 9 | extern int v; // a declaration of v as a variable 10 | \end{lstlisting} 11 | 12 | 注意,宏定义和goto标签在C++中不是声明。 13 | 14 | 当声明的结构已知时,或对于变量,必须分配存储空间时,声明就变成了定义。对于类类型定义,必须提供带括号的主体。对于函数定义,这就必须提供(一般情况下)用大括号括起来的函数体,或者必须将函数指定为=default或=delete。对于变量,初始化或缺少extern说明符会导致声明变成定义。下面是补充上述非定义声明的示例: 15 | 16 | \begin{tcolorbox}[colback=webgreen!5!white,colframe=webgreen!75!black] 17 | \hspace*{0.75cm}默认函数是由编译器提供默认实现的特殊成员函数,例如:默认复制构造函数。 18 | \end{tcolorbox} 19 | 20 | \begin{lstlisting}[style=styleCXX] 21 | class C {}; // definition (and declaration) of class C 22 | 23 | void f(int p) { // definition (and declaration) of function f() 24 | std::cout << p << ’\n’; 25 | } 26 | 27 | extern int v = 1; // an initializer makes this a definition for v 28 | 29 | int w; // global variable declarations not preceded by 30 | // extern are also definitions 31 | \end{lstlisting} 32 | 33 | 通过扩展,类模板或函数模板的声明若有主体,就称为定义。因此, 34 | 35 | \begin{lstlisting}[style=styleCXX] 36 | template 37 | void func (T); 38 | \end{lstlisting} 39 | 40 | 声明不是定义, 41 | 42 | \begin{lstlisting}[style=styleCXX] 43 | template 44 | class S {}; 45 | \end{lstlisting} 46 | 47 | 实际上是一个定义。 48 | 49 | \subsubsubsection{10.3.1\hspace{0.2cm}完整类型与不完整类型} 50 | 51 | 类型可以完整的或不完整的,这是一个与声明和定义之间的区别密切相关的概念。有些语言构造需要完整的类型,而其他语言构造也可以使用不完整的类型。 52 | 53 | 不完整类型是以下类型之一: 54 | 55 | \begin{itemize} 56 | \item 57 | 已声明但尚未定义的类类型。 58 | 59 | \item 60 | 未指定边界的数组类型。 61 | 62 | \item 63 | 不完整类型的数组类型。 64 | 65 | \item 66 | 无效类型 67 | 68 | \item 69 | 枚举类型,基础类型或枚举值未定义。 70 | 71 | \item 72 | 上面应用const和/或volatile的类型。 73 | \end{itemize} 74 | 75 | 其他类型都是完整的。例如: 76 | 77 | \begin{lstlisting}[style=styleCXX] 78 | class C; // C is an incomplete type 79 | C const* cp; // cp is a pointer to an incomplete type 80 | extern C elems[10]; // elems has an incomplete type 81 | extern int arr[]; // arr has an incomplete type 82 | ... 83 | class C { }; // C now is a complete type (and therefore cpand elems 84 | // no longer refer to an incomplete type) 85 | int arr[10]; // arr now has a complete type 86 | \end{lstlisting} 87 | 88 | 有关如何处理模板中不完整类型的提示,请参阅11.5节。 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /content/1/chapter10/4.tex: -------------------------------------------------------------------------------- 1 | C++语言定义对各种实体的声明施加了一些约束。这些约束的总和称为单一定义规则或ODR。这个规则的细节有点复杂,并且涉及到很多不同的情况。后面的章节将说明在每个适用的上下文中产生的各种情况,可以在附录A中找到ODR的完整描述。现在,记住ODR的基础知识就足够了: 2 | 3 | \begin{itemize} 4 | \item 5 | 普通(即,不是模板)非内联函数和成员函数,以及(非内联)全局变量和静态数据成员在整个程序中应该只定义一次。 6 | 7 | \begin{tcolorbox}[colback=webgreen!5!white,colframe=webgreen!75!black] 8 | \hspace*{0.75cm}C++17后,全局变量和静态变量,以及数据成员都可以定义为内联。这样就不需要在同一个翻译单元中定义它们。 9 | \end{tcolorbox} 10 | 11 | \item 12 | 类类型(包括结构和联合)、模板(包括偏特化,但不包括全特化)以及内联函数和变量在每个翻译单元中最多定义一次,而且所有这些定义应该相同。 13 | \end{itemize} 14 | 15 | 翻译单元是对源文件进行预处理的结果;也就是说,包含了由\#include指令命名并由宏展开生成的内容。 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 | -------------------------------------------------------------------------------- /content/1/chapter10/5.tex: -------------------------------------------------------------------------------- 1 | 2 | 比较下面的类模板: 3 | 4 | \begin{lstlisting}[style=styleCXX] 5 | template 6 | class ArrayInClass { 7 | public: 8 | T array[N]; 9 | }; 10 | \end{lstlisting} 11 | 12 | 使用普通类: 13 | 14 | \begin{lstlisting}[style=styleCXX] 15 | class DoubleArrayInClass { 16 | public: 17 | double array[10]; 18 | }; 19 | \end{lstlisting} 20 | 21 | 如果将参数T和N分别替换为double和10,则后者在本质上等价于前者。C++中,这个替换可表示为 22 | 23 | \begin{lstlisting}[style=styleCXX] 24 | ArrayInClass 25 | \end{lstlisting} 26 | 27 | 注意模板名称后面是如何用尖括号将模板参数括起来的。 28 | 29 | 不管这些实参本身是否依赖于模板形参,模板名后跟尖括号中的实参的组合称为模板标识。 30 | 31 | 可以像使用非模板一样使用这个名称。例如: 32 | 33 | \begin{lstlisting}[style=styleCXX] 34 | int main() 35 | { 36 | ArrayInClass ad; 37 | ad.array[0] = 1.0; 38 | } 39 | \end{lstlisting} 40 | 41 | 区分模板形参和模板实参很重要。简而言之,“参数由实参初始化”。 42 | 43 | \begin{tcolorbox}[colback=webgreen!5!white,colframe=webgreen!75!black] 44 | \hspace*{0.75cm}学术界中,参数有时称为实际参数,而声明参数称为形式参数。 45 | \end{tcolorbox} 46 | 47 | 或者更准确地说: 48 | 49 | \begin{itemize} 50 | \item 51 | 模板参数是那些列在模板声明或定义中的关键字Template之后的参数(示例中是T和N)。 52 | 53 | \item 54 | 模板实参是替代模板形参的项(示例中是double和10)。与模板形参不同,模板实参可以不仅仅是“名称”。 55 | \end{itemize} 56 | 57 | 当使用模板标识表示时,模板实参对模板形参是显式替换的。但在许多情况下,替换是隐式的(例如,如果模板形参被默认实参替换)。 58 | 59 | 基本原则是,模板参数必须在编译时确定。这个需求对于模板实体运行时成本有巨大的好处。因为模板形参最终会被编译时的值替代,所以其本身可以形成编译时表达式。这在ArrayInClass模板中可以用来调整成员数组的大小。数组的大小必须是一个常量表达式,而模板参数N刚好符合此条件。 60 | 61 | 可以进一步推进这种方式:因为模板形参是编译时实体,所以也可以用来创建有效的模板形参。下面是一个例子: 62 | 63 | \begin{lstlisting}[style=styleCXX] 64 | template 65 | class Dozen { 66 | public: 67 | ArrayInClass contents; 68 | }; 69 | \end{lstlisting} 70 | 71 | 本例中,名称T既是模板形参,又是模板实参。因此,可以使用一种机制从更简单的模板构建更复杂的模板。当然,这与组装类型和函数的机制没有区别。 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 | -------------------------------------------------------------------------------- /content/1/chapter10/6.tex: -------------------------------------------------------------------------------- 1 | 2 | \begin{itemize} 3 | \item 4 | 对于属于模板的类、函数和变量,请分别使用类模板、函数模板和变量模板。 5 | 6 | \item 7 | 模板实例化是通过将模板形参替换为具体实参,来创建常规类或函数的过程。产生的实体是特化模板类型。 8 | 9 | \item 10 | 类型可以是完整的或不完整的。 11 | 12 | \item 13 | 根据单一定义规则(ODR):程序中,非内联函数、成员函数、全局变量和静态数据成员应该只定义一次。 14 | \end{itemize} -------------------------------------------------------------------------------- /content/1/chapter11/0.tex: -------------------------------------------------------------------------------- 1 | 目前,对模板的讨论集中在特定特性、功能和约束上,同时考虑到直接使用和应用程序(这是应用程序开发者会遇到的事情)。然而,模板在用于编写通用库和框架时效率很高。在这些地方,设计必须考虑潜在的用途,这些用途是通用的,所以不受限制。虽然本书中所有的内容都适用于这样的设计,但在编写可移植组件时,应该考虑一些通用性问题,这些组件要适用于尚未想象到的类型。 2 | 3 | 这里提出的问题并不完整,但它总结了迄今为止介绍的一些特性,介绍了一些附加特性,并引用了本书后面涉及的一些特性。我们希望本章能推动读者阅读后面的章节。 -------------------------------------------------------------------------------- /content/1/chapter11/3.tex: -------------------------------------------------------------------------------- 1 | 如6.1节所示,可以使用转发引用和std::forward<>来“完美转发”泛型参数: 2 | 3 | \begin{lstlisting}[style=styleCXX] 4 | template 5 | void f (T&& t) // t is forwarding reference 6 | { 7 | g(std::forward(t)); // perfectly forward passed argument t to g() 8 | } 9 | \end{lstlisting} 10 | 11 | 然而,有时必须完美地转发不通过参数的泛型代码中的数据。可以使用auto\&\&来创建一个可以转发的变量,假设对函数get()和set()进行了链接调用,其中get()的返回值应该完美地转发给set(): 12 | 13 | \begin{lstlisting}[style=styleCXX] 14 | template 15 | void foo(T x) 16 | { 17 | set(get(x)); 18 | } 19 | \end{lstlisting} 20 | 21 | 进一步假设需要更新代码,以便对get()产生的中间值执行一些操作。通过将值保存在使用auto\&\&声明的变量中来实现: 22 | 23 | \begin{lstlisting}[style=styleCXX] 24 | template 25 | void foo(T x) 26 | { 27 | auto&& val = get(x); 28 | ... 29 | // perfectly forward the return value of get() to set(): 30 | set(std::forward(val)); 31 | } 32 | \end{lstlisting} 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 | -------------------------------------------------------------------------------- /content/1/chapter11/5.tex: -------------------------------------------------------------------------------- 1 | 实现模板时,有时会出现这样的问题:代码是否能够处理不完整的类型(参见10.3.1节)。来看看下面的类模板: 2 | 3 | \begin{lstlisting}[style=styleCXX] 4 | template 5 | class Cont { 6 | private: 7 | T* elems; 8 | public: 9 | ... 10 | }; 11 | \end{lstlisting} 12 | 13 | 这个类可以与不完整类型一起使用。例如:当类引用自己类型的元素时: 14 | 15 | \begin{lstlisting}[style=styleCXX] 16 | struct Node 17 | { 18 | std::string value; 19 | Cont next; // only possible if Cont accepts incomplete types 20 | }; 21 | \end{lstlisting} 22 | 23 | 然而,仅通过使用一些特性,就会失去处理不完整类型的能力。例如: 24 | 25 | \begin{lstlisting}[style=styleCXX] 26 | template 27 | class Cont { 28 | private: 29 | T* elems; 30 | public: 31 | ... 32 | typename std::conditional::value, 33 | T&&, 34 | T& 35 | >::type 36 | foo(); 37 | }; 38 | \end{lstlisting} 39 | 40 | 这里,使用特征std::conditional(参见D.5节)来决定成员函数foo()的返回类型是T\&\&还是T\&。这取决于模板参数类型T是否支持移动语义。 41 | 42 | 问题是特性std::is\_move\_constructible要求参数是一个完整的类型(不是void或未知边界的数组;参见的D.3.2节)。在foo()的这个声明中,struct Node的声明失败了。 43 | 44 | \begin{tcolorbox}[colback=webgreen!5!white,colframe=webgreen!75!black] 45 | \hspace*{0.75cm}如果std::is\_move\_constructible是一个完整的类型,并不是所有的编译器都会产生错误。因为对于这种错误,不需要进行诊断。所以,在需要平台移植时需要考虑这个问题。 46 | \end{tcolorbox} 47 | 48 | 可以将foo()替换为成员模板来解决这个问题,这样std::is\_move\_constructible的计算就会延迟到foo()的实例化点: 49 | 50 | \begin{lstlisting}[style=styleCXX] 51 | template 52 | class Cont { 53 | private: 54 | T* elems; 55 | public: 56 | template std::conditional::value, 57 | T&&, 58 | T& 59 | >::type 60 | foo(); 61 | }; 62 | \end{lstlisting} 63 | 64 | 现在,特性依赖于模板参数D(默认为T,我们想要的值),编译器必须等到foo()调,如Node之前,再评估特性(那时Node是一个完整的类型,只是在定义时不完整)。 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /content/1/chapter11/6.tex: -------------------------------------------------------------------------------- 1 | 让我们列出一些在实现泛型库时需要记住的事情(注意,其中一些可能会在后面会介绍到): 2 | 3 | \begin{itemize} 4 | \item 5 | 模板中使用转发引用来转发值(参见第91页6.1节)。如果值不依赖于模板参数,使用auto\&\&(参见11.3节)。 6 | 7 | \item 8 | 当参数声明为转发引用时,模板参数在传递左值时要有引用类型(参见15.6.2节)。 9 | 10 | \item 11 | 当需要依赖于模板形参的对象地址时,使用std::addressof(),以避免当对象绑定到带有重载操作符\&的类型时出现意外(11.2.2节) 12 | 13 | \item 14 | 对于成员函数模板,确保不会比预定义的复制/移动构造函数或赋值操作符更好地匹配(6.4节)。 15 | 16 | \item 17 | 模板参数可能是字符串字面值,且不通过值传递时(7.4节和D.4节),请考虑使用std::decay。 18 | 19 | \item 20 | 如果模板参数有out或inout,请准备好处理参数可能指定为const类型的情况(参见7.2.2节)。 21 | 22 | \item 23 | 准备好处理模板参数引用的副作用(参见11.4节了解详细信息,19.6.1节为示例)。特别是,要确保返回类型不能是引用(参见7.5节)。 24 | 25 | \item 26 | 准备好处理不完全类型,从而进行以支持,例如:递归数据结构(参见11.5节)。 27 | 28 | \item 29 | 重载所有数组类型,而不仅仅是T[SZ](参见5.4节)。 30 | \end{itemize} -------------------------------------------------------------------------------- /content/1/chapter11/7.tex: -------------------------------------------------------------------------------- 1 | 2 | \begin{itemize} 3 | \item 4 | 模板允许将函数、函数指针、函数对象、函子和Lambda作为可调用对象传递。 5 | 6 | \item 7 | 使用重载operator()定义类时,将其声明为const(除非调用改变了状态)。 8 | 9 | \item 10 | 使用std::invoke(),可以处理所有可调用对象的代码,包括成员函数。 11 | 12 | \item 13 | 使用decltype(auto)来完美地转发返回值。 14 | 15 | \item 16 | 类型特征是检查类型属性和功能性函数。 17 | 18 | \item 19 | 当需要模板中对象的地址时,可以使用std::addressof()。 20 | 21 | \item 22 | 使用std::declval()在未计算的表达式中创建特定类型的值。 23 | 24 | \item 25 | 如果对象的类型不依赖于模板参数,可以使用auto\&\&在泛型代码中完美转发。 26 | 27 | \item 28 | 准备好处理模板参数作为引用的副作用。 29 | 30 | \item 31 | 可以使用模板来推迟对表达式的求值(例如,支持在类模板中使用不完整类型)。 32 | \end{itemize} -------------------------------------------------------------------------------- /content/1/chapter2/0.tex: -------------------------------------------------------------------------------- 1 | 与函数类似,类也可以用一个或多个类型参数化。用于管理特定类型元素的容器类就是个例子。通过使用类模板,可以在元素类型开放的情况下实现容器类。本章中,我们将使用堆栈作为类模板的示例。 -------------------------------------------------------------------------------- /content/1/chapter2/10.tex: -------------------------------------------------------------------------------- 1 | 2 | 聚合类(不由用户提供、显式或继承的构造函数的类/结构,没有private或protected的非静态数据成员,没有虚函数,也没有virtual、private或protected基类)也可以是模板。例如: 3 | 4 | \begin{lstlisting}[style=styleCXX] 5 | template 6 | struct ValueWithComment { 7 | T value; 8 | std::string comment; 9 | }; 10 | \end{lstlisting} 11 | 12 | 定义一个聚合,参数化了value的类型。可以像其他类模板一样声明对象,并且可以以聚合的方式进行使用: 13 | 14 | \begin{lstlisting}[style=styleCXX] 15 | ValueWithComment vc; 16 | vc.value = 42; 17 | vc.comment = "initial value"; 18 | \end{lstlisting} 19 | 20 | C++17后,甚至可以为聚合类模板定义推导策略: 21 | 22 | \begin{lstlisting}[style=styleCXX] 23 | ValueWithComment(char const*, char const*) 24 | -> ValueWithComment; 25 | ValueWithComment vc2 = {"hello", "initial value"}; 26 | \end{lstlisting} 27 | 28 | 因为ValueWithComment没有用于执行推导的构造函数,所以若没有推导策略,初始化将不可能完成。 29 | 30 | 标准库类std::array<>也是一个聚合,参数化了元素类型和大小。C++17标准库还为其定义了推导策略,这将在4.4.4节中讨论。 31 | -------------------------------------------------------------------------------- /content/1/chapter2/11.tex: -------------------------------------------------------------------------------- 1 | 2 | \begin{itemize} 3 | \item 4 | 类模板是在实现时保留一个或多个类型参数的类。 5 | 6 | \item 7 | 要使用类模板,需要将类型作为模板参数传递。并为这些类型,实例化(并编译)类模板。 8 | 9 | \item 10 | 对于类模板,只有调用的成员函数会实例化。 11 | 12 | \item 13 | 可以为某些类型特化类模板。 14 | 15 | \item 16 | 可以偏特化某些类型的类模板。 17 | 18 | \item 19 | C++17后,可以从构造函数中自动推导出类模板的参数。 20 | 21 | \item 22 | 可以定义聚合类模板。 23 | 24 | \item 25 | 若声明为按值调用,则模板类型的调用参数会衰变。 26 | 27 | \item 28 | 模板只能在全局/命名空间作用域或类声明内部声明和定义。 29 | \end{itemize} -------------------------------------------------------------------------------- /content/1/chapter2/2.tex: -------------------------------------------------------------------------------- 1 | C++17前,要使用类模板的对象,必须显式指定模板参数。 2 | 3 | \begin{tcolorbox}[colback=webgreen!5!white,colframe=webgreen!75!black] 4 | \hspace*{0.75cm}C++17引入了类参数模板推导,若模板参数可以从构造函数派生,则可以跳过这些参数。这会在第2.9节中进行讨论。 5 | \end{tcolorbox} 6 | 7 | 下面的例子展示了如何使用类模板Stack<>: 8 | 9 | \hspace*{\fill} \\ %插入空行 10 | \noindent 11 | \textit{basics/stack1test.cpp} 12 | \begin{lstlisting}[style=styleCXX] 13 | #include "stack1.hpp" 14 | #include 15 | #include 16 | 17 | int main() 18 | { 19 | Stack intStack; // stack of ints 20 | Stack stringStack; // stack of strings 21 | 22 | // manipulate int stack 23 | intStack.push(7); 24 | std::cout << intStack.top() << '\n'; 25 | 26 | // manipulate string stack 27 | stringStack.push("hello"); 28 | std::cout << stringStack.top() << '\n'; 29 | stringStack.pop(); 30 | } 31 | \end{lstlisting} 32 | 33 | 通过声明类型Stack,int在类模板中作为类型T。因此,intStack是一个对象,使用的是vector类型,并且调用相应的成员函数。类似地,通过声明和使用Stack,将创建使用vector的对象,使用相应的成员函数。 34 | 35 | 注意,代码只对调用的模板(成员)函数实例化。对于类模板,只有在使用成员函数时才实例化。当然,这节省了时间和空间,并且只允许使用部分地类模板,这会在2.3节中详细讨论。 36 | 37 | 例子中,默认构造函数push()和top()为int和string实例化,但pop()只对string实例化。如果类模板具有静态成员,则对于使用类模板的每个类型实例,这些成员也会实例化一次。 38 | 39 | 可以像使用其他类型一样使用实例化的类模板类型。可以使用const、volatile或从中派生数组和引用类型对其进行限定。也可以将其作为类型定义的一部分,使用typedef或using(请参阅第2.8节了解关于类型定义的详细信息),或者在构建另一个模板类型时将其用作类型参数。例如: 40 | 41 | \begin{lstlisting}[style=styleCXX] 42 | void foo(Stack const& s) // parameter s is int stack 43 | { 44 | using IntStack = Stack; // IntStack is another name for Stack 45 | Stack istack[10]; // istack is array of 10 int stacks 46 | IntStack istack2[10]; // istack2 is also an array of 10 int stacks (same type) 47 | ... 48 | } 49 | \end{lstlisting} 50 | 51 | 模板参数可以是任何类型,例如float指针,甚至是Stack: 52 | 53 | \begin{lstlisting}[style=styleCXX] 54 | Stack floatPtrStack; // stack of float pointers 55 | Stack> intStackStack; // stack of stack of ints 56 | \end{lstlisting} 57 | 58 | 这里的要求是,需要这种类型支持所使用的操作。 59 | 60 | 注意,在C++11之前,必须在两个模板右括号之间放空格: 61 | 62 | \begin{lstlisting}[style=styleCXX] 63 | Stack > intStackStack; // 所有C++版本都可以用 64 | \end{lstlisting} 65 | 66 | 如果没有这样做,就使用>{}>,会导致语法错误: 67 | 68 | \begin{lstlisting}[style=styleCXX] 69 | Stack> intStackStack; // C++11之前会报错 70 | \end{lstlisting} 71 | 72 | 旧行为可以帮助C++编译器对独立于代码语义源码进行标记。然而,由于缺少空格是一个错误,这需要相应的错误消息,因此无论如何都必须考虑代码的语义。因此,在C++11中,在两个模板右括号之间放一个空格的规则被“尖括号黑客”删除了(详见13.3.1节)。 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /content/1/chapter2/3.tex: -------------------------------------------------------------------------------- 1 | 2 | 类模板通常对其实例化的模板参数使用多个操作(包括构造和析构)。这可能会使,这些模板参数必须为所有成员函数,提供所有必要的操作,而模板参数只需要提供所需的必要操作(而不是可能需要)即可。 3 | 4 | 例如,类Stack<>提供成员函数printOn()打印堆栈中的全部内容,并对每个元素使用操作符<{}<: 5 | 6 | \begin{lstlisting}[style=styleCXX] 7 | template 8 | class Stack { 9 | ... 10 | void printOn(std::ostream& strm) const { 11 | for (T const& elem : elems) { 12 | strm << elem << ' '; // 对每个元素使用<< 13 | } 14 | } 15 | }; 16 | \end{lstlisting} 17 | 18 | 对于没有定义操作符<{}<的类型,仍然可以使用这个类: 19 | 20 | \begin{lstlisting}[style=styleCXX] 21 | Stack> ps; // 注意: std::pair<>不支持<< 22 | ps.push({4, 5}); // OK 23 | ps.push({6, 7}); // OK 24 | std::cout << ps.top().first << '\n'; // OK 25 | std::cout << ps.top().second << '\n'; // OK 26 | \end{lstlisting} 27 | 28 | 只在使用printOn()时代码才会出错,因为不能实例化特定元素类型的操作符<{}<: 29 | 30 | \begin{lstlisting}[style=styleCXX] 31 | ps.printOn(std::cout); // ERROR: 元素类型不支持操作符<< 32 | \end{lstlisting} 33 | 34 | \subsubsubsection{2.3.1\hspace{0.2cm}概念} 35 | 36 | 先来看一个问题:如何知道模板需要哪些操作才能实例化?“概念”通常用来表示模板库中需要的约束。例如,C++标准库依赖于随机访问迭代器和默认可构造函数等概念。 37 | 38 | 目前(如C++17),概念只能通过文字进行表达(如代码注释)。这可能会成为一个严重的问题,因为不遵守约束可能会导致大量的错误消息(参见9.4)。 39 | 40 | 多年来,也有一些方法和试验支持将概念定义和验证为一种语言特性。然而,到C++17为止,还这样的方法还没标准化。 41 | 42 | 从C++11开始,可以通过使用static\_assert和预定义类型特征来检查约束。例如: 43 | 44 | \begin{lstlisting}[style=styleCXX] 45 | template 46 | class C 47 | { 48 | static_assert(std::is_default_constructible::value, 49 | "Class C requires default-constructible elements"); 50 | ... 51 | }; 52 | \end{lstlisting} 53 | 54 | 使用默认构造函数,若没有这个断言,编译会失败。然而,错误消息描述的是整个模板实例化的过程,从最初的实例化原因到检测到错误的实际模板的定义(参见章节9.4)。 55 | 56 | 但需要更复杂的代码来检查,例如:T类型的对象是够提供了特定的成员函数,或者是否有可用的小于操作符。相关的示例,请参见第19.6.3节。 57 | 58 | 有关C++概念的详细讨论,请参阅附录E。 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /content/1/chapter2/4.tex: -------------------------------------------------------------------------------- 1 | 2 | 使用printOn()打印堆栈内容,不如为堆栈实现<{}<操作符。然而,通常<{}<操作符会实现为非成员函数,然后内联调用printOn(): 3 | 4 | \begin{lstlisting}[style=styleCXX] 5 | template 6 | class Stack { 7 | ... 8 | void printOn(std::ostream& strm) const { 9 | ... 10 | } 11 | friend std::ostream& operator<< (std::ostream& strm, 12 | Stack const& s) { 13 | s.printOn(strm); 14 | return strm; 15 | } 16 | }; 17 | \end{lstlisting} 18 | 19 | 这意味着用于类Stack<>的<{}<操作符不是一个函数模板,而是在需要时用类模板实例化的“普通”函数。 20 | 21 | \begin{tcolorbox}[colback=webgreen!5!white,colframe=webgreen!75!black] 22 | \hspace*{0.75cm}其是一个模板实体,参见12.1节。 23 | \end{tcolorbox} 24 | 25 | 但当试图声明友元函数并实现时,事情会变得更加复杂。这里有两种选择: 26 | 27 | \begin{enumerate} 28 | \item 29 | 隐式声明一个新的函数模板,但要使用不同的模板参数,比如U: 30 | 31 | \begin{lstlisting}[style=styleCXX] 32 | template 33 | class Stack { 34 | ... 35 | template 36 | friend std::ostream& operator<< (std::ostream&, Stack const&); 37 | }; 38 | \end{lstlisting} 39 | 40 | 再次使用T或跳过模板参数都不起作用(要么内部的T隐藏外部的T,要么在命名空间作用域中声明一个非模板函数)。 41 | 42 | \item 43 | 可以将Stack的输出操作符转发声明为模板,这样就需要转发声明Stack: 44 | 45 | \begin{lstlisting}[style=styleCXX] 46 | template 47 | class Stack; 48 | template 49 | std::ostream& operator<< (std::ostream&, Stack const&); 50 | \end{lstlisting} 51 | 52 | 然后,将该函数声明为友元: 53 | 54 | \begin{lstlisting}[style=styleCXX] 55 | template 56 | class Stack { 57 | ... 58 | friend std::ostream& operator<< (std::ostream&, 59 | Stack const&); 60 | }; 61 | \end{lstlisting} 62 | 63 | 注意“函数名”操作符<{}<后面的。因此,需要将非成员函数模板的特化声明为友元。若没有,需要声明新的非模板函数。详细信息请参见12.5.2。 64 | \end{enumerate} 65 | 66 | 其他情况下,可以对没有定义<{}<操作符的元素使用这个类。只有对这个堆栈调用操作符<{}<时才会出现错误: 67 | 68 | \begin{lstlisting}[style=styleCXX] 69 | Stack> ps; // std::pair<> has no operator<< defined 70 | ps.push({4, 5}); // OK 71 | ps.push({6, 7}); // OK 72 | std::cout << ps.top().first << '\n'; // OK 73 | std::cout << ps.top().second << '\n'; // OK 74 | std::cout << ps << '\n'; // ERROR: operator<< not supported 75 | \end{lstlisting} 76 | 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /content/1/chapter2/5.tex: -------------------------------------------------------------------------------- 1 | 可以为某些模板参数特化类模板。类似于函数模板的重载(参见第1.5节),特化类模板允许优化特定类型的实现,或者为类模板的实例化修复特定类型的错误行为。但若特化类模板,则必须特化所有成员函数。虽然可以特化类模板的单个成员函数,但若这样做了,就不能再特化该特化成员所属的整个类模板实例。 2 | 3 | 要特化类模板,必须用template<>和类模板特化的类型声明类。这些类型可用作模板参数,必须直接在类名之后指定: 4 | 5 | \begin{lstlisting}[style=styleCXX] 6 | template<> 7 | class Stack { 8 | ... 9 | }; 10 | \end{lstlisting} 11 | 12 | 对于这些特化,成员函数的定义必须是一个“普通的”成员函数,每次出现T都会使用特化类型替换: 13 | 14 | \begin{lstlisting}[style=styleCXX] 15 | void Stack::push (std::string const& elem) 16 | { 17 | elems.push_back(elem); // append copy of passed elem 18 | } 19 | \end{lstlisting} 20 | 21 | 下面是std::string类型的Stack<>特化: 22 | 23 | \hspace*{\fill} \\ %插入空行 24 | \noindent 25 | \textit{basics/stack2.hpp} 26 | \begin{lstlisting}[style=styleCXX] 27 | #include "stack1.hpp" 28 | #include 29 | #include 30 | #include 31 | 32 | template<> 33 | class Stack { 34 | private: 35 | std::deque elems; // elements 36 | public: 37 | void push(std::string const&); // push element 38 | void pop(); // pop element 39 | std::string const& top() const; // return top element 40 | bool empty() const { // return whether the stack is empty 41 | return elems.empty(); 42 | } 43 | }; 44 | 45 | void Stack::push (std::string const& elem) 46 | { 47 | elems.push_back(elem); // append copy of passed elem 48 | } 49 | 50 | void Stack::pop () 51 | { 52 | assert(!elems.empty()); 53 | elems.pop_back(); // remove last element 54 | } 55 | 56 | std::string const& Stack::top () const 57 | { 58 | assert(!elems.empty()); 59 | return elems.back(); // return last element 60 | } 61 | \end{lstlisting} 62 | 63 | 例子中,特化使用引用语义将字符串参数传递给push(),这对这个特定类型有意义(不过,应该更好地传递转发引用,将在第6.1节中讨论)。 64 | 65 | 另一个区别是使用deque(而非vector)来管理堆栈内的元素。尽管这在这里没有什么区别,但说明了特化实现可能与主模板实现完全不同。 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /content/1/chapter2/7.tex: -------------------------------------------------------------------------------- 1 | 对于函数模板,可以为类模板参数设置默认值。例如,在Stack<>类中,可以将用于管理元素的容器定义为第二个模板参数,并使用std::vector<>作为默认值: 2 | 3 | \hspace*{\fill} \\ %插入空行 4 | \noindent 5 | \textit{basics/stack3.hpp} 6 | \begin{lstlisting}[style=styleCXX] 7 | #include 8 | #include 9 | 10 | template> 11 | class Stack { 12 | private: 13 | Cont elems; // elements 14 | 15 | public: 16 | void push(T const& elem); // push element 17 | void pop(); // pop element 18 | T const& top() const; // return top element 19 | bool empty() const { // return whether the stack is empty 20 | return elems.empty(); 21 | } 22 | }; 23 | 24 | template 25 | void Stack::push (T const& elem) 26 | { 27 | elems.push_back(elem); // append copy of passed elem 28 | } 29 | 30 | template 31 | void Stack::pop () 32 | { 33 | assert(!elems.empty()); 34 | elems.pop_back(); // remove last element 35 | } 36 | 37 | template 38 | T const& Stack::top () const 39 | { 40 | assert(!elems.empty()); 41 | return elems.back(); // return last element 42 | } 43 | \end{lstlisting} 44 | 45 | 现在有了两个模板参数,因此成员函数的每个定义都必须用这两个参数定义: 46 | 47 | \begin{lstlisting}[style=styleCXX] 48 | template 49 | void Stack::push (T const& elem) 50 | { 51 | elems.push_back(elem); // append copy of passed elem 52 | } 53 | \end{lstlisting} 54 | 55 | 可以像以前那样使用这个堆栈。若将第一个也是唯一的参数作为元素类型传递,则会使用vector来管理该类型的元素: 56 | 57 | \begin{lstlisting}[style=styleCXX] 58 | template> 59 | class Stack { 60 | private: 61 | Cont elems; // elements 62 | ... 63 | }; 64 | \end{lstlisting} 65 | 66 | 当在程序中声明Stack对象时,可以指定元素的容器类型: 67 | 68 | \hspace*{\fill} \\ %插入空行 69 | \noindent 70 | \textit{basics/stack3test.cpp} 71 | \begin{lstlisting}[style=styleCXX] 72 | #include "stack3.hpp" 73 | #include 74 | #include 75 | 76 | int main() 77 | { 78 | // stack of ints: 79 | Stack intStack; 80 | 81 | // stack of doubles using a std::deque<> to manage the elements 82 | Stack> dblStack; 83 | 84 | // manipulate int stack 85 | intStack.push(7); 86 | std::cout << intStack.top() << '\n'; 87 | intStack.pop(); 88 | 89 | // manipulate double stack 90 | dblStack.push(42.42); 91 | std::cout << dblStack.top() << '\n'; 92 | dblStack.pop(); 93 | } 94 | \end{lstlisting} 95 | 96 | \begin{lstlisting}[style=styleCXX] 97 | Stack> 98 | \end{lstlisting} 99 | 100 | 上面的代码声明一个double数的栈,使用std::deque<>在内部管理元素。 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /content/1/chapter3/0.tex: -------------------------------------------------------------------------------- 1 | 对于函数和类模板来说,模板参数可以是类型,也可以是普通值。与使用类型参数的模板一样,定义在使用之前。使用这样的模板时,必须显式地指定值。然后,实例化生成的代码。本章演示了新版栈类模板的特性。此外,还会展示非类型函数模板参数的示例,并讨论了这种技术的限制。 -------------------------------------------------------------------------------- /content/1/chapter3/2.tex: -------------------------------------------------------------------------------- 1 | 还可以为函数模板定义非类型参数。例如,下面的函数模板定义了一组函数,可以为其加一个数: 2 | 3 | \hspace*{\fill} \\ %插入空行 4 | \noindent 5 | \textit{basics/addvalue.hpp} 6 | \begin{lstlisting}[style=styleCXX] 7 | template 8 | T addValue (T x) 9 | { 10 | return x + Val; 11 | } 12 | \end{lstlisting} 13 | 14 | 如果函数或操作当作参数,这些类型的函数可能很有用。例如使用C++标准库,可以通过函数模板的实例化来给集合的每个元素加一个数: 15 | 16 | \begin{lstlisting}[style=styleCXX] 17 | std::transform (source.begin(), source.end(), // start and end of source 18 | dest.begin(), // start of destination 19 | addValue<5,int>); // operation 20 | \end{lstlisting} 21 | 22 | 最后一个参数实例化函数模板addValue<>(),将5加到传入的int值上。对source中的每个元素调用生成的函数,同时将其转换为目标dest。 23 | 24 | 注意,必须为模板参数addValue<>()指定为int。推导仅适用于即时调用,而std::transform()需要完整的类型来推导其第四个参数的类型。不支持只替换/推导一些模板参数,并查看哪些参数适合,然后推导剩下的参数。 25 | 26 | 同样,也可以指定模板参数是从前面的参数推导出来的。例如,从传递的非类型派生返回类型: 27 | 28 | \begin{lstlisting}[style=styleCXX] 29 | template 30 | T foo(); 31 | \end{lstlisting} 32 | 33 | 或者确保传递的值与传递的类型相同: 34 | 35 | \begin{lstlisting}[style=styleCXX] 36 | template 37 | T bar(); 38 | \end{lstlisting} 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 | -------------------------------------------------------------------------------- /content/1/chapter3/3.tex: -------------------------------------------------------------------------------- 1 | 非类型模板参数有一些限制,只能是整型常量值(包括枚举),指向对象/函数/成员的指针,指向对象或函数的左值引用,或者std::nullptr\_t(nullptr的类型)。 2 | 3 | 浮点数和类型对象不允许作为非类型模板参数: 4 | 5 | \begin{lstlisting}[style=styleCXX] 6 | template // ERROR: floating-point values are not 7 | double process (double v) // allowed as template parameters 8 | { 9 | return v * VAT; 10 | } 11 | 12 | template // ERROR: class-type objects are not 13 | class MyClass { // allowed as template parameters 14 | ... 15 | }; 16 | \end{lstlisting} 17 | 18 | 当向指针或引用传递模板参数时,对象不能是字符串字面值、临时对象或数据成员和其他子对象。在C++17前,每个C++版本都放宽了这些限制,所以还有其他的限制: 19 | 20 | \begin{itemize} 21 | \item 22 | C++11中,对象必须具有外部链接。 23 | 24 | \item 25 | C++14中,对象必须具有外部或内部链接。 26 | \end{itemize} 27 | 28 | 因此,以下情况会报错: 29 | 30 | \begin{lstlisting}[style=styleCXX] 31 | template 32 | class Message { // OK 33 | ... 34 | }; 35 | 36 | Message<"hello"> x; // ERROR: string literal "hello" not allowed 37 | \end{lstlisting} 38 | 39 | 但也有解决方法(同样取决于C++的版本): 40 | 41 | \begin{lstlisting}[style=styleCXX] 42 | extern char const s03[] = "hi"; // external linkage 43 | char const s11[] = "hi"; // internal linkage 44 | int main() 45 | { 46 | Message m03; // OK (all versions) 47 | Message m11; // OK since C++11 48 | static char const s17[] = "hi"; // no linkage 49 | Message m17; // OK since C++17 50 | } 51 | \end{lstlisting} 52 | 53 | 三种情况下,常量字符数组都由"hi"初始化,该对象用作用char const*声明的模板参数。若对象具有外部链接(s03),这在所有C++版本中都有效;若对象具有内部链接(s11),则在C++11和C++14中也是有效的;若对象没有链接,则需要C++17的支持。 54 | 55 | 请参阅第12.3.3节和第17.2节讨论了该领域在未来变化的可能。 56 | 57 | \hspace*{\fill} \\ %插入空行 58 | \noindent 59 | \textbf{避免无效的表达式} 60 | 61 | 非类型模板参数可以是编译时表达式。例如: 62 | 63 | \begin{lstlisting}[style=styleCXX] 64 | template 65 | class C; 66 | ... 67 | C c; 68 | \end{lstlisting} 69 | 70 | 但是,若在表达式中使用>,必须将整个表达式放入圆括号中,以便编译器确定>在哪里结束: 71 | 72 | \begin{lstlisting}[style=styleCXX] 73 | C<42, sizeof(int) > 4> c; // ERROR: first > ends the template argument list 74 | C<42, (sizeof(int) > 4)> c; // OK 75 | \end{lstlisting} 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /content/1/chapter3/5.tex: -------------------------------------------------------------------------------- 1 | 2 | \begin{itemize} 3 | \item 4 | 模板的模板参数可以是值,而非类型。 5 | 6 | \item 7 | 不能将浮点数或类类型对象作为非类型模板的参数。对于指向字符串字面量、临时对象和子对象的指针/引用,有一些限制。 8 | 9 | \item 10 | 使用auto可使模板具有泛型值的非类型模板参数。 11 | \end{itemize} -------------------------------------------------------------------------------- /content/1/chapter4/0.tex: -------------------------------------------------------------------------------- 1 | C++11后,模板可以使用可变数量的模板参数。该特性允许在传递任意数量的类型参数的地方使用模板。典型的应用是通过类或框架传递任意数量的类型参数,另一个应用是提供泛型来处理任意数量的类型参数。 -------------------------------------------------------------------------------- /content/1/chapter4/3.tex: -------------------------------------------------------------------------------- 1 | 可变参数模板在实现通用库(如C++标准库)时扮演着重要的角色。 2 | 3 | 典型的应用是转发可变数量的类型参数。例如: 4 | 5 | \begin{itemize} 6 | \item 7 | 向指针中堆对象的构造函数传递参数: 8 | \begin{lstlisting}[style=styleCXX] 9 | // create shared pointer to complex initialized by 4.2 and 7.7: 10 | auto sp = std::make_shared>(4.2, 7.7); 11 | \end{lstlisting} 12 | 13 | \item 14 | 将参数传递给由线程库启动的线程: 15 | \begin{lstlisting}[style=styleCXX] 16 | std::thread t (foo, 42, "hello"); // call foo(42,"hello") in a separate thread 17 | \end{lstlisting} 18 | 19 | \item 20 | 将参数传递给进入vector中元素的构造函数: 21 | \begin{lstlisting}[style=styleCXX] 22 | std::vector v; 23 | ... 24 | v.emplace_back("Tim", "Jovi", 1962); // insert a Customer initialized by three arguments 25 | \end{lstlisting} 26 | 27 | \end{itemize} 28 | 29 | 通常,参数使用移动语义“完美转发”(见第6.1节),相应的声明为: 30 | 31 | \begin{lstlisting}[style=styleCXX] 32 | namespace std { 33 | template shared_ptr 34 | make_shared(Args&&... args); 35 | class thread { 36 | public: 37 | template 38 | explicit thread(F&& f, Args&&... args); 39 | ... 40 | }; 41 | 42 | template> 43 | class vector { 44 | public: 45 | template reference emplace_back(Args&&... args); 46 | ... 47 | }; 48 | } 49 | \end{lstlisting} 50 | 51 | 可变参数函数模板参数与普通参数适用相同的规则。通过值传递,参数复制并衰变(例如,数组变成指针),若通过引用传递,参数指向原始参数,而不衰变: 52 | 53 | \begin{lstlisting}[style=styleCXX] 54 | // args are copies with decayed types: 55 | template void foo (Args... args); 56 | // args are nondecayed references to passed objects: 57 | template void bar (Args const&... args); 58 | \end{lstlisting} 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 | -------------------------------------------------------------------------------- /content/1/chapter4/5.tex: -------------------------------------------------------------------------------- 1 | 2 | \begin{itemize} 3 | \item 4 | 通过使用参数包,可以为任意数量、类型的模板参数定义模板。 5 | 6 | \item 7 | 要处理参数,需要递归和/或匹配的非变参函数。 8 | 9 | \item 10 | 操作符sizeof...可为参数包提供的参数数量。 11 | 12 | \item 13 | 可变参数模板的一个应用是转发任意数量的类型参数。 14 | 15 | \item 16 | 通过使用折叠表达式,可以对参数包中的所有参数使用相应的操作符。 17 | \end{itemize} -------------------------------------------------------------------------------- /content/1/chapter5/0.tex: -------------------------------------------------------------------------------- 1 | 本章进一步介绍了与模板使用相关的一些基本知识:typename关键字,将成员函数和嵌套类定义为模板,双重模板参数,零值初始化,以及在函数模板中使用字符串字面值作为实参的一些细节。这些都很基础,每个开发者都应该了解一下。 -------------------------------------------------------------------------------- /content/1/chapter5/1.tex: -------------------------------------------------------------------------------- 1 | 关键字typename是在C++标准化过程中引入的,目的是说明模板内的标识符是类型。比如下面的例子: 2 | 3 | \begin{lstlisting}[style=styleCXX] 4 | template 5 | class MyClass { 6 | public: 7 | ... 8 | void foo() { 9 | typename T::SubType* ptr; 10 | } 11 | }; 12 | \end{lstlisting} 13 | 14 | 这里,第二个typename用于说明SubType是在类T中定义的类型。因此,ptr是指向类型T::SubType的指针。 15 | 16 | 若没有typename, SubType将假定为非类型成员(例如,静态数据成员或枚举数常量)。因此,表达式 17 | 18 | \begin{lstlisting}[style=styleCXX] 19 | T::SubType* ptr 20 | \end{lstlisting} 21 | 22 | 表示的是类T的静态SubType成员与ptr的乘积,这不是一个错误,因为对于MyClass<>的某些实例化,这可能有效。 23 | 24 | 当模板参数是类型时,必须使用typename。这将在第13.3.2节中详细讨论。 25 | 26 | typename的一种应用是在泛型代码中声明标准容器的迭代器: 27 | 28 | \noindent 29 | \textit{basics/printcoll.hpp} 30 | \begin{lstlisting}[style=styleCXX] 31 | #include 32 | 33 | // print elements of an STL container 34 | template 35 | void printcoll (T const& coll) 36 | { 37 | typename T::const_iterator pos; // iterator to iterate over coll 38 | typename T::const_iterator end(coll.end()); // end position 39 | for (pos=coll.begin(); pos!=end; ++pos) { 40 | std::cout << *pos << ' '; 41 | } 42 | std::cout << '\n'; 43 | } 44 | \end{lstlisting} 45 | 46 | 这个函数模板中,调用参数是一个T类型的标准容器。要遍历容器的所有元素,需要使用容器的迭代器类型,在每个标准容器类中声明为const\_iterator的类型: 47 | 48 | \begin{lstlisting}[style=styleCXX] 49 | class stlcontainer { 50 | public: 51 | using iterator = ...; // iterator for read/write access 52 | using const_iterator = ...; // iterator for read access 53 | ... 54 | }; 55 | \end{lstlisting} 56 | 57 | 因此,要访问模板类型T的const\_iterator类型,必须用typename进行限定: 58 | 59 | \begin{lstlisting}[style=styleCXX] 60 | typename T::const_iterator pos; 61 | \end{lstlisting} 62 | 63 | 有关在C++17前需要typename的更多细节,请参阅第13.3.2节。注意,C++20在许多常见情况下可能会删除对typename的需要(请参阅第17.1节了解详细信息)。 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /content/1/chapter5/2.tex: -------------------------------------------------------------------------------- 1 | 对于基本类型,如int、double或指针类型,没有默认构造函数可以初始化。相反,任何未初始化的局部变量都有一个未定义的值: 2 | 3 | \begin{lstlisting}[style=styleCXX] 4 | void foo() 5 | { 6 | int x; // x has undefined value 7 | int* ptr; // ptr points to anywhere (instead of nowhere) 8 | } 9 | \end{lstlisting} 10 | 11 | 若编写模板,并想让模板类型的变量用默认值初始化,就会遇到一个问题,简单的定义对内置类型并没有进行初始化: 12 | 13 | \begin{lstlisting}[style=styleCXX] 14 | template 15 | void foo() 16 | { 17 | T x; // x has undefined value if T is built-in type 18 | } 19 | \end{lstlisting} 20 | 21 | 因此,可以显式调用内置类型的默认构造函数,该构造函数用0初始化内置类型(bool为false,指针为nullptr)。因此,即使是内置类型,也可以通过编写以下代码来确保正确的初始化: 22 | 23 | \begin{lstlisting}[style=styleCXX] 24 | template 25 | void foo() 26 | { 27 | T x{}; // x is zero (or false or nullptr) if T is a built-in type 28 | } 29 | \end{lstlisting} 30 | 31 | 这种初始化方式称为值初始化,要么调用提供的构造函数,要么对对象进行零初始化。即使构造函数是显式的,也可以这样做。 32 | 33 | C++11前,正确初始化的语法是 34 | 35 | \begin{lstlisting}[style=styleCXX] 36 | T x = T(); // x is zero (or false or nullptr) if T is a built-in type 37 | \end{lstlisting} 38 | 39 | C++17前,这种机制(现在仍受支持)只有在为复制初始化选择的构造函数不是显式的情况下才有效。在C++17中,省略了强制复制,从而避免了这种限制,而且两种语法都有效。但若没有默认构造函数,带大括号的初始化表示法可以使用初始化列表构造函数。 40 | 41 | \begin{tcolorbox}[colback=webgreen!5!white,colframe=webgreen!75!black] 42 | \hspace*{0.75cm}对于某种类型X,有参数类型为std::initializer\_list的构造函数。 43 | \end{tcolorbox} 44 | 45 | 为了确保将类型参数化的类模板成员初始化,可以定义默认构造函数,使用带大括号的初始化式来初始化成员: 46 | 47 | \begin{lstlisting}[style=styleCXX] 48 | template 49 | class MyClass { 50 | private: 51 | T x; 52 | public: 53 | MyClass() : x{} { // ensures that x is initialized even for built-in types 54 | } 55 | ... 56 | }; 57 | \end{lstlisting} 58 | 59 | C++11前的语法 60 | 61 | \begin{lstlisting}[style=styleCXX] 62 | MyClass() : x() { // ensures that x is initialized even for built-in types 63 | } 64 | \end{lstlisting} 65 | 66 | 在后续标准中使用,仍然有效。 67 | 68 | C++11后,还可以为非静态成员提供默认初始化,这样也可以实现以下操作: 69 | 70 | \begin{lstlisting}[style=styleCXX] 71 | template 72 | class MyClass { 73 | private: 74 | T x{}; // zero-initialize x unless otherwise specified 75 | ... 76 | }; 77 | \end{lstlisting} 78 | 79 | 但是,请注意默认参数不能使用该语法。例如, 80 | 81 | \begin{lstlisting}[style=styleCXX] 82 | template 83 | void foo(T p{}) { // ERROR 84 | ... 85 | } 86 | \end{lstlisting} 87 | 88 | 必须这样写: 89 | 90 | \begin{lstlisting}[style=styleCXX] 91 | template 92 | void foo(T p = T{}) { // OK (must use T() before C++11) 93 | ... 94 | } 95 | \end{lstlisting} 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /content/1/chapter5/3.tex: -------------------------------------------------------------------------------- 1 | 对于具有依赖于模板参数的基类类模板,即使成员x被继承,使用名称x本身并不总是等同于this\texttt{->}x。例如: 2 | 3 | \begin{lstlisting}[style=styleCXX] 4 | template 5 | class Base { 6 | public: 7 | void bar(); 8 | }; 9 | 10 | template 11 | class Derived : Base { 12 | public: 13 | void foo() { 14 | bar(); // calls external bar() or error 15 | } 16 | }; 17 | \end{lstlisting} 18 | 19 | 本例中,解析foo()内部的符号bar,不会考虑Base中定义的bar()。因此,要么出现错误,要么调用另一个bar()实现(例如全局bar())。 20 | 21 | 我们将在第13.4.2节详细讨论这个问题。目前,建议始终对基类中声明的符号进行限定,这些符号在某种程度上依赖于模板参数this\texttt{->}或Base::。 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 | -------------------------------------------------------------------------------- /content/1/chapter5/8.tex: -------------------------------------------------------------------------------- 1 | 2 | \begin{itemize} 3 | \item 4 | 要访问依赖于模板参数的类型名称,必须使用typename对名称进行限定。 5 | 6 | \item 7 | 要访问依赖于模板参数的基类成员,必须通过this\texttt{->}或类名访问。 8 | 9 | \item 10 | 嵌套类和成员函数也可以是模板,一种应用是能够实现具有内部类型转换的泛型操作。 11 | 12 | \item 13 | 模板的构造函数或赋值操作符,不能替换预定义的构造函数或赋值操作符。 14 | 15 | \item 16 | 通过使用带大括号的初始化或显式调用默认构造函数,即使使用内置类型实例化,也可以使用默认值初始化模板的变量和成员。 17 | 18 | \item 19 | 可以为原始数组提供特定的模板,这些模板也可以应用于字符串字面量。 20 | 21 | \item 22 | 当传递原始数组或字符串字面量时,且参数不是引用时,类型在推导过程中会衰变(执行数组到指针的转换)。 23 | 24 | \item 25 | 可以定义变量模板(C++14起)。 26 | 27 | \item 28 | 也可以使用类模板作为模板参数,或作为双重模板参数。 29 | 30 | \item 31 | 模板参数必须精确匹配。 32 | \end{itemize} -------------------------------------------------------------------------------- /content/1/chapter6/0.tex: -------------------------------------------------------------------------------- 1 | C++11引入的最亮眼的特性是移动语义。通过将内部资源从源对象移动(“窃取”)到目标对象,而不是复制这些内容,可以使用它来优化复制和赋值。若原始值不再需要内部值或状态(因为即将丢弃),就可以移动。 2 | 3 | 移动语义对模板的设计有很重要的影响,在通用代码中需要引入特殊的规则来支持移动语义。本章将对这些特性进行介绍。 -------------------------------------------------------------------------------- /content/1/chapter6/3.tex: -------------------------------------------------------------------------------- 1 | 从C++11开始,标准库提供了辅助模板std::enable\_if<>,以在特定的编译时条件下忽略函数模板。 2 | 3 | 例如,函数模板foo<>()有如下定义: 4 | 5 | \begin{lstlisting}[style=styleCXX] 6 | template 7 | typename std::enable_if<(sizeof(T) > 4)>::type 8 | foo() { 9 | } 10 | \end{lstlisting} 11 | 12 | 如果sizeof(T) > 4生成false,则忽略foo<>()的定义。 13 | 14 | \begin{tcolorbox}[colback=webgreen!5!white,colframe=webgreen!75!black] 15 | \hspace*{0.75cm}不要忘记将条件放入圆括号中,否则条件中的>将视为模板参数列表的结束。 16 | \end{tcolorbox} 17 | 18 | 如果sizeof(T) > 4的结果为true,则函数模板实例展开为 19 | 20 | \begin{lstlisting}[style=styleCXX] 21 | void foo() { 22 | } 23 | \end{lstlisting} 24 | 25 | 也就是说,std::enable\_if<>是一种类型特征,计算作为其(第一个)模板参数传递的给定编译时表达式: 26 | 27 | \begin{itemize} 28 | \item 29 | 若表达式结果为true,其类型成员类型将产生一个类型: 30 | 31 | \begin{itemize} 32 | \item[-] 33 | 若没有传递第二个模板参数,则该类型为void。 34 | 35 | \item[-] 36 | 否则,该类型就是第二个模板参数类型。 37 | \end{itemize} 38 | 39 | \item 40 | 若表达式结果为false,则没有定义成员类型。由于名为SFINAE的模板特性(替换失败不为过)(请参阅8.4节),这将忽略使用enable\_if表达式的函数模板。 41 | \end{itemize} 42 | 43 | 对于自C++14产生类型的类型特征,有一个对应的别名模板std::enable\_if\_t<>,允许跳过typename和::type(请参阅第2.8节了解详细信息)。因此,从C++14起就使用 44 | 45 | \begin{lstlisting}[style=styleCXX] 46 | template 47 | std::enable_if_t<(sizeof(T) > 4)> 48 | foo() { 49 | } 50 | \end{lstlisting} 51 | 52 | 若第二个参数传递至enable\_if<>或enable\_if\_t<>: 53 | 54 | \begin{lstlisting}[style=styleCXX] 55 | template 56 | std::enable_if_t<(sizeof(T) > 4), T> 57 | foo() { 58 | return T(); 59 | } 60 | \end{lstlisting} 61 | 62 | 如果表达式为true,则enable\_if构造展开为第二个参数。因此,若MyType是传递或推导为T的具体类型,其大小大于4,则效果为 63 | 64 | \begin{lstlisting}[style=styleCXX] 65 | MyType foo(); 66 | \end{lstlisting} 67 | 68 | 在声明中间使用enable\_if表达式非常笨拙。由于这个原因,使用std::enable\_if<>的常见方法是使用带有默认值的函数模板参数: 69 | 70 | \begin{lstlisting}[style=styleCXX] 71 | template 4)>> 73 | void foo() { 74 | } 75 | \end{lstlisting} 76 | 77 | 其会扩展为 78 | 79 | \begin{lstlisting}[style=styleCXX] 80 | template 82 | void foo() { 83 | } 84 | \end{lstlisting} 85 | 86 | 当sizeof(T) > 4时。 87 | 88 | 若感觉还是太笨拙,并且想让需求/约束更明确,可以使用别名模板为它命名: 89 | 90 | \begin{lstlisting}[style=styleCXX] 91 | template 92 | using EnableIfSizeGreater4 = std::enable_if_t<(sizeof(T) > 4)>; 93 | 94 | template> 96 | void foo() { 97 | } 98 | \end{lstlisting} 99 | 100 | 请参阅第20.3节,以了解如何实现std::enable\_if。 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /content/1/chapter6/5.tex: -------------------------------------------------------------------------------- 1 | 因为它使用了一种变通方法,即使在使用别名模板时,enable\_if语法也相当笨拙:为了获得所需的效果,添加了一个的模板参数,并“滥用”该参数来提供函数模板可用的特定要求。这样的代码很难读懂,也使函数模板的其他部分难以理解。 2 | 3 | 原则上,只需要一种语言特性,允许制定函数的需求或约束,如果需求/约束没有得到满足,函数就会忽略。 4 | 5 | 这是期待已久的语言特性概念的应用,其允许用自己简单的语法制定模板的需求/条件。但尽管经过长时间的讨论,概念仍然没有成为C++17标准的一部分。然而,一些编译器提供了对这种特性的实验性支持,而且概念可能会成为C++17后的下一个标准的一部分。 6 | 7 | 对于概念,正如其作用,只需写下以下内容: 8 | 9 | \begin{lstlisting}[style=styleCXX] 10 | template 11 | requires std::is_convertible_v 12 | Person(STR&& n) : name(std::forward(n)) { 13 | ... 14 | } 15 | \end{lstlisting} 16 | 17 | 甚至可以将需求指定为一般概念 18 | 19 | \begin{lstlisting}[style=styleCXX] 20 | template 21 | concept ConvertibleToString = std::is_convertible_v; 22 | \end{lstlisting} 23 | 24 | 把这个概念表述为一种需求 25 | 26 | \begin{lstlisting}[style=styleCXX] 27 | template 28 | requires ConvertibleToString 29 | Person(STR&& n) : name(std::forward(n)) { 30 | ... 31 | } 32 | \end{lstlisting} 33 | 34 | 也可以这样表述: 35 | 36 | \begin{lstlisting}[style=styleCXX] 37 | template 38 | Person(STR&& n) : name(std::forward(n)) { 39 | ... 40 | } 41 | \end{lstlisting} 42 | 43 | 关于C++概念的详细讨论,请参阅附录E。 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /content/1/chapter6/6.tex: -------------------------------------------------------------------------------- 1 | 2 | \begin{itemize} 3 | \item 4 | 模板中,通过将参数声明为转发引用(声明为模板参数名称后跟\&\&形成的类型)并在转发调用中使用std::forward<>(),就可以“完美”地转发参数了。 5 | 6 | \item 7 | 使用完美转发成员函数模板时,可能会比预定义用于复制或移动对象的特殊成员函数更匹配。 8 | 9 | \item 10 | 使用std::enable\_if<>,可以在编译时条件为false时禁用函数模板(当条件确定,将忽略模板)。 11 | 12 | \item 13 | 通过std::enable\_if<>,可以避免为单个参数调用的构造函数模板,或赋值操作符模板,以及比隐式生成的特殊成员函数更好匹配的问题。 14 | 15 | \item 16 | 通过删除const volatile预定义的特殊成员函数,可以对特殊成员函数进行模板化(并应用enable\_if<>)。 17 | 18 | \item 19 | 概念允许对函数模板需求使用更直观的语法。 20 | \end{itemize} -------------------------------------------------------------------------------- /content/1/chapter7/0.tex: -------------------------------------------------------------------------------- 1 | 从一开始,C++就提供了按值和按引用调用,但要决定选择哪一种并不那么容易:通常按引用调用对于重要的对象来说成本更低,但更复杂。C++11添加了移动语义,现在有了不同的通过引用传递的方式: 2 | 3 | \begin{tcolorbox}[colback=webgreen!5!white,colframe=webgreen!75!black] 4 | \hspace*{0.75cm}常量右值引用X const\&\&也可以,但没有既定的语义。 5 | \end{tcolorbox} 6 | 7 | \begin{enumerate} 8 | \item 9 | X const\& (常量左值引用): 10 | 11 | 参数引用传递的对象,但不能修改。 12 | 13 | \item 14 | X\& (非常数的左值引用): 15 | 16 | 参数引用传递的对象,并能够修改。 17 | 18 | \item 19 | X\&\& (右值引用): 20 | 21 | 参数引用传递的对象,带有移动语义,可以修改或“窃取”值。 22 | \end{enumerate} 23 | 24 | 决定如何声明已知具体类型的参数已经够复杂的了。模板中类型是未知的,因此很难决定哪种传递机制更为合适。 25 | 26 | 第1.6.1节中,我们确实建议在函数模板中按值传递参数,除非有很好的理由: 27 | 28 | \begin{itemize} 29 | \item 30 | 不能复制 31 | 32 | \begin{tcolorbox}[colback=webgreen!5!white,colframe=webgreen!75!black] 33 | \hspace*{0.75cm}由于C++17,即使没有可用的复制或移动构造函数,也可以通过值传递临时实体(右值)(参见B.2.1节)。因此,由于C++17的约束,不可能复制左值。 34 | \end{tcolorbox} 35 | 36 | \item 37 | 参数用于返回数据。 38 | 39 | \item 40 | 模板只是通过保留原始参数的所有属性,将参数转发到其他地方 41 | 42 | \item 43 | 有显著的性能改进 44 | \end{itemize} 45 | 46 | 本章讨论了在模板中声明参数的不同方法,提出了通过值传递参数的建议,并为不这样做的原因提供了参数。本文还讨论了在处理字符串字面值和其他数组时遇到的棘手问题。 47 | 48 | 阅读本章时,熟悉值类别(lvalue、rvalue、prvalue、xvalue等)的术语对理解本章内容会有帮助,这些在附录B中都有解释。 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 | -------------------------------------------------------------------------------- /content/1/chapter7/1.tex: -------------------------------------------------------------------------------- 1 | 按值传递参数时,原则上必须复制每个参数,每个参数都成为所传递实参的副本。对于类,作为副本创建的对象通常由复制构造函数初始化。 2 | 3 | 调用复制构造函数的代价可能会很高。然而,即使在按值传递参数时,也有方法来避免复制:编译器可能会优化复制对象的复制操作,并且通过移动语义,对复杂对象的操作也可以变得廉价。 4 | 5 | 来看一个实现的简单函数模板,参数通过值传递: 6 | 7 | \begin{lstlisting}[style=styleCXX] 8 | template 9 | void printV (T arg) { 10 | ... 11 | } 12 | \end{lstlisting} 13 | 14 | 为整数调用函数模板时,结果代码为 15 | 16 | \begin{lstlisting}[style=styleCXX] 17 | void printV (int arg) { 18 | ... 19 | } 20 | \end{lstlisting} 21 | 22 | 参数arg成为传入参数的副本,无论它是对象、文字还是函数返回的值。 23 | 24 | 若定义一个std::string类型,并为此调用函数模板: 25 | 26 | \begin{lstlisting}[style=styleCXX] 27 | std::string s = "hi"; 28 | printV(s); 29 | \end{lstlisting} 30 | 31 | 模板参数T实例化为std::string,得到 32 | 33 | \begin{lstlisting}[style=styleCXX] 34 | void printV (std::string arg) 35 | { 36 | ... 37 | } 38 | \end{lstlisting} 39 | 40 | 传递字符串时,arg变成了s的副本。这次的副本是由string类的复制构造函数创建的,这是一个昂贵的操作,因为这个复制操作创建了一个完整的“深”副本,以便该副本内部分配自己的内存来保存该值。 41 | 42 | \begin{tcolorbox}[colback=webgreen!5!white,colframe=webgreen!75!black] 43 | \hspace*{0.75cm}string类的实现本身可能进行了一些优化,以降低复制成本。一种是小字符串优化(SSO),只要值不太长,就直接使用对象内部的一些内存来保存值,而不分配内存。另一种是写时复制优化,只要源文件和副本都没有修改,就会使用与源文件相同的内存创建副本。但是,写时复制优化在多线程代码中有明显的缺陷。由于这个原因,C++11的标准字符串是禁止使用写时复制优化。 44 | \end{tcolorbox} 45 | 46 | 但是,复制构造函数并不总调用。考虑以下代码: 47 | 48 | \begin{lstlisting}[style=styleCXX] 49 | std::string returnString(); 50 | std::string s = "hi"; 51 | printV(s); // copy constructor 52 | printV(std::string("hi")); // copying usually optimized away (if not, move constructor) 53 | printV(returnString()); // copying usually optimized away (if not, move constructor) 54 | printV(std::move(s)); // move constructor 55 | \end{lstlisting} 56 | 57 | 第一次调用中,传递了一个左值,使用了复制构造函数。然而,第二次和第三次调用中,当直接调用函数模板来获取prvalues(动态创建或由另一个函数返回的临时对象;参见附录B),编译器通常会优化传递参数,这样就不会调用复制构造函数了。C++17起,这种优化是必需的。C++17前,不能优化复制的编译器,至少需要使用移动语义,这通常会使复制成本降低。最后一次调用中,当传递xvalue(一个现有的带有std::move()的非常量对象)时,不再需要s的值来强制使用移动构造函数。 58 | 59 | 因此,可以调用printV()的实现,来声明按值传递的参数,通常只在传递左值(之前创建的对象,因为没有使用std::move()来传递它,所以在之后仍然可用)时才会很昂贵。不幸的是,这是一个常见的情况。在早期创建对象时,在稍后(经过一些修改)将其传递给其他函数的是常规操作。 60 | 61 | \hspace*{\fill} \\ %插入空行 62 | \noindent 63 | \textbf{值传递的类型衰变} 64 | 65 | 还有一个按值传递的属性:当按值传递参数时,类型会衰变。从而数组将转换为指针,并删除const和volatile等限定符(就像使用值作为使用auto声明的对象的初始化式一样): 66 | 67 | \begin{tcolorbox}[colback=webgreen!5!white,colframe=webgreen!75!black] 68 | \hspace*{0.75cm}术语衰变来自C语言,也适用于从函数到函数指针的类型转换(参见11.1.1节)。 69 | \end{tcolorbox} 70 | 71 | \begin{lstlisting}[style=styleCXX] 72 | template 73 | void printV (T arg) { 74 | ... 75 | } 76 | 77 | std::string const c = "hi"; 78 | printV(c); // c decays so that arg has type std::string 79 | 80 | printV("hi"); // decays to pointer so that arg has type char const* 81 | 82 | int arr[4]; 83 | printV(arr); // decays to pointer so that arg has type int* 84 | \end{lstlisting} 85 | 86 | 因此,当传递字符串字面值"hi"时,类型char const[3]衰变为char const*,从而成为T的推导类型。 87 | 88 | \begin{lstlisting}[style=styleCXX] 89 | void printV (char const* arg) 90 | { 91 | ... 92 | } 93 | \end{lstlisting} 94 | 95 | 这种行为有利有弊,简化了对传递的字符串字面量的处理,但缺点是在printV()中,无法区分传递单个元素的指针和传递数组。因此,将在7.4节中将讨论如何处理字符串字面值和其他数组。 96 | 97 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /content/1/chapter7/3.tex: -------------------------------------------------------------------------------- 1 | C++11起,可以让调用者决定函数模板参数是通过值传递,还是通过引用传递。当模板声明为按值接受参数时,调用者可以使用在头文件中声明的std::cref()和std::ref(),通过引用传递参数。例如: 2 | 3 | \begin{lstlisting}[style=styleCXX] 4 | template 5 | void printT (T arg) { 6 | ... 7 | } 8 | 9 | std::string s = "hello"; 10 | printT(s); // pass s by value 11 | printT(std::cref(s)); // pass s “as if by reference” 12 | \end{lstlisting} 13 | 14 | 注意std::cref()不会改变模板中参数的处理,之类使用了一个技巧:用一个引用的对象来包装传递的参数。事实上,这会创建了一个std::reference\_wrapper<>类型的对象,引用原始参数,并按值传递了这个对象。包装器或多或少支持一种操作:返回原始类型的隐式类型转换,生成原始对象。 15 | 16 | \begin{tcolorbox}[colback=webgreen!5!white,colframe=webgreen!75!black] 17 | \hspace*{0.75cm}还可以在引用包装器上调用get()并将其用作函数对象。 18 | \end{tcolorbox} 19 | 20 | 因此,只要对传递的对象有有效的操作符,就可以使用引用包装器。例如: 21 | 22 | \hspace*{\fill} \\ %插入空行 23 | \noindent 24 | \textit{basics/cref.cpp} 25 | \begin{lstlisting}[style=styleCXX] 26 | #include // for std::cref() 27 | #include 28 | #include 29 | 30 | void printString(std::string const& s) 31 | { 32 | std::cout << s << ’\n’; 33 | } 34 | 35 | template 36 | void printT (T arg) 37 | { 38 | printString(arg); // might convert arg back to std::string 39 | } 40 | 41 | int main() 42 | { 43 | std::string s = "hello"; 44 | printT(s); // print s passed by value 45 | printT(std::cref(s)); // print s passed “as if by reference” 46 | } 47 | \end{lstlisting} 48 | 49 | 最后一次调用通过值传递std::reference\_wrapper类型的对象到参数arg,然后传递并转换回其底层类型std::string。 50 | 51 | 编译器必须知道返回原始类型必要的隐式转换。因此,只有通过泛型代码将对象传递给非泛型函数时,std::ref()和std::cref()才能正常工作。例如,因为没有为std::reference\_wrapper<>定义输出操作符,直接尝试输出传递的泛型类型T对象将失败: 52 | 53 | \begin{lstlisting}[style=styleCXX] 54 | template 55 | void printV (T arg) { 56 | std::cout << arg << ’\n’; 57 | } 58 | ... 59 | std::string s = "hello"; 60 | printV(s); // OK 61 | printV(std::cref(s)); // ERROR: no operator << for reference wrapper defined 62 | \end{lstlisting} 63 | 64 | 此外,因为不能比较一个引用包装与char const*或std::string,所以会失败: 65 | 66 | \begin{lstlisting}[style=styleCXX] 67 | template 68 | bool isless(T1 arg1, T2 arg2) 69 | { 70 | return arg1 < arg2; 71 | } 72 | ... 73 | std::string s = "hello"; 74 | if (isless(std::cref(s), "world")) ... // ERROR 75 | if (isless(std::cref(s), std::string("world"))) ... // ERROR 76 | \end{lstlisting} 77 | 78 | 将arg1和arg2声明为通用类型T也没有帮助: 79 | 80 | \begin{lstlisting}[style=styleCXX] 81 | template 82 | bool isless(T arg1, T arg2) 83 | { 84 | return arg1 < arg2; 85 | } 86 | \end{lstlisting} 87 | 88 | 因为当编译器试图为arg1和arg2推导T时,类型会冲突。 89 | 90 | 因此,类std::reference\_wrapper<>的作用是将引用用作“第一个类对象”,可以复制,并通过值传递给函数模板。也可以在类中使用它,例如:保存容器中对象的引用。但是最终需要转换回基础类型。 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /content/1/chapter7/4.tex: -------------------------------------------------------------------------------- 1 | 2 | 已经看到了模板参数在使用字符串字面值和数组时不同的效果: 3 | 4 | \begin{itemize} 5 | \item 6 | 按值调用会衰变,使其成为指向元素类型的指针。 7 | 8 | \item 9 | 引用调用都不会衰变,因此参数成为仍然是数组。 10 | \end{itemize} 11 | 12 | 两者都可能是可工作的,也可能是不工作的。当数组衰变为指针时,将失去区分处理指向元素的指针和处理传递的数组的能力。另一方面,处理可能传递字符串字面值的参数时,因为不同大小的字符串字面值有不同的类型,所以不衰变可能有问题。例如: 13 | 14 | \begin{lstlisting}[style=styleCXX] 15 | template 16 | void foo (T const& arg1, T const& arg2) 17 | { 18 | ... 19 | } 20 | 21 | foo("hi", "guy"); // ERROR 22 | \end{lstlisting} 23 | 24 | 这里,foo("hi","guy")无法进行编译,因为"hi"的类型是char const[3],而"guy"的类型是char const[4],但模板要求它们具有相同的类型T。只有当字符串字面值具有相同的长度时,这样的代码才能编译。出于这个原因,强烈建议在测试用例中使用不同长度的字符串字面值。 25 | 26 | 通过声明函数模板foo()来通过值传递参数: 27 | 28 | \begin{lstlisting}[style=styleCXX] 29 | template 30 | void foo (T arg1, T arg2) 31 | { 32 | ... 33 | } 34 | 35 | foo("hi", "guy"); // compiles, but ... 36 | \end{lstlisting} 37 | 38 | 但是,这并不意味着所有的问题都消失了。更糟糕的是,编译时问题可能变成了运行时问题。考虑下面的代码,使用operator==比较传递的参数: 39 | 40 | \begin{lstlisting}[style=styleCXX] 41 | template 42 | void foo (T arg1, T arg2) 43 | { 44 | if (arg1 == arg2) { // OOPS: compares addresses of passed arrays 45 | ... 46 | } 47 | } 48 | 49 | foo("hi", "guy"); // compiles, but ... 50 | \end{lstlisting} 51 | 52 | 因为模板还必须处理来自已衰变的字符串字面量参数(例如,通过value调用的函数或赋值给使用auto声明的对象),所以编译器必须知道应该将传递的字符指针解释为字符串。 53 | 54 | 许多情况下衰变是有用的,特别是用于检查两个对象(都作为参数传递,或者一个作为传递参数,另一个作为期待参数)是否具有或转换为相同的类型。典型的用法是完美转发,但想使用完美转发,必须将参数声明为转发引用。这种情况下,可以使用类型特征std::decay<>()显式地衰变参数。具体的例子请参阅第120页7.6节中的std::make\_pair()。 55 | 56 | 其他类型特征有时也隐式衰变,例如std::common\_type<>,会产生两个传递参数类型的通用类型(参见章节1.3.3和章节D.5)。 57 | 58 | \subsubsubsection{7.4.1\hspace{0.2cm}字符串字面值和数组的特殊实现} 59 | 60 | 可能需要根据传递的是指针,还是数组来区分实现。当然,这要求传递的数组没有衰变。 61 | 62 | 要区分这些情况,必须检测是否传递了数组。基本上有两种选择: 63 | 64 | \begin{itemize} 65 | \item 66 | 可以声明模板参数,使其只对数组有效: 67 | 68 | \begin{lstlisting}[style=styleCXX] 69 | template 70 | void foo(T (&arg1)[L1], T (&arg2)[L2]) 71 | { 72 | T* pa = arg1; // decay arg1 73 | T* pb = arg2; // decay arg2 74 | if (compareArrays(pa, L1, pb, L2)) { 75 | ... 76 | } 77 | } 78 | \end{lstlisting} 79 | 80 | 这里,arg1和arg2必须是数组,具有相同的元素类型T,但L1和L2大小不同。但请注意,可能需要多个实现来支持数组(参见第5.4节)。 81 | 82 | \item 83 | 可以使用类型特征来检测是否传递了数组(或指针): 84 | 85 | \begin{lstlisting}[style=styleCXX] 86 | template>> 88 | void foo (T&& arg1, T&& arg2) 89 | { 90 | ... 91 | } 92 | \end{lstlisting} 93 | \end{itemize} 94 | 95 | 由于这些属于特殊的处理方式,而常用处理数组方式就是使用不同的函数名。当然,更好的方法是确定模板的调用者使用std::vector或std::array。只要字符串字面值是数组,就必须考虑这种情况。 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /content/1/chapter7/5.tex: -------------------------------------------------------------------------------- 1 | 2 | 3 | 对于返回值,还可以决定是通过值返回,还是通过引用返回。但返回引用可能是麻烦的来源,因为引用的东西超出了控制范围。一些情况下,返回引用是常见的编程实践: 4 | 5 | \begin{itemize} 6 | \item 7 | 返回容器或字符串元素(例如,通过operator[]或front()) 8 | 9 | \item 10 | 授予类成员写访问权限 11 | 12 | \item 13 | 返回链式调用的对象(流的operator<{}<和operator>{}>,类对象的operator=) 14 | \end{itemize} 15 | 16 | 此外,通过返回const引用来授予成员读权限。 17 | 18 | 若使用不当,可能会产生麻烦。例如: 19 | 20 | \begin{lstlisting}[style=styleCXX] 21 | std::string* s = new std::string("whatever"); 22 | auto& c = (*s)[0]; 23 | delete s; 24 | std::cout << c; // run-time ERROR 25 | \end{lstlisting} 26 | 27 | 这里,获得了一个字符串元素的引用,但是当使用这个引用时,底层字符串已经不存在了(创建了一个悬空引用),并且出现了未定义行为。这个例子有些做作(有经验的程序员可能马上就会注意到问题),但是事情可能会变得不那么明显。例如: 28 | 29 | \begin{lstlisting}[style=styleCXX] 30 | auto s = std::make_shared("whatever"); 31 | auto& c = (*s)[0]; 32 | s.reset(); 33 | std::cout << c; // run-time ERROR 34 | \end{lstlisting} 35 | 36 | 因此,应该确保函数模板按值返回结果。正如本章所讨论的,使用模板参数T并不能保证它不是引用,因为T有时可能会隐式推导为引用: 37 | 38 | \begin{lstlisting}[style=styleCXX] 39 | template 40 | T retR(T&& p) // p is a forwarding reference 41 | { 42 | return T{...}; // OOPS: returns by reference when called for lvalues 43 | } 44 | \end{lstlisting} 45 | 46 | 即使T是由按值调用推导而来的模板参数,当显式指定模板参数为引用时,也可能成为引用类型: 47 | 48 | \begin{lstlisting}[style=styleCXX] 49 | template 50 | T retV(T p) // Note: T might become a reference 51 | { 52 | return T{...}; // OOPS: returns a reference if T is a reference 53 | } 54 | 55 | int x; 56 | retV(x); // retT() instantiated for T as int& 57 | \end{lstlisting} 58 | 59 | 安全起见,这里有两个选择: 60 | 61 | \begin{itemize} 62 | \item 63 | 使用类型特征std::remove\_reference<>(参见D.4节)将类型T转换为非引用: 64 | 65 | \begin{lstlisting}[style=styleCXX] 66 | template 67 | typename std::remove_reference::type retV(T p) 68 | { 69 | return T{...}; // always returns by value 70 | } 71 | \end{lstlisting} 72 | 73 | 其他特征,如std::decay<>(参见D.4节),因为隐式地移除引用,在这里也可能会有用。 74 | 75 | \item 76 | 编译器通过声明返回类型auto来推断返回类型(C++14起;参见第1.3.2节),因为auto总会衰变: 77 | 78 | \begin{lstlisting}[style=styleCXX] 79 | template 80 | auto retV(T p) // by-value return type deduced by compiler 81 | { 82 | return T{...}; // always returns by value 83 | } 84 | \end{lstlisting} 85 | 86 | \end{itemize} 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /content/1/chapter7/6.tex: -------------------------------------------------------------------------------- 1 | 正如前几节所述,声明依赖于模板参数类型有不同的方式: 2 | 3 | \begin{itemize} 4 | \item 5 | 通过值传递参数: 6 | 7 | 这种方法很简单,衰变字符串字面值和数组,但不能为大型对象提供最佳性能。调用者可以决定使用std::cref()和std::ref()通过引用传递,但是必须确定这样做的必要性。 8 | 9 | \item 10 | 通过引用传递参数: 11 | 12 | 这种方法通常可以为大型对象提供更好的性能,特别是在传递参数时 13 | 14 | \begin{itemize} 15 | \item[-] 16 | 现有对象(lvalue)到左值引用, 17 | 18 | \item[-] 19 | 临时对象(prvalue)或标记为可移动(xvalue)的对象将引用右值, 20 | 21 | \item[-] 22 | 或者两者都为转发引用。 23 | \end{itemize} 24 | 25 | 这些情况下,参数都不会衰变,所以在传递字符串字面值和其他数组时,可能需要特别注意。对于转发引用,还必须注意使用模板参数隐式推导出引用类型的方法。 26 | \end{itemize} 27 | 28 | \hspace*{\fill} \\ %插入空行 29 | \noindent 30 | \textbf{不要过于泛化} 31 | 32 | 实践中,函数模板通常不支持任意类型的参数,可以进行了一些约束。例如,可能知道只传递某种类型的vector。这种情况下,最好不要太泛化地声明这样的函数。如前所述,这可能会出现令人惊讶的副作用,可以使用以下声明: 33 | 34 | \begin{lstlisting}[style=styleCXX] 35 | template 36 | void printVector (std::vector const& v) 37 | { 38 | ... 39 | } 40 | \end{lstlisting} 41 | 42 | 通过在printVector()中声明参数v,可以确定传递的T不能成为引用,因为vector不能使用引用作为元素类型。另外,因为std::vector<>的复制构造函数会创建元素副本,所以按值传递vector的成本很高。出于这个原因,将这样的vector参数声明为按值传递可能不合适。若将参数v的声明交由类型T来决定,那么按值调用和按引用调用之间的区别就不那么明显了。 43 | 44 | \hspace*{\fill} \\ %插入空行 45 | \noindent 46 | \textbf{std::make\_pair()的实例} 47 | 48 | std::make\_pair<>()是一个很好的例子,演示了决定参数传递机制的缺陷。C++标准库中,可以使用其进行类型推导,并创建std::pair<>对象(一个方便的函数模板)。它的声明在不同版本的C++标准中有所不同: 49 | 50 | \begin{itemize} 51 | \item 52 | C++98中,make\_pair<>()在命名空间std中声明,使用引用调用来避免不必要的复制: 53 | 54 | \begin{lstlisting}[style=styleCXX] 55 | template 56 | pair make_pair (T1 const& a, T2 const& b) 57 | { 58 | return pair(a,b); 59 | } 60 | \end{lstlisting} 61 | 62 | 然而,使用字符串字面值对或不同大小的数组时,这会导致严重的问题。 63 | 64 | \begin{tcolorbox}[colback=webgreen!5!white,colframe=webgreen!75!black] 65 | \hspace*{0.75cm}请参见C++库的181号issue[LibIssue181] 66 | \end{tcolorbox} 67 | 68 | \item 69 | C++03中,函数定义改为使用按值调用: 70 | 71 | \begin{lstlisting}[style=styleCXX] 72 | template 73 | pair make_pair (T1 a, T2 b) 74 | { 75 | return pair(a,b); 76 | } 77 | \end{lstlisting} 78 | 79 | 如同在问题解决方案的基本原理中所了解到的那样,“与其他两个建议相比,这似乎是对标准的一个小修改,而且任何效率方面的担忧都由该解决方案的优点所抵消。” 80 | 81 | \item[-] 82 | C++11中,make\_pair()必须支持移动语义,因此参数必须成为转发引用。因此,该定义又发生了如下变化: 83 | 84 | \begin{lstlisting}[style=styleCXX] 85 | template 86 | constexpr pair::type, typename decay::type> 87 | make_pair (T1&& a, T2&& b) 88 | { 89 | return pair::type, 90 | typename decay::type>(forward(a), 91 | forward(b)); 92 | } 93 | \end{lstlisting} 94 | 95 | 完整的实现甚至更加复杂:为了支持std::ref()和std::cref(),该函数还将std::reference\_wrapper的实例展开为实际的引用。 96 | \end{itemize} 97 | 98 | C++标准库现在在许多地方以类似的方式完美地转发传递的参数,并与std::decay<>一起使用。 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /content/1/chapter7/7.tex: -------------------------------------------------------------------------------- 1 | 2 | \begin{itemize} 3 | \item 4 | 测试模板时,可以使用不同长度的字符串字面值。 5 | 6 | \item 7 | 通过值传递的模板参数会衰变,而通过引用传递的模板参数不会衰变。 8 | 9 | \item 10 | 类型特征std::decay<>允许在引用传递的模板中衰变参数。 11 | 12 | \item 13 | 某些情况下,函数模板声明参数通过值传递时,允许通过std::cref()和std::ref()传递参数的引用。 14 | 15 | \item 16 | 按值传递模板参数很简单,但可能无法获得最佳性能。 17 | 18 | \item 19 | 按值传递参数给函数模板,除非有很好的理由不这样做。 20 | 21 | \item 22 | 确保返回值通常按值传递(模板参数不能直接指定返回类型)。 23 | 24 | \item 25 | 有时需要衡量性能。不要依赖直觉,因为直觉很可能是错的。 26 | \end{itemize} -------------------------------------------------------------------------------- /content/1/chapter8/0.tex: -------------------------------------------------------------------------------- 1 | C++有一些在编译时计算的方法。模板添加了更多编译时计算的方法,而语言的发展也对此进行了增强。 2 | 3 | 并且可以决定是否使用某些模板代码,或者在不同的模板代码之间进行选择。但若所有必要的输入都可用,编译器可以在编译时计算控制流的结果。 4 | 5 | 事实上,C++可以通过多种特性来支持编译时编程: 6 | 7 | \begin{itemize} 8 | \item 9 | C++98前,模板提供了编译时计算的能力,包括使用循环和执行路径选择(因为使用了非直观的语法,所以有些人认为这是对模板特性的“滥用”)。 10 | 11 | \item 12 | 使用偏特化,可以在编译时根据约束或要求在不同的类模板实现之间进行选择。 13 | 14 | \item 15 | 使用SFINAE,可以针对类型或约束在函数模板实现之间进行选择。 16 | 17 | \item 18 | C++11和C++14中,通过使用“直观的”执行路径选择,以及自C++14后的大多数语句类型(包括for循环、switch语句等)的constexpr特性,编译时计算得到了更好的支持。 19 | 20 | \item 21 | C++17引入了“编译时if”解除依赖于编译时条件或约束的语句,其甚至可以在模板外工作。 22 | \end{itemize} 23 | 24 | 本章将介绍这些特性,特别关注模板的角色和上下文。 25 | 26 | 27 | -------------------------------------------------------------------------------- /content/1/chapter8/1.tex: -------------------------------------------------------------------------------- 1 | 模板在编译时实例化(与动态语言相反,动态语言在运行时处理泛型)。C++模板的一些特性可以与实例化过程结合,成为一种递归“编程语言”。 2 | 3 | \begin{tcolorbox}[colback=webgreen!5!white,colframe=webgreen!75!black] 4 | \hspace*{0.75cm}Erwin Unruh通过在编译时计算素数,成为第一个发现编译时计算的人。详见第23.7节。 5 | \end{tcolorbox} 6 | 7 | 因此,模板可以用来“计算”。第23章将详细介绍所有功能,这里只是举一个简单的例子。 8 | 9 | 下面的代码,在编译时找出给定的数字是否为素数: 10 | 11 | \hspace*{\fill} \\ %插入空行 12 | \noindent 13 | \textit{basics/isprime.hpp} 14 | \begin{lstlisting}[style=styleCXX] 15 | template // p: number to check, d: current divisor 16 | struct DoIsPrime { 17 | static constexpr bool value = (p%d != 0) && DoIsPrime::value; 18 | }; 19 | 20 | template // end recursion if divisor is 2 21 | struct DoIsPrime { 22 | static constexpr bool value = (p%2 != 0); 23 | }; 24 | 25 | template // primary template 26 | struct IsPrime { 27 | // start recursion with divisor from p/2: 28 | static constexpr bool value = DoIsPrime::value; 29 | }; 30 | 31 | // special cases (to avoid endless recursion with template instantiation): 32 | template<> 33 | struct IsPrime<0> { static constexpr bool value = false; }; 34 | template<> 35 | struct IsPrime<1> { static constexpr bool value = false; }; 36 | template<> 37 | struct IsPrime<2> { static constexpr bool value = true; }; 38 | template<> 39 | struct IsPrime<3> { static constexpr bool value = true; }; 40 | \end{lstlisting} 41 | 42 | IsPrime<>模板返回成员值,无论传递的模板参数p是否是一个素数。为了实现,实例化DoIsPrime<>,其递归地展开为一个表达式,检查p/2和2之间的每个除数d是否能整除p。 43 | 44 | 例如,表达式为 45 | 46 | \begin{lstlisting}[style=styleCXX] 47 | IsPrime<9>::value 48 | \end{lstlisting} 49 | 50 | 扩展为 51 | 52 | \begin{lstlisting}[style=styleCXX] 53 | DoIsPrime<9,4>::value 54 | \end{lstlisting} 55 | 56 | 继续扩展 57 | 58 | \begin{lstlisting}[style=styleCXX] 59 | 9%4!=0 && DoIsPrime<9,3>::value 60 | \end{lstlisting} 61 | 62 | 继续扩展 63 | 64 | \begin{lstlisting}[style=styleCXX] 65 | 9%4!=0 && 9%3!=0 && DoIsPrime<9,2>::value 66 | \end{lstlisting} 67 | 68 | 继续扩展 69 | 70 | \begin{lstlisting}[style=styleCXX] 71 | 9%4!=0 && 9%3!=0 && 9%2!=0 72 | \end{lstlisting} 73 | 74 | 计算结果为false,因为9\%3是0。 75 | 76 | 正如这个实例链所示: 77 | 78 | \begin{itemize} 79 | \item 80 | 使用递归展开DoIsPrime<>来遍历从p/2到2的所有除数,以确定这些除数是否能整除给定整数。 81 | 82 | \item 83 | 当d等于2时,DoIsPrime<>的偏特化作为结束递归。 84 | \end{itemize} 85 | 86 | 注意,所有这些都在编译时完成。也就是说, 87 | 88 | \begin{lstlisting}[style=styleCXX] 89 | IsPrime<9>::value 90 | \end{lstlisting} 91 | 92 | 在编译时展开为false。 93 | 94 | 模板语法可以说是笨拙的,但类似的代码自C++98(或更早)就一直有效,并且可以提高运行库的效率。 95 | 96 | \begin{tcolorbox}[colback=webgreen!5!white,colframe=webgreen!75!black] 97 | \hspace*{0.75cm}C++11前,通常将值成员声明为枚举数常量,而不是静态数据成员,以避免静态数据成员需要在类外定义(参见第23.6节了解详细信息)。例如: 98 | \begin{lstlisting}[style=styleCXX] 99 | enum f value = (p%d != 0) && DoIsPrime::value g; 100 | \end{lstlisting} 101 | \end{tcolorbox} 102 | 103 | 详见第23章。 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 | -------------------------------------------------------------------------------- /content/1/chapter8/2.tex: -------------------------------------------------------------------------------- 1 | C++11引入了一个新特性constexpr,简化了各种形式的编译时计算。特别是,如果有输入适当,可以在编译时对constexpr函数求值。虽然在C++11中引入constexpr函数有严格限制(每个constexpr函数本质上都需要包含一个return语句),但在C++14中,这些限制大部分都取消了。当然,成功地计算constexpr函数仍然需要所有的计算步骤,在编译时是可行和有效的:这排除了堆分配或抛出异常的情况。 2 | 3 | 我们测试一个数字是否是素数的例子,可以使用C++11进行如下实现: 4 | 5 | \hspace*{\fill} \\ %插入空行 6 | \noindent 7 | \textit{basics/isprime11.hpp} 8 | \begin{lstlisting}[style=styleCXX] 9 | constexpr bool 10 | doIsPrime (unsigned p, unsigned d) // p: number to check, d: current divisor 11 | { 12 | return d!=2 ? (p%d!=0) && doIsPrime(p,d-1) // check this and smaller divisors 13 | : (p%2!=0); // end recursion if divisor is 2 14 | } 15 | 16 | constexpr bool isPrime (unsigned p) 17 | { 18 | return p < 4 ? !(p<2) // handle special cases 19 | : doIsPrime(p,p/2); // start recursion with divisor from p/2 20 | } 21 | \end{lstlisting} 22 | 23 | 由于只有一条语句的限制,只能使用条件操作符作为选择机制,并且需要递归来遍历元素。但其语法是普通的C++函数代码,这使得它比依赖于模板实例化的第一个版本更容易使用。 24 | 25 | C++14中,constexpr函数可以使用通用C++代码中的控制结构。因此,不用编写笨拙的模板代码或有些“奇怪的”单行程序,现在只使用普通的for循环: 26 | 27 | \hspace*{\fill} \\ %插入空行 28 | \noindent 29 | \textit{basics/isprime14.hpp} 30 | \begin{lstlisting}[style=styleCXX] 31 | constexpr bool isPrime (unsigned int p) 32 | { 33 | for (unsigned int d=2; d<=p/2; ++d) { 34 | if (p % d == 0) { 35 | return false; // found divisor without remainder 36 | } 37 | } 38 | return p > 1; // no divisor without remainder found 39 | } 40 | \end{lstlisting} 41 | 42 | 使用C++11和C++14版本的constexpr isPrime()实现,可以直接调用 43 | 44 | \begin{lstlisting}[style=styleCXX] 45 | isPrime(9) 46 | \end{lstlisting} 47 | 48 | 找出9是否为质数。可以在编译时这样做,但不一定要这样做。在需要编译时值的上下文中(例如,数组长度或非类型模板参数),编译器将尝试在编译时计算对constexpr函数的调用。若无法计算,则会产生错误(因为最后必须生成一个常量)。其他上下文中,编译器在编译时可能尝试或不尝试求值,但若这样的求值失败,是不会产生错误信息,而是将问题留给运行时。 49 | 50 | \begin{tcolorbox}[colback=webgreen!5!white,colframe=webgreen!75!black] 51 | \hspace*{0.75cm}2017年写这本书的时候,编译器似乎确实在尝试编译时求值,即使不严格的情况下。 52 | \end{tcolorbox} 53 | 54 | 例如: 55 | 56 | \begin{lstlisting}[style=styleCXX] 57 | constexpr bool b1 = isPrime(9); // evaluated at compile time 58 | \end{lstlisting} 59 | 60 | 编译时计算该值,也适用于 61 | 62 | \begin{lstlisting}[style=styleCXX] 63 | const bool b2 = isPrime(9); // evaluated at compile time if in namespace scope 64 | \end{lstlisting} 65 | 66 | 假设b2是全局定义的或在命名空间中定义的。块作用域中,编译器可以决定是在编译时计算,还是在运行时计算。 67 | 68 | \begin{tcolorbox}[colback=webgreen!5!white,colframe=webgreen!75!black] 69 | \hspace*{0.75cm}理论上,即使使用了constexpr,编译器也可以决定在运行时计算b的初始值。编译器只需要在编译时检查它是否可计算即可。 70 | \end{tcolorbox} 71 | 72 | 例如,这样: 73 | 74 | \begin{lstlisting}[style=styleCXX] 75 | bool fiftySevenIsPrime() { 76 | return isPrime(57); // evaluated at compile or running time 77 | } 78 | \end{lstlisting} 79 | 80 | 编译器可能会在编译时计算对isPrime的调用。 81 | 82 | 另外: 83 | 84 | \begin{lstlisting}[style=styleCXX] 85 | int x; 86 | ... 87 | std::cout << isPrime(x); // evaluated at run time 88 | \end{lstlisting} 89 | 90 | 将生成在运行时计算x是否为素数的代码。 91 | 92 | -------------------------------------------------------------------------------- /content/1/chapter8/3.tex: -------------------------------------------------------------------------------- 1 | isPrime()等编译时测试的一种应用是,在编译时使用偏特化在不同实现之间进行选择。 2 | 3 | 例如,可以根据模板参数是否是素数来选择不同的实现: 4 | 5 | \begin{lstlisting}[style=styleCXX] 6 | // primary helper template: 7 | template 8 | struct Helper; 9 | 10 | // implementation if SZ is not a prime number: 11 | template 12 | struct Helper 13 | { 14 | ... 15 | }; 16 | 17 | // implementation if SZ is a prime number: 18 | template 19 | struct Helper 20 | { 21 | ... 22 | }; 23 | 24 | template 25 | long foo (std::array const& coll) 26 | { 27 | Helper h; // implementation depends on whether array has prime number as size 28 | ... 29 | } 30 | \end{lstlisting} 31 | 32 | 根据std::array<>参数的大小是否为素数,使用了Helper<>类的两种不同实现。这种偏特化的应用广泛适用于函数根据模板参数,选择不同的实现。 33 | 34 | 上面,使用了两个偏特化来实现两个可能的替代方案,也可以对其中一个替代(默认)情况使用主模板,并对其他情况使用偏特化实现: 35 | 36 | \begin{lstlisting}[style=styleCXX] 37 | // primary helper template (used if no specialization fits): 38 | template 39 | struct Helper 40 | { 41 | ... 42 | }; 43 | 44 | // special implementation if SZ is a prime number: 45 | template 46 | struct Helper 47 | { 48 | ... 49 | }; 50 | \end{lstlisting} 51 | 52 | 因为函数模板不支持偏特化,所以必须使用其他机制根据某些约束来更改函数实现。可供的选择包括: 53 | 54 | \begin{itemize} 55 | \item 56 | 带有静态函数的类, 57 | 58 | \item 59 | std::enable\_if,在第6.3节中介绍。 60 | 61 | \item 62 | SFINAE特性, 63 | 64 | \item 65 | 编译时if特性,该特性从C++17引入,将在第8.5节中介绍。 66 | \end{itemize} 67 | 68 | 第20章讨论了基于约束选择函数实现的技术。 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 | -------------------------------------------------------------------------------- /content/1/chapter8/5.tex: -------------------------------------------------------------------------------- 1 | 偏特化、SFINAE和std::enable\_if允许启用或禁用模板。C++17引入了编译时if语句,允许我们根据编译时条件启用或禁用特定语句。使用if constexpr(…)语法,编译器使用编译时表达式来决定是应用then部分,还是else部分(如果有的话)。 2 | 3 | 作为第一个例子,4.1.1节中介绍的可变参数函数模板print(),使用递归打印参数(任意类型)。与提供一个单独的函数来结束递归不同,constexpr if特性决定是否继续递归: 4 | 5 | \begin{tcolorbox}[colback=webgreen!5!white,colframe=webgreen!75!black] 6 | \hspace*{0.75cm}虽然代码读取if constexpr,但该特性称为constexpr if,因为它是if的“constexpr”形式(由于历史原因)。 7 | \end{tcolorbox} 8 | 9 | \begin{lstlisting}[style=styleCXX] 10 | template 11 | void print (T const& firstArg, Types const&... args) 12 | { 13 | std::cout << firstArg << ’\n’; 14 | if constexpr(sizeof...(args) > 0) { 15 | print(args...); // code only available if sizeof...(args)>0 (since C++17) 16 | } 17 | } 18 | \end{lstlisting} 19 | 20 | 这里,若只对一个参数调用print(), args将成为空的参数包,因此sizeof…(args)将变为0。结果,print()的递归调用变成了一个丢弃的语句,代码没有实例化。因此,相应的函数不需要存在,递归结束。 21 | 22 | 代码没有实例化意味着只执行第一个翻译阶段,检查正确的语法和不依赖于模板参数的名称(参见第1.1.3节)。 23 | 24 | \begin{lstlisting}[style=styleCXX] 25 | template 26 | void foo(T t) 27 | { 28 | if constexpr(std::is_integral_v) { 29 | if (t > 0) { 30 | foo(t-1); // OK 31 | } 32 | } 33 | else { 34 | undeclared(t); // error if not declared and not discarded (i.e. T is not integral) 35 | undeclared(); // error if not declared (even if discarded) 36 | static_assert(false, "no integral"); // always asserts (even if discarded) 37 | static_assert(!std::is_integral_v, "no integral"); // OK 38 | } 39 | } 40 | \end{lstlisting} 41 | 42 | 若constexpr可以用于任何函数,而不仅在模板中。只需要一个产生布尔值的编译时表达式。例如: 43 | 44 | \begin{lstlisting}[style=styleCXX] 45 | int main() 46 | { 47 | if constexpr(std::numeric_limits::is_signed) { 48 | foo(42); // OK 49 | } 50 | else { 51 | undeclared(42); // error if undeclared() not declared 52 | static_assert(false, "unsigned"); // always asserts (even if discarded) 53 | static_assert(!std::numeric_limits::is_signed, 54 | "char is unsigned"); // OK 55 | } 56 | } 57 | \end{lstlisting} 58 | 59 | 有了这个特性,若给定的大小不是质数,可以使用第8.2节中的isPrime()在编译时执行: 60 | 61 | \begin{lstlisting}[style=styleCXX] 62 | template 63 | void foo (std::array const& coll) 64 | { 65 | if constexpr(!isPrime(SZ)) { 66 | ... // special additional handling if the passed array has no prime number as size 67 | } 68 | ... 69 | } 70 | \end{lstlisting} 71 | 72 | 详见14.6节。 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 | -------------------------------------------------------------------------------- /content/1/chapter8/6.tex: -------------------------------------------------------------------------------- 1 | 2 | \begin{itemize} 3 | \item 4 | 模板提供了在编译时进行计算的能力(使用递归进行迭代,使用偏特化或三元操作符进行选择)。 5 | 6 | \item 7 | 使用constexpr函数,可以将大多数编译时计算替换为,可在编译时上下文中调用的“普通函数”。 8 | 9 | \item 10 | 使用偏特化,可以根据特定的编译时约束,在类模板的不同实现之间进行选择。 11 | 12 | \item 13 | 模板只在需要的时候使用,在函数模板声明中的替换不会导致代码无效。这个原则称为SFINAE(替换失败不为过)。 14 | 15 | \item 16 | SFINAE只能用于为特定类型和/或约束提供函数模板。 17 | 18 | \item 19 | C++17起,编译时if允许根据编译时条件(甚至在模板外部)启用或丢弃语句。 20 | \end{itemize} -------------------------------------------------------------------------------- /content/1/chapter9/0.tex: -------------------------------------------------------------------------------- 1 | 模板代码与普通代码略有不同,模板介于宏和普通(非模板)声明之间。这可能是一种过度简化,但这不仅影响使用模板编写算法和数据结构的方式,还影响了表达和对模板的分析。 2 | 3 | 本章中,将讨论其中的一些比较实用的内容,而不探究研究背后的技术细节,这些细节将在第14章中进行探讨。这里,假设C++编译系统由传统的编译器和链接器组成(不属于这一类的C++系统很少)。 -------------------------------------------------------------------------------- /content/1/chapter9/2.tex: -------------------------------------------------------------------------------- 1 | 将函数声明为内联是提高程序运行时间的常用方式。内联说明符旨在提示实现:在调用点内联替换函数体优于通常的函数调用机制。 2 | 3 | 但是,实现可能会忽略这个提示。因此,内联唯一可以保证的是,允许函数定义在程序中出现多次(出现在需要多次包含的头文件中)。 4 | 5 | 与内联函数一样,函数模板可以在多个翻译单元中定义。可以通过将定义放在由多个CPP文件包含的头文件中来实现。 6 | 7 | 但这并不意味着函数模板默认使用内联替换,以及何时在调用点内联替换函数模板体是否优于通常的函数调用机制,这完全取决于编译器。也许,在评估内联调用是否会让性能提高方面,编译器会比人类做得更好。因此,关于内联的具体策略会因编译器而异,甚至取决于特定的编译选项。 8 | 9 | 然而,可以使用性能监视工具,让开发者比编译器拥有更多的信息,可能有希望重写编译器(例如,在针对特定平台(如手机或特定输入)调优软件时)。有时,这可能与编译器特定的属性有关,如noinline或always\_inline。 10 | 11 | 函数模板的全特化在这方面就像普通函数一样:定义只能出现一次,除非以内联方式定义(参见第16.3节)。有关本主题的更广泛、详细的概述,请参见附录A。 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 | -------------------------------------------------------------------------------- /content/1/chapter9/3.tex: -------------------------------------------------------------------------------- 1 | 2 | 即使没有模板,C++头文件也会变得非常大,因此需要很长时间来编译。模板增加了这种趋势,由于开发者的强烈抗议,驱使供应商了一种称为预编译头文件(PCH)的方案。该方案在标准范围之外运行,并依赖于特定于供应商的选项。尽管如何创建和使用预编译头文件的信息,在具有此特性的各种C++编译系统的文档中有介绍,但对其工作原理多少还要了解一下。 3 | 4 | 当编译器翻译一个文件时,其会从文件的开头开始,一直翻译到末尾。当处理来自文件(可能来自\#include的文件)的标记时,会调整它的内部状态,包括像向符号表添加条目这样的事情,以便后续查找。这样做的同时,编译器也可以在目标文件中生成代码。 5 | 6 | 预编译头方案依赖于:许多文件以相同的代码行开始。为了方便讨论,假设每个要编译的文件都以相同的N行代码开始。可以编译这N行,并将编译器的完整状态保存在一个预编译头中。然后,对于程序中的每个文件,可以重新加载保存的状态,并在第N+1行开始编译,重新加载保存的状态的操作比实际编译前N行要快几个数量级。然而,首先保存状态的开销通常比编译N行代码更大,成本的增加大约在20\%到200\%之间。 7 | 8 | 有效使用预编译头的关键是确保(尽可能多的)文件以最大数量的公共代码行开始。实践中,文件必须以相同的\#include指令开始,这些指令(如前所述)消耗了构建时间的很大一部分。因此,头文件的包含顺序也很重要。例如以下两个文件: 9 | 10 | \begin{lstlisting}[style=styleCXX] 11 | #include 12 | #include 13 | #include 14 | ... 15 | \end{lstlisting} 16 | 17 | 和 18 | 19 | \begin{lstlisting}[style=styleCXX] 20 | #include 21 | #include 22 | ... 23 | \end{lstlisting} 24 | 25 | 禁止使用预编译头文件,因为源文件中没有常见的初始状态。 26 | 27 | 一些开发者决定,与其传递使用预编译头来加速文件的翻译,不如包含一些额外的不必要的头文件。这个决定可以大大简化包含政策的管理。例如,创建一个名为std.hpp的头文件通常是相对简单的,包含所有标准头文件: 28 | 29 | \begin{tcolorbox}[colback=webgreen!5!white,colframe=webgreen!75!black] 30 | \hspace*{0.75cm}理论上,标准头文件实际上不需要与物理文件相对应。在实践中,确实如此,而且文件非常大。 31 | \end{tcolorbox} 32 | 33 | \begin{lstlisting}[style=styleCXX] 34 | #include 35 | #include 36 | #include 37 | #include 38 | #include 39 | ... 40 | \end{lstlisting} 41 | 42 | 可以对这个文件进行预编译,然后每个使用标准库的程序文件就可以按如下方式启动: 43 | 44 | \begin{lstlisting}[style=styleCXX] 45 | #include "std.hpp" 46 | ... 47 | \end{lstlisting} 48 | 49 | 通常,这将需要一段时间来编译。但若系统有足够的内存,预编译头文件的处理速度快于不进行预编译的标准头文件。标准头文件在这种方式下特别方便,因为它们很少更改,因此std.hpp文件的预编译头文件只需要构建一次。否则,预编译头文件通常是项目依赖配置的一部分(例如,会根据需要由构建工具或集成开发环境(IDE)的项目构建工具进行更新)。 50 | 51 | 管理预编译头文件的方法是创建预编译头文件层,这些头文件层从最广泛使用和最稳定的头文件(例如,std.hpp头文件)到那些不会随时更改的头文件,因此仍然可以使用预编译的头文件。但若头文件需要大量开发,那创建预编译的头文件所花费的时间,可能比重用它们要要多。这种方法的关键概念是,可以重用较稳定层的预编译头,以提高较不稳定头文件的预编译时间。例如,假设除了std.hpp头文件(已经预编译过了),还要定义了一个core.hpp头文件,包含了一些特定于项目的附加工具,但仍然具有一定程度的稳定性: 52 | 53 | \begin{lstlisting}[style=styleCXX] 54 | #include "std.hpp" 55 | #include "core_data.hpp" 56 | #include "core_algos.hpp" 57 | ... 58 | \end{lstlisting} 59 | 60 | 因为该文件以\#include "standard.hpp"开始,编译器可以加载相关的预编译头文件,并继续下一行,而无需重新编译所有标准头文件。当文件完全处理后,可以生成一个新的预编译头文件。因为编译器可以加载后一个预编译头文件,多以应用可以使用\#include "core.hpp"来快速提供对大量功能的访问。 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /content/1/chapter9/5.tex: -------------------------------------------------------------------------------- 1 | 在头文件和CPP文件中组织源代码具有一定义规则或以ODR形式的结果。附录A对此规则进行了讨论。 2 | 3 | 包含模型是一个实用的方式,主要由C++编译器的现有实践决定。第一个C++实现很特殊:包含的模板定义是隐式的,从而造成了某种分离的错觉(参见第14章了解这个原始模型的详细信息)。 4 | 5 | 第一个C++标准([C++98])通过导出的模板对模板编译的分离模型提供了支持。分离模型允许标记导出的模板声明在头文件中声明,而它们相应的定义放在CPP文件中,非常像非模板代码的声明和定义。与包含模型不同的是,这个模型不基于现有实现的理论模型,而且实现本身比C++标准化委员预期的要复杂得多。它花了五年多的时间才发布了第一个实现(2002年5月),此后的几年里没有出现其他实现。为了更好地使C++标准与现有的实践保持一致,标准化委员会在C++11中删除了导出模板。有兴趣了解更多分离模型细节(和陷阱)的读者可以阅读本书第一版的6.3和10.3节([VandevoordeJosuttisTemplates1st])。 6 | 7 | 有时很容易想象扩展预编译头概念的方法,以便在编译中加载多个头文件,这允许使用更细粒度的方法进行预编译。这里的障碍主要是预处理器:头文件中的宏可能会改变后续头文件的含义。然而,当文件预编译,宏处理就完成了,而试图为其他头文件引入的预处理器效果修订预编译头文件不现实。不久的将来,会有一个称为“模块”的新语言特性(参见17.11节)添加到C++中,来解决这个问题(宏定义不能泄露到模块接口中)。 -------------------------------------------------------------------------------- /content/1/chapter9/6.tex: -------------------------------------------------------------------------------- 1 | 2 | \begin{itemize} 3 | \item 4 | 模板的包含模型是组织模板代码广泛使用的方式。备选方案将在第14章中讨论。 5 | 6 | \item 7 | 只有在类或结构外部的头文件中定义函数模板的特化时才需要内联。 8 | 9 | \item 10 | 要利用预编译的头文件,请确保对\#include保持相同的顺序。 11 | 12 | \item 13 | 调试使用模板的代码很有挑战性。 14 | \end{itemize} -------------------------------------------------------------------------------- /content/2/Section.tex: -------------------------------------------------------------------------------- 1 | 本书的第一部分提供了C++模板底层的大多数语言概念,这些表述足以回答日常C++编程中可能出现的大多数问题。本书的第二部分提供了参考,回答了将语应用于高级软件时出现的问题。可以在第一次阅读时跳过这一部分,然后根据后面章节中的引用或在索引中查找某个概念后返回到特定的章节。 2 | 3 | 我们的目标是清晰且完整,但也保持讨论的简洁。所以例子很简短,而且常常比较有代表性。这也确保了我们的讨论不会偏离主题,从而避免涉及到不相关的话题。 4 | 5 | 此外,我们还将探讨C++中模板语言特性未来可能的更改和扩展。 6 | 7 | 这部分的主题包括: 8 | 9 | \begin{itemize} 10 | \item 11 | 基础的模板声明问题 12 | 13 | \item 14 | 模板中名称的含义 15 | 16 | \item 17 | C++模板实例化机制 18 | 19 | \item 20 | 模板参数的推导规则 21 | 22 | \item 23 | 特化和重载 24 | 25 | \item 26 | 未来的可能性 27 | \end{itemize} 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /content/2/chapter12/0.tex: -------------------------------------------------------------------------------- 1 | 这章中,将深入了解本书第一部分中介绍的基础知识:模板声明、模板形参和实参的限制等。 -------------------------------------------------------------------------------- /content/2/chapter12/6.tex: -------------------------------------------------------------------------------- 1 | 自20世纪80年代后期,C++模板的一般概念和语法保持相对稳定。类模板和函数模板是初始模板工具的一部分,类型参数和非类型参数也是。 2 | 3 | 然而,最初的设计中也有一些重要功能的添加,主要是出于C++标准库的需要。成员模板可能是这些新增功能中最基本的。奇怪的是,只有成员函数模板正式纳入了C++标准。由于编辑上的疏忽,成员类模板成为标准的一部分。 4 | 5 | 友元模板、默认模板参数和双重模板参数,是在C++98标准化过程中出现的。声明双重模板参数的能力有时称为高阶泛型。最初引入的原因是为了支持C++标准库中的某个分配器模型,但这个分配器模型后来使用了不依赖于双重模板参数的模型。后来,双重模板参数几乎要从该语言中删除了,直到1998年标准的标准化过程的时候,规范仍然是不完整。最终,委员会的大多数成员决定保留它们,从而形成了新的标准。 6 | 7 | 别名模板是2011年标准的一部分。别名模板与经常要求的“类型定义模板”特性具有相同的需求,它使编写一个模板变得容易,而这个模板只不过是现有类模板的不同拼写。使之成为标准的规范(N2258)是由Gabriel Dos Reis和Bjarne Stroustrup编写的,Mat Marcus也参与了该提案的早期草案。Gaby还为C++14(N3651)制定了变量模板建议的细节。最初,该提案只打算支持constexpr变量,但在标准草案中采用时,这一限制已经取消了。 8 | 9 | 可变参数模板是由C++11标准库和Boost库(参见[Boost])的需求驱动的,其中C++模板库使用越来越高级(和复杂)的技术可以接受任意数量模板参数。Doug Gregor, J{\"a}kko Jarvi, Gary Powell, Jens Maurer和Jason Merrill提供了标准(N2242)的初始规范。开发规范的同时,Doug还开发了该特性的原始实现(在GNU的GCC中),这对在标准库中使用该特性有很大帮助。 10 | 11 | 折叠表达式是Andrew Sutton和Richard Smith的成果,通过N4191将其添加到C++17中。 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /content/2/chapter13/0.tex: -------------------------------------------------------------------------------- 1 | 大多数编程语言中,名称是一个基本概念。它们是开发者可以引用之前构造的实体的方法。C++编译器遇到一个名称时,必须“查找”以识别所引用的实体。从实现者的角度来看,C++在这方面是一门很难的语言。C++语句x*y;,如果x和y是变量名,这条语句就是一个乘法语句,但如果x是类型名,那么这条语句将y声明为指向类型为x的实体的指针。 2 | 3 | 这个小示例表明C++(像C一样)是一种上下文敏感的语言:若不了解上下文,就不能总是理解一个构造。这与模板有什么关系?模板构造必须处理多个上下文:(1)模板出现的上下文,(2)模板实例化的上下文,(3)模板实例化的模板参数相关的上下文。因此,在C++中必须非常小心地处理“名称”。 -------------------------------------------------------------------------------- /content/2/chapter13/5.tex: -------------------------------------------------------------------------------- 1 | 真正用于解析模板定义的第一个编译器是在20世纪90年代中期,由一家名为Taligent的公司开发的。在此之前——甚至几年后——大多数编译器都将模板视为在实例化时才处理的标记。因此,除了找到模板定义结束位置等少许操作以外,没有进行任何解析。撰写本文时,Microsoft Visual C++编译器仍然以这种方式工作。爱迪生设计集团(EDG)的编译器前端使用了一种混合技术,模板在内部视为一个带注释的标记序列,在需要的模式中执行“通用解析”来验证语法(EDG的产品模拟多个其他编译器,可以很好地模拟微软编译器的行为)。 2 | 3 | Bill Gibbons是Taligent在C++委员会的代表,其极力主张让模板可以无二义性地进行解析。然而,直到惠普公司完成第一个完整的编译器之后,Taligent公司的努力才真正产品化,也才有了一个真正编译模板的C++编译器。和其他具有竞争性优点的产品一样,这个C++编译器很快就由于高质量的错误信息而得到业界的认可。模板的错误信息不会总延迟到实例化时才发出,也要归功于这个编译器。 4 | 5 | 模板的早期开发过程中,Tom Pennello(Metaware公司的一位著名解析专家)就意识到了尖括号所带来的一些问题。Stroustrup也对这个话题进行了讨论[StroustrupDnE],而且认为人们更喜欢阅读尖括号,而不是圆括号。然而,除了尖括号和圆括号,还存在其他的一些可能性:Pennello在1991年的C++标准大会(在达拉斯举办)上特别地提议使用大括号,例如(List{::X})。 6 | 7 | \begin{tcolorbox}[colback=webgreen!5!white,colframe=webgreen!75!black] 8 | \hspace*{0.75cm}使用括号也不是完全没有问题。具体来说,特化类模板的语法需要进行重大的调整。 9 | \end{tcolorbox} 10 | 11 | 那时,因为嵌入在其他模板内部的模板(也称为成员模板)还是不合法的,所以问题的影响范围也非常有限,也就不会涉及到13.3.3节的问题。最后,委员会拒绝了这个取代尖括号的提议。 12 | 13 | 13.4.2节中描述的非依赖型名称和依赖型基类的名称查找规则,是在1993年C++标准中引入的。在1994年,Bjarne Stroustrup的[StroustrupDnE]首次公开描述了这一规则。然而直到1997年惠普才把这一规则引入C++编译器,自那以后出现了大量的派生自依赖型基类的类模板代码。当惠普工程师开始测试该实现时,发现大部分以特殊方式使用模板的代码都无法编译成功了。 14 | 15 | \begin{tcolorbox}[colback=webgreen!5!white,colframe=webgreen!75!black] 16 | \hspace*{0.75cm}幸运的是,在发布新功能之前就发现了。 17 | \end{tcolorbox} 18 | 19 | 特别地,STL的所有实现都打破了这一规则。 20 | 21 | \begin{tcolorbox}[colback=webgreen!5!white,colframe=webgreen!75!black] 22 | \hspace*{0.75cm}具有讽刺意味的是,第一个实现也是由惠普开发的。 23 | \end{tcolorbox} 24 | 25 | 考虑到客户的转换代码的成本,对于那些“假定非依赖型名称可以在依赖型基类中进行查找”的代码,惠普弱化了相关的错误信息。例如,对于位于类模板作用域的非依赖型名称,若利用标准原则不能找到该名称,C++就会在依赖型基类中进行查找。若仍然找不到,才会给出一个错误而编译失败。然而,若在依赖型基类中找到了该名称,那么就会给出一个警告,对该名称进行标记,并且当成是依赖型名称,然后在实例化时再次查找。 26 | 27 | 查找过程中,“非依赖型基类中的名称,会隐藏相同名称的模板参数(13.4.1节)”这一规则显然是一个疏忽,但修改这一规则的建议还没被C++标准委员会所认可。最好的办法就是避免使用非依赖型基类中的名称作为模板参数名称。命名转换对这一类问题都是一种良好的解决方式。 28 | 29 | 友元注入一度认为是有害的,会使得程序的合法性与实例出现的顺序紧密相关。Bill Gibbons(此时他还在Taligent公司开发编译器)就是解决这一问题的最大支持者,因为消除实例顺序依赖性激活了一个新的、有趣的C++开发领域(传闻Taligent正在做)。然而,Barton-Nackman技巧(21.2.1节)需要友元注入的形式,正是这种特殊的技术使它以基于ADL的(弱化)形式保留在语言中。 30 | 31 | Andrew Koenig首次为操作符函数提出了ADL查找(这就是为什么有时候ADL也称为Koenig查找),动机主要是考虑美观性:“用外围命名空间显式地限定操作符名称”看起来很拖沓(例如,对于a+b,需要这样编写:N::operator+(a,b)),而为每个操作符使用using声明又会让代码看起来非常笨重。因此,才决定操作符可以在参数关联的命名空间中查找。ADL随后扩展到普通函数名称的查找,得以容纳有限种类的友元名称注入,并为模板及其实例支持两阶段查找模型(第14章)。泛化的ADL规则也称作扩展Koenig查找。 32 | 33 | David Vandevoorde通过他的论文N1757在C++11中添加了“尖括号黑客”规范。还通过核心问题1104的解决方案添加了“有向图黑客”,以解决美国对C++11标准草案的审查要求。 -------------------------------------------------------------------------------- /content/2/chapter13/images/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Cpp-Templates-2nd/579a1728a33ba4e1f410354a4a427e1668f37190/content/2/chapter13/images/1.png -------------------------------------------------------------------------------- /content/2/chapter14/0.tex: -------------------------------------------------------------------------------- 1 | 模板实例化是从泛型模板定义中生成类型、函数和变量的过程。 2 | 3 | \begin{tcolorbox}[colback=webgreen!5!white,colframe=webgreen!75!black] 4 | \hspace*{0.75cm}术语实例化有时也用来指从类型创建对象。在本书中,用来表示模板实例化。 5 | \end{tcolorbox} 6 | 7 | C++模板实例化的概念非常基础,但有时又错综复杂。因为,模板生成的实体定义不再局限于源代码的位置。模板本身的位置、模板使用的位置,以及模板参数类型定义的位置均在实体的含义中扮演着重要角色。 8 | 9 | 本章中,将解释如何组织源代码以正确使用模板。此外,我们调研了主流C++编译器用于处理模板实例化的各种方法。尽管这些方法在语义上应该等价,但理解编译器实例化策略的基本原则是大有裨益的。构建实际的软件时,每种机制都带有一组小怪癖,并且每种机制都影响了标准C++规范。 -------------------------------------------------------------------------------- /content/2/chapter14/1.tex: -------------------------------------------------------------------------------- 1 | 当C++编译器遇到模板特化时,将通过替换模板参数来创建该特化。 2 | 3 | \begin{tcolorbox}[colback=webgreen!5!white,colframe=webgreen!75!black] 4 | \hspace*{0.75cm}术语特化用于一般意义上的实体,是模板的一个特定实例(参见第10章),不涉及第16章中描述的显式特化机制。 5 | \end{tcolorbox} 6 | 7 | 特化是自动完成的,不需要特殊代码(或者模板定义)的指导。这种按需变化的实例化特性将C++模板与其他早期编译语言不同(如Ada或Eiffel;一些语言需要显式的实例化指令,而其他语言则使用运行时分发机制来避免实例化)。这有时也称为隐式或自动实例化。 8 | 9 | 按需实例化要求编译器在使用时,需要经常访问模板及其部分成员的完整定义(不仅仅是声明)。看看下面的示例: 10 | 11 | \begin{lstlisting}[style=styleCXX] 12 | template class C; // #1 declaration only 13 | 14 | C* p = 0; // #2 fine: definition of C not needed 15 | template 16 | 17 | class C { 18 | public: 19 | void f(); // #3 member declaration 20 | }; // #4 class template definition completed 21 | 22 | void g (C& c) // #5 use class template declaration only 23 | { 24 | c.f(); // #6 use class template definition; 25 | } // will need definition of C::f() 26 | 27 | // in this translation unit 28 | template 29 | void C::f() // required definition due to #6 30 | { } 31 | \end{lstlisting} 32 | 33 | \#1处只有模板的声明是可用的,而不是定义(这种声明称为前置声明)。与普通类的情况一样,不需要类模板的定义来声明指向该类型的指针或引用,就像\#2那样。例如,函数g()的参数类型不需要模板C的完整定义。但当组件需要知道模板特化的大小时,或者访问类的特化成员时,整个类模板定义就必须可见。这解释了为什么在源代码中\#6,必须看到类模板定义;否则,编译器无法验证该成员是否存在,以及是否可访问(不是private或protected)。此外,也需要成员函数的定义,因为\#6的调用需要C::f()。 34 | 35 | 下面是另一个表达式,因为需要C的大小,所以需要实例化之前的类模板: 36 | 37 | \begin{lstlisting}[style=styleCXX] 38 | C* p = new C; 39 | \end{lstlisting} 40 | 41 | 这种情况下需要实例化,以便编译器可以确定C的大小,new表达式需要确定分配多少存储空间。对于这个特定的模板,替代T的参数X的类型不会影响模板的大小,因为C是一个空类。但编译器不需要分析模板定义来避免实例化(编译器都会执行实例化)。此外,本例中还需要实例化来确定C是否具有可访问的默认构造函数,并确保C不会声明成员操作符new或delete。 42 | 43 | 访问类模板成员在源代码中并不总是显式可见,例如:C++重载解析要求对候选函数的参数类型可见: 44 | 45 | \begin{lstlisting}[style=styleCXX] 46 | template 47 | class C { 48 | public: 49 | C(int); // a constructor that can be called with a single parameter 50 | }; // may be used for implicit conversions 51 | 52 | void candidate(C); // #1 53 | void candidate(int) { } // #2 54 | 55 | int main() 56 | { 57 | candidate(42); // both previous function declarations can be called 58 | } 59 | \end{lstlisting} 60 | 61 | 调用candidate(42)时,将解析为在\#2处的重载声明。然而,\#1的声明也可以实例化,以检查是否是匹配的候选(因为单参数构造函数可以隐式地将42转换为类型C的右值)。如果编译器可以在没有实例化的情况下解析调用,则允许(但不是必需)执行此实例化(本例可能就是这种情况,因为在精确匹配上不会选择隐式转换)。还要注意,C的实例化可能会触发错误。 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 | -------------------------------------------------------------------------------- /content/2/chapter14/6.tex: -------------------------------------------------------------------------------- 1 | 正如第8.5节中介绍的,C++17添加了一种新的语句类型,编写模板时非常有用:编译时if。不过,这在实例化过程中引入了一个新的问题。 2 | 3 | 下面的例子演示了其基本操作: 4 | 5 | \begin{lstlisting}[style=styleCXX] 6 | template bool f(T p) { 7 | if constexpr (sizeof(T) <= sizeof(long long)) { 8 | return p>0; 9 | } else { 10 | return p.compare(0) > 0; 11 | } 12 | } 13 | 14 | bool g(int n) { 15 | return f(n); // OK 16 | } 17 | \end{lstlisting} 18 | 19 | 编译时if是一个if语句,其中if关键字紧跟着constexpr关键字(如本例所示)。 20 | 21 | \begin{tcolorbox}[colback=webgreen!5!white,colframe=webgreen!75!black] 22 | \hspace*{0.75cm}虽然代码读取if constexpr,但该特性称为constexpr if,因为它是if的“constexpr”形式。 23 | \end{tcolorbox} 24 | 25 | 后面的圆括号条件必须有一个确定的布尔值(包含了对bool的隐式转换)。因此,编译器知道将选择哪个分支;另一个分支为丢弃分支。特别值得注意的是,在模板(包括泛型Lambda)的实例化期间,不会实例化丢弃分支。我们使用T = int实例化f(T),将丢弃else分支。如果没有丢弃,将会实例化,并且会遇到表达式p.compare(0)的错误信息(当p是一个简单整数时,这个表达式是错误是)。 26 | 27 | C++17及其constexpr if语句可用之前,为了避免此类错误,需要显式的模板特化或重载(参见第16章)来达到类似的效果。 28 | 29 | 上面的例子中,在C++14可以这样实现: 30 | 31 | \begin{lstlisting}[style=styleCXX] 32 | template struct Dispatch { // only to be instantiated when b is false 33 | static bool f(T p) { // (due to next specialization for true) 34 | return p.compare(0) > 0; 35 | } 36 | }; 37 | 38 | template<> struct Dispatch { 39 | static bool f(T p) { 40 | return p > 0; 41 | } 42 | }; 43 | 44 | template bool f(T p) { 45 | return Dispatch::f(p); 46 | } 47 | 48 | bool g(int n) { 49 | return f(n); // OK 50 | } 51 | \end{lstlisting} 52 | 53 | 显然,constexpr if替代方案使我们的意图,更加清楚和简洁。然而,需要实现来细化实例化的单元:以前的函数定义总是作为一个整体实例化,现在必须抑制部分实例化。 54 | 55 | constexpr if的另一个用法是表示处理函数参数包所需的递归。为了推广这个例子,在第8.5节中进行了介绍: 56 | 57 | \begin{lstlisting}[style=styleCXX] 58 | template 59 | void f(Head&& h, Remainder&&... r) 60 | doSomething(std::forward(h)); 61 | if constexpr (sizeof...(r) != 0) { 62 | // handle the remainder recursively (perfectly forwarding the arguments): 63 | f(std::forward(r)...); 64 | } 65 | } 66 | \end{lstlisting} 67 | 68 | 如果没有constexpr if语句,就需要对f()模板进行重载,以确保递归终止。 69 | 70 | 即使在非模板上下文中,constexpr if语句也有些独特的效果: 71 | 72 | \begin{lstlisting}[style=styleCXX] 73 | void h(); 74 | void g() { 75 | if constexpr (sizeof(int) == 1) { 76 | h(); 77 | } 78 | } 79 | \end{lstlisting} 80 | 81 | 在大多数平台上,g()中的条件为false,因此会丢弃h()的调用。因此,h()不需要定义(当然,除非在其他地方使用)。如果本例中省略了关键字constexpr,那么缺少h()的定义通常会在链接时引起错误。 82 | 83 | \begin{tcolorbox}[colback=webgreen!5!white,colframe=webgreen!75!black] 84 | \hspace*{0.75cm}优化可能会掩盖错误。如果保证不存在问题,则可以使用constexpr。 85 | \end{tcolorbox} 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /content/2/chapter14/7.tex: -------------------------------------------------------------------------------- 1 | C++标准库包含许多模板,这些模板通常只用于基本类型。例如,std::basic\_string类模板最常与char(std::string是std::basic\_string的类型别名)或wchar\_t一起使用,可以用其他类似字符的类型实例化。因此,标准库实现通常会为这些常见情况引入显式的实例化声明。例如: 2 | 3 | \begin{lstlisting}[style=styleCXX] 4 | namespace std { 5 | template, 6 | typename Allocator = allocator> 7 | class basic_string { 8 | ... 9 | }; 10 | extern template class basic_string; 11 | extern template class basic_string; 12 | } 13 | \end{lstlisting} 14 | 15 | 实现标准库的源文件将包含显式实例化定义,这些公共实现可以在标准库的所有用户间共享。类似的显式实例化通常存在于各种流类中,例如basic\_iostream、basic\_istream等。 -------------------------------------------------------------------------------- /content/2/chapter14/8.tex: -------------------------------------------------------------------------------- 1 | 本章讨论两个相关但不同的问题:C++模板编译模型和C++模板实例化机制。 2 | 3 | 编译模型在程序转换的各个阶段决定模板的含义。特别地,它确定了模板中各种构造在实例化时的含义。名称查找是编译模型的重要组成部分。 4 | 5 | 标准C++只支持一种编译模型,即包含模型。然而,1998年和2003年的标准也支持模板编译的分离模型,允许在与实例化不同的翻译单元中编写模板定义。这些导出的模板只由爱迪生设计团队(EDG)实现过。 6 | 7 | \begin{tcolorbox}[colback=webgreen!5!white,colframe=webgreen!75!black] 8 | \hspace*{0.75cm}具有讽刺意味的是,将该技术添加到标准文档中时,EDG是最强烈的反对者。 9 | \end{tcolorbox} 10 | 11 | 实现工作确定了:(1)实现C++模板的分离模型比预期的要困难得多,耗时也多;(2)分离模型的假设优势(比如:改进编译时间),由于模型的复杂性,并没有实现。随着2011年标准的开发接近尾声,并且其他实现者不打算支持该特性,C++标准委员会投票决定从该语言中删除导出模板的特性。对分离模型的细节感兴趣的读者,推荐本书的第一版([VandevoordeJosuttisTemplates1st]),其中描述了导出模板的具体行为。 12 | 13 | 实例化机制允许C++实现正确创建实例化的机制,这些机制可能会受到链接器和其他软件构建工具的限制。虽然实例化机制在不同的实现中不同(每个实现都有其优缺点),但通常不会对C++的编程产生重大影响。 14 | 15 | C++11完成后不久,Walter Bright、Herb Sutter和Andrei Alexandrescu提出了一个与constexpr if类似的“静态if”特性(通过N3329)。然而,它是一个更普遍的特性,甚至可以出现在函数定义之外(Walter Bright是D编程语言的主要设计者和实现者,D语言也有类似的功能)。例如: 16 | 17 | \begin{lstlisting}[style=styleCXX] 18 | template 19 | struct Fact { 20 | static if (N <= 1) { 21 | constexpr unsigned long value = 1; 22 | } else { 23 | constexpr unsigned long value = N*Fact::value; 24 | } 25 | }; 26 | \end{lstlisting} 27 | 28 | 本例中,类作用域声明是有条件的。然而,这种强大能力具有争议,一些委员会成员担心它会滥用,而另一些则不喜欢该提议使用的某些技术(例如,大括号没有引入范围,并且丢弃的分支根本没有解析)。 29 | 30 | 几年后,Ville Voutilainen带着提案(P0128)回来了,就是后来的constexpr if语句。经过了一些设计迭代(涉及到暂定的关键字static\_if和constexpr\_if),在Jens Maurer的帮助下,Ville最终将该提案引入语言(通过P0292r2)。 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /content/2/chapter15/0.tex: -------------------------------------------------------------------------------- 1 | 每次调用函数模板时显式地指定模板参数(例如,concat(s, 3)),代码很快就会变得很笨重。幸运的是,C++编译器可以使用模板参数推导功能自动确定模板参数。 2 | 3 | 本章中,我们会解释模板参数推导的详细过程。就像在C++中出现的情况一样,许多规则通常会产生直观的结果。 4 | 5 | 尽管模板参数推导最初是为了简化函数模板调用添加的,但后来扩展到了其他用途,包括从初始化式确定变量的类型。 -------------------------------------------------------------------------------- /content/2/chapter15/1.tex: -------------------------------------------------------------------------------- 1 | 基本推导过程是,将函数调用的参数类型与函数模板相应的类型参数进行比较,并尝试对推导出的一个或多个参数类型进行替换。每个参数对都进行独立分析,若最后得出的结论不同,则推导失败。看看下面的例子: 2 | 3 | \begin{lstlisting}[style=styleCXX] 4 | template 5 | T max (T a, T b) 6 | { 7 | return b < a ? a : b; 8 | } 9 | 10 | auto g = max(1, 1.0); 11 | \end{lstlisting} 12 | 13 | 第一个调用参数是int类型,因此初始max()模板的参数T试探性地推导为int类型。然而,第二个调用参数是double,因此对于这个参数T应该是double型参数:这与前面的结论冲突。这里说的是“推导失败”,而不是“程序无效”。毕竟,对于另一个名为max的模板,推导可能会成功(函数模板可以像普通函数一样重载;请参阅第1.5节和第16章)。 14 | 15 | 若推导出的所有模板参数结论一致,那么若在函数声明的其余部分中替换参数导致无效构造,推导过程仍可能失败。例如: 16 | 17 | \begin{lstlisting}[style=styleCXX] 18 | template 19 | typename T::ElementT at (T a, int i) 20 | { 21 | return a[i]; 22 | } 23 | 24 | void f (int* p) 25 | { 26 | int x = at(p, 7); 27 | } 28 | \end{lstlisting} 29 | 30 | 这里T推断为int*(只有一种参数类型出现了T,因此显然没有分析冲突)。但在返回类型T::ElementT中用int*替换T显然无效,所以推导失败。 31 | 32 | \begin{tcolorbox}[colback=webgreen!5!white,colframe=webgreen!75!black] 33 | \hspace*{0.75cm}这种情况下,推导失败会导致错误。但这属于SFINAE(参见第8.4节):如果另一个函数推导成功,代码可能是有效的。 34 | \end{tcolorbox} 35 | 36 | 继续探索参数匹配是如何进行的。我们将其描述为匹配类型A(从调用参数类型派生)到参数化类型P(从调用参数声明派生)。若调用参数是用引用声明符声明的,P为引用的类型,A是具体的参数类型。但在其他情况下,P是声明的参数类型,而A是通过(忽略const和volatile限定符)将数组和函数类型衰变为指针类型。 37 | 38 | \begin{tcolorbox}[colback=webgreen!5!white,colframe=webgreen!75!black] 39 | \hspace*{0.75cm}衰变是指函数和数组类型隐式转换为指针类型的术语。 40 | \end{tcolorbox} 41 | 42 | 例如: 43 | 44 | \begin{lstlisting}[style=styleCXX] 45 | template void f(T); // parameterized type P is T 46 | template void g(T&); // parameterized type P is also T 47 | 48 | double arr[20]; 49 | int const seven = 7; 50 | 51 | f(arr); // nonreference parameter: T is double* 52 | g(arr); // reference parameter: T is double[20] 53 | f(seven); // nonreference parameter: T is int 54 | g(seven); // reference parameter: T is int const 55 | f(7); // nonreference parameter: T is int 56 | g(7); // reference parameter: T is int => ERROR: can’t pass 7 to int& 57 | \end{lstlisting} 58 | 59 | 对于调用f(arr),arr的数组类型衰变为double*类型,这是T的类型。f(7)中去掉了const限定,因此T推导为int。相反,调用g(x)可以将T推导为double[20]类型(没有发生衰变)。类似地,g(7)的左值参数类型为int const,在匹配引用参数时不删除const和volatile限定符,因此T推导为int const。但注意g(7)会推导T为int(非类右值表达式没有const或volatile限定类型),并且调用会失败,因为7不能传递给int\&类型的参数。 60 | 61 | 当参数是字符串字面值时,绑定到引用的参数不会发生衰变。重新考虑用引用声明的max()模板: 62 | 63 | \begin{lstlisting}[style=styleCXX] 64 | template 65 | T const& max(T const& a, T const& b); 66 | \end{lstlisting} 67 | 68 | 可以合理地预期,对于表达式max(“Apple”,“Pie”),T推导为char const*。然而,“Apple”的类型是char const[6],而“Pie”的类型是char const[4]。没有发生数组到指针的衰变(推导涉及到引用参数),因此T必须同时是char[6]和char[4],才能推导成功,但这是不可能的。参见第7.4节关于如何处理这种情况的讨论。 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /content/2/chapter15/11.tex: -------------------------------------------------------------------------------- 1 | 别名模板(参见第2.8节)在推导方面是“透明的”。只要一个别名模板带有一些模板参数出现,别名定义(等号右边的类型)会替换为参数,产生的结果为推导所用。例如,模板参数推导对下面的三个调用都会成功: 2 | 3 | \hspace*{\fill} \\ %插入空行 4 | \noindent 5 | \textit{deduce/aliastemplate.cpp} 6 | \begin{lstlisting}[style=styleCXX] 7 | template 8 | class Stack; 9 | 10 | template 11 | using DequeStack = Stack>; 12 | 13 | template 14 | void f1(Stack); 15 | 16 | template 17 | void f2(DequeStack); 18 | 19 | template 20 | void f3(Stack); // equivalent to f2 21 | 22 | void test(DequeStack intStack) 23 | { 24 | f1(intStack); // OK: T deduced to int, Cont deduced to std::deque 25 | f2(intStack); // OK: T deduced to int 26 | f3(intStack); // OK: T deduced to int 27 | } 28 | \end{lstlisting} 29 | 30 | 第一次调用(对f1())中,在intStack类型中使用别名模板DequeStack对推导没有影响:指定的类型DequeStack可视为其替代类型Stack{}>。第二次和第三次调用具有相同的涂掉行为,因为f2()中的DequeStack和f3()中的替代形式与Stack{}>相同。对模板参数推导的目标来说,模板别名是透明的:可以用来区分和简化代码,但是对于推导如何进行却不会产生效果。 31 | 32 | 因为别名模板不能特化(参见第16章关于模板特化主题的详细信息),所以这是可能的。假设以下代码可行: 33 | 34 | \begin{lstlisting}[style=styleCXX] 35 | template using A = T; 36 | template<> using A = void; // ERROR, but suppose it were possible... 37 | \end{lstlisting} 38 | 39 | 因为A和A都等于void,所以不能将A与void类型匹配,并得出结论T一定是void。因为别名的每次使用都可以根据其定义进行扩展,从而使别名可以进行透明地推导。 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 | -------------------------------------------------------------------------------- /content/2/chapter15/13.tex: -------------------------------------------------------------------------------- 1 | 函数模板的模板参数推导是最初C++设计的一部分。事实上,由显式模板参数提供的替代方法,直到许多年后才成为C++的一部分。 2 | 3 | SFINAE是本书第一版中介绍的术语,很快在整个C++编程社区中变得流行起来。在C++98中,SFINAE并不像现在这样强大:只应用于有限的类型操作集,并没有涵盖任意表达式或访问控制。随着越来越多的模板技术开始依赖SFINAE(见第19.4节),推广SFINAE条件的必要性变得明显。Steve Adamczyk和John Spicer在C++11中(通过N2634)进行了开发实现。尽管标准中的措辞变化相对较小,但在一些编译器中实现的工作量却大得不成比例。 4 | 5 | auto类型说明符和decltype构造最早在C++03引入,并成为C++11的一部分。他们的发展是由Bjarne Stroustrup和Jaakko J{\"a}rvi牵头的(参见他们的N1607关于auto类型说明符和N2343关于decltype)。 6 | 7 | Stroustrup在最初的C++实现(称为Cfront)中已经考虑了auto语法。当这个特性在C++11中引入时,auto作为存储说明符(继承自C语言)的原始含义保留了下来,并且消除歧义规则决定了关键字应该如何解释。当爱迪生设计团队的前端实现这个特性时,David Vandevoorde发现这个规则可能会让C++11程序员(N2337)感到意外。研究了这个问题之后,标准化委员会通过论文N2546(David Vandevoorde和Jens Maurer)决定完全放弃auto的传统使用(C++03中使用关键字auto的地方,也可以忽略)。这是一个不寻常的先例,即从该语言中删除一个特性,而不先反对它,但后来证明这是一个正确的决定。 8 | 9 | GNU的GCC编译器接受了与decltype特性类似的扩展类型,开发者发现它在模板编程中很有用。但其是在C语言环境中开发的特性,并不完全适合C++。因此,C++委员会既不能合并它,也不修改它,因为那样会破坏依赖于GCC行为的现有代码。这就是为什么decltype不拼写为typeof。Jason Merrill和其他人已经提出了强有力的论点,认为使用不同的操作符会更好,而不是使用现在decltype(x)和decltype((x))不同的方式,但他们没有足够的说服力来改变最终的规范。 10 | 11 | C++17中使用auto声明非类型模板参数的能力主要是由Mike Spertus在James Touton、David Vandevoorde和其他人一起开发的。该特性的更改在P0127R2中提出。尚不清楚是否有意使用decltype(auto)代替auto成为该语言的一部分(显然,委员会未对此进行讨论,但这超出了规范的范畴)。 12 | 13 | Mike Spertus还推动了C++17中类模板参数推导的开发,Richard Smith和Faisal Vali贡献了重要的技术思想(包括推导指引的思想)。P0091R3是会纳入了下一个语言标准的工作论文。 14 | 15 | 结构化绑定主要由Herb Sutter驱动,他与Gabriel Dos Reis和Bjarne Stroustrup一起撰写了P0144R1,提出了该特性。委员会讨论期间进行了许多调整,包括使用括号来分隔分解标识符。Jens Maurer将该提案转化为标准的最终规范(P0217R3)。 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /content/2/chapter15/2.tex: -------------------------------------------------------------------------------- 1 | 比“T”复杂得多的参数化类型可以匹配到给定的参数类型。下面是一些例子: 2 | 3 | \begin{lstlisting}[style=styleCXX] 4 | template 5 | void f1(T*); 6 | 7 | template 8 | void f2(E(&)[N]); 9 | 10 | template 11 | void f3(T1 (T2::*)(T3*)); 12 | 13 | class S { 14 | public: 15 | void f(double*); 16 | }; 17 | 18 | void g (int*** ppp) 19 | { 20 | bool b[42]; 21 | f1(ppp); // deduces T to be int** 22 | f2(b); // deduces E to be bool and N to be 42 23 | f3(&S::f); // deduces T1 = void, T2 = S, and T3 = double 24 | } 25 | \end{lstlisting} 26 | 27 | 复杂类型声明有基本的构造(指针、引用、数组和函数声明符;指向成员的声明符;模板标识;等等),匹配过程从顶层构造开始,递归地遍历各个元素。大多数类型声明构造都可以用这种方式匹配,这称为推导上下文。然而,有一些结构不可推导上下文。例如: 28 | 29 | \begin{itemize} 30 | \item 31 | 限定类型名称。例如,永远不会使用像Q::X这样的类型名来推导模板参数T。 32 | 33 | \item 34 | 非类型参数不只有非类型表达式,像S这样的类型名永远不会用于推断I,也不会通过匹配类型为int(\&)[sizeof(S)]的参数来推断T的类型。 35 | \end{itemize} 36 | 37 | 这些限制并不奇怪,因为推导过程通常不唯一(甚至是有限的),尽管限定类型名的这种限制有时容易忽略。非推导上下文不会表示程序出错,甚至不会说明分析的参数不能参与类型推导。为了说明这一点,看看下面这个更复杂的例子: 38 | 39 | \hspace*{\fill} \\ %插入空行 40 | \noindent 41 | \textit{basics/fppm.cpp} 42 | \begin{lstlisting}[style=styleCXX] 43 | template 44 | class X { 45 | public: 46 | using I = int; 47 | void f(int) { 48 | } 49 | }; 50 | 51 | template 52 | void fppm(void (X::*p)(typename X::I)); 53 | 54 | int main() 55 | { 56 | fppm(&X<33>::f); // fine: N deduced to be 33 57 | } 58 | \end{lstlisting} 59 | 60 | 函数模板fppm()中,子构造X::I不用推导上下文。然而,成员指针类型的成员类组件X可推导上下文,当它推导出的参数N放入到非推导上下文时,将获得与实际参数类型兼容的类型\&X<33>::f。因此,参数匹配,推导成功。 61 | 62 | 相反,对于完全由推导上下文构建的参数类型,推导也可能出现矛盾。例如,声明的类模板X和Y: 63 | 64 | \begin{lstlisting}[style=styleCXX] 65 | template 66 | void f(X, Y>); 67 | 68 | void g() 69 | { 70 | f(X, Y>()); // OK 71 | f(X, Y>()); // ERROR: deduction fails 72 | } 73 | \end{lstlisting} 74 | 75 | 第二次调用函数模板f()的问题是,两个参数T会推导出不同的类型,这是无效的(两种情况下,函数调用参数是通过调用类模板X的默认构造函数,获得的临时对象)。 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 | -------------------------------------------------------------------------------- /content/2/chapter15/3.tex: -------------------------------------------------------------------------------- 1 | 几种情况下,用于推导的一对(A, P)不能从函数调用的参数和函数模板参数中获得。第一种情况发生在获取函数模板地址时,P是函数模板声明的参数化类型,A是初始化或赋值给指针的函数类型。例如: 2 | 3 | \begin{lstlisting}[style=styleCXX] 4 | template 5 | void f(T, T); 6 | 7 | void (*pf)(char, char) = &f; 8 | \end{lstlisting} 9 | 10 | 例子中,P是void(T, T), A是void(char, char)。char替换T后,推导成功,pf初始化为特化的地址f。 11 | 12 | 类似地,函数类型用于P和A的一些特殊情况: 13 | 14 | \begin{itemize} 15 | \item 16 | 确定重载函数模板之间的部分顺序 17 | 18 | \item 19 | 显式特化与函数模板匹配 20 | 21 | \item 22 | 显式实例化与模板匹配 23 | 24 | \item 25 | 友元函数模板特化与模板进行匹配 26 | 27 | \item 28 | 替换delete操作符或delete[]操作符,new操作符或new[]操作符的模板进行匹配 29 | \end{itemize} 30 | 31 | 其中一些话题,以及在类模板偏特化中使用模板参数推导,将在第16章中进一步讨论。 32 | 33 | 另一种特殊情况发生在转换函数模板中。例如: 34 | 35 | \begin{lstlisting}[style=styleCXX] 36 | class S { 37 | public: 38 | template operator T&(); 39 | }; 40 | \end{lstlisting} 41 | 42 | 这种情况下,获得(P, A)对,就好像包含一个要转换的类型和一个作为转换函数返回类型的类型: 43 | 44 | \begin{lstlisting}[style=styleCXX] 45 | void f(int (&)[20]); 46 | 47 | void g(S s) 48 | { 49 | f(s); 50 | } 51 | \end{lstlisting} 52 | 53 | 这里,试图将S转换为int(\&)[20]。因此,类型A为int[20],类型P为T。int[20]替换T,推导成功。 54 | 55 | 最后,还需要对推导自动占位符类型进行一些特殊处理。这在第15.10.4节中会进行讨论。 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 | -------------------------------------------------------------------------------- /content/2/chapter15/4.tex: -------------------------------------------------------------------------------- 1 | 当函数调用的参数是初始化列表时,参数没有特定的类型,通常不会从一对给定的(A, P)进行推断,因为没有A。例如: 2 | 3 | \begin{lstlisting}[style=styleCXX] 4 | #include 5 | 6 | template void f(T p); 7 | 8 | int main() { 9 | f({1, 2, 3}); // ERROR: cannot deduce T from a braced list 10 | } 11 | \end{lstlisting} 12 | 13 | 然而,若参数类型P在除去引用,以及const和volatile限定符后,等价于某些类型P0的std::initializer\_list,这种类型的P0可推导。通过将P0与初始化列表中每个元素的类型进行比较,只有当所有元素都具有相同类型时,推导才会成功: 14 | 15 | \hspace*{\fill} \\ %插入空行 16 | \noindent 17 | \textit{basics/initlist.cpp} 18 | \begin{lstlisting}[style=styleCXX] 19 | #include 20 | 21 | template void f(std::initializer_list); 22 | 23 | int main() 24 | { 25 | f({2, 3, 5, 7, 9}); // OK: T is deduced to int 26 | f({’a’, ’e’, ’i’, ’o’, ’u’, 42}); // ERROR: T deduced to both char and int 27 | } 28 | \end{lstlisting} 29 | 30 | 类似地,若参数类型P是一个类型数组的引用(可推导),推导过程也会将初始化列表的每个元素的类型与P类型数组的元素类型进行比较,当所有元素都有相同的类型时,推导才成功。此外,若(数组)边界可推导(即,只指定非类型模板参数),那么该边界会推导为初始化列表中元素的数量。 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 | -------------------------------------------------------------------------------- /content/2/chapter15/images/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Cpp-Templates-2nd/579a1728a33ba4e1f410354a4a427e1668f37190/content/2/chapter15/images/1.png -------------------------------------------------------------------------------- /content/2/chapter16/0.tex: -------------------------------------------------------------------------------- 1 | 我们已经了解了C++模板如何将泛型定义扩展为一系列相关的类、函数或变量。尽管这是一种强大的机制,但对于模板参数的特定替换,操作的泛化形式远非最佳。 2 | 3 | 支持泛型编程的其他流行编程语言中,C++有些独特,因为它具有丰富的特性,支持用更特殊方式替换泛型定义。本章中,将研究两种C++语言机制,它们允许泛化的特殊形式:模板特化和函数模板重载。 -------------------------------------------------------------------------------- /content/2/chapter16/5.tex: -------------------------------------------------------------------------------- 1 | 当变量模板添加到C++11标准草案中时,忽略了规范的几个方面,其中一些问题仍然没有解决。然而,实际的实现通常会对这些问题进行处理。 2 | 3 | 也许这些问题中最令人惊讶的是,标准提到了偏特化变量模板的能力,但它没有描述如何声明它们或它们的含义。因此,下面的内容是基于实践中的C++实现(确实允许这样的偏特化),而不是基于C++标准。 4 | 5 | 其语法类似于全变量模板特化,不同的是使用实际的模板声明替换template<>,并且变量模板名称后面的模板参数列表必须依赖于模板参数。例如: 6 | 7 | \begin{lstlisting}[style=styleCXX] 8 | template constexpr std::size_t SZ = sizeof(T); 9 | 10 | template constexpr std::size_t SZ = sizeof(void*); 11 | \end{lstlisting} 12 | 13 | 与变量模板的全特化一样,偏特化的类型不需要匹配主模板类型: 14 | 15 | \begin{lstlisting}[style=styleCXX] 16 | template typename T::iterator null_iterator; 17 | 18 | template T* null_iterator = null_ptr; 19 | // T* doesn’t match T::iterator, and that is fine 20 | \end{lstlisting} 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 | -------------------------------------------------------------------------------- /content/2/chapter16/6.tex: -------------------------------------------------------------------------------- 1 | 全特模板从一开始就是C++模板机制的一部分。另一方面,函数模板重载和类模板偏特化出现要晚得多。HP aC++编译器是第一个实现函数模板重载的,EDG的C++前端是第一个实现类模板偏特化的。本章描述的部分排序原则最初是由EDG的Steve Adamczyk和John Spicer发明。 2 | 3 | 模板特化终止无限递归模板定义的能力(如第16.4节中给出的List示例)由来已久。然而,Erwin Unruh可能是第一个注意到这可以进行模板元编程的人:使用模板实例化机制在编译时执行计算。我们会在第23章专门讨论这个话题。 4 | 5 | 可能想知道为什么只有类模板和变量模板可以偏特化,主要是历史原因。其实,也可以为函数模板定义相同的机制(参见第17章)。重载函数模板的效果类似,但有细微的差别。使用时,只需要查找主模板。特化只在使用后考虑,以确定应该使用哪种实现。相反,必须通过查找将所有重载函数模板引入重载集,而且它们可能来自不同的命名空间或类。这无意中增加了重载模板名称的可能性。 6 | 7 | 相反,可以想象允许重载类模板和变量模板的形式: 8 | 9 | \begin{lstlisting}[style=styleCXX] 10 | // invalid overloading of class templates 11 | template class Pair; 12 | template class Pair; 13 | \end{lstlisting} 14 | 15 | 然而,对这样的机制,似乎并没有迫切的需求。 -------------------------------------------------------------------------------- /content/2/chapter17/0.tex: -------------------------------------------------------------------------------- 1 | 2 | 从1988年的最初设计到2003年、2011年、2014年和2017年的各种标准化里程碑,C++模板在不断地发展。可以说,模板在某种程度上与1998年最初的标准之后增加的主要语言有关。 3 | 4 | 第一版列出了在第一个标准之后会看到的一些扩展,其中一些已经成为现实: 5 | 6 | \begin{itemize} 7 | \item 8 | 尖括号的修改:C++11去掉了在两个尖括号之间插入空格的需要。 9 | 10 | \item 11 | 默认函数模板参数:C++11允许函数模板有默认模板参数。 12 | 13 | \item 14 | typdef模板:C++11引入了类似的别名模板。 15 | 16 | \item 17 | 偏特化的模板参数列表不应与主模板的参数列表相同(忽略重命名) 18 | 19 | \item 20 | typeof操作符:C++11引入了decltype操作符,其功能相同(因为不能完全满足C++开发者社区的需求,所以使用了不同的标记,避免与现有的扩展冲突)。 21 | 22 | \item 23 | 静态属性:第一版预期编译器将直接支持类型特征的选择。这已经实现了,尽管接口是使用标准库来表示(使用编译器扩展来实现多个特性)。 24 | 25 | \item 26 | 自定义实例化诊断:新的关键字static\_assert实现了第一版中的std::instantiation\_error。 27 | 28 | \item 29 | 参数列表:C++11中的参数包。 30 | 31 | \item 32 | 布局控制:C++11的alignof和alignas满足了第一版中描述的需求。此外,C++17添加了std::variant模板支持联合结构。 33 | 34 | \item 35 | 初始化式推导:C++17增加了类模板参数推导,解决了同样的问题。 36 | 37 | \item 38 | 函数表达式:C++11的Lambda表达式提供了这种功能(其语法与第一版中讨论的有所不同)。 39 | \end{itemize} 40 | 41 | 第一版中提出的其他方向还没有进入现代C++,但大多数仍在进行讨论,我们将它们的呈现在本书中。与此同时,也有其他想法的出现,这里会展示其中的一些。 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /content/2/chapter17/1.tex: -------------------------------------------------------------------------------- 1 | 2 | 第一版中,本节建议将来可能会对typename的使用规则进行两种放宽(请参阅第13.3.2节):以前不允许使用typename的地方,现在可以用typename了,编译器可以相对容易地推导出带有依赖限定符的限定名,所以不必为命名类型使用typename。前者实现了(C++11中的typename冗余使用),但后者没有。 3 | 4 | 最近,在对类型说明符的期望明确的各种类型,可以选用typename: 5 | 6 | \begin{itemize} 7 | \item 8 | 命名空间和类作用域中的函数,以及成员函数声明的返回类型和参数类型。类似地,函数和成员函数模板,以及出现在作用域中的Lambda表达式。 9 | 10 | \item 11 | 变量、变量模板和静态数据成员声明的类型。同样,变量模板也类似。 12 | 13 | \item 14 | 别名类型或别名模板声明中等号表示后的类型。 15 | 16 | \item 17 | 模板的类型参数的默认参数。 18 | 19 | \item 20 | 出现在尖括号中的类型,紧跟在satatic\_cast、const\_cast、dynamic\_cast或reinterpret\_cast之后。 21 | 22 | \item 23 | new表达式中命名的类型。 24 | \end{itemize} 25 | 26 | 尽管这是一个相对特别的列表,但语言中的这种更改允许删除typename,会使代码更紧凑和可读更强。 27 | 28 | -------------------------------------------------------------------------------- /content/2/chapter17/10.tex: -------------------------------------------------------------------------------- 1 | 参数包是在C++11中引入的,但处理它们通常需要递归模板实例化技术。回想一下14.6节中讨论的代码示例: 2 | 3 | \begin{lstlisting}[style=styleCXX] 4 | template 5 | void f(Head&& h, Remainder&&... r) { 6 | doSomething(h); 7 | if constexpr (sizeof...(r) != 0) { 8 | // handle the remainder recursively (perfectly forwarding the arguments): 9 | f(r...); 10 | } 11 | } 12 | \end{lstlisting} 13 | 14 | 通过使用C++17编译时if语句的特性(请参阅第8.5节),这个示例变得更简单了,但仍然属于递归实例化技术,编译起来可能会很耗时。 15 | 16 | 委员会的几项提案试图在某种程度上简化这种状况。一个例子是引入了从包中挑选特定元素的符号。特别地,对于一个组合P,有人建议用P[N]来表示该组合中的元素N+1。同样,也有人建议将包装表示为“片”(例如,使用P.[b, e]符号)。 17 | 18 | 研究这些建议时,可以清楚地看到它们与上面讨论的反射元编程概念有一定的相关性。目前还不清楚是否会将特定的包选择机制添加到该C++中,或者是否会提供满足这一需求的元编程工具。 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 | -------------------------------------------------------------------------------- /content/2/chapter17/11.tex: -------------------------------------------------------------------------------- 1 | 另一个即将到来的扩展——模块,只与模板有外围关系,但它仍然值得一提,因为模板库是最大受益者之一。 2 | 3 | 目前,库接口在头文件中指定,这些头文件以文本形式包含在翻译单元中。这种方法有几个缺点,令人讨厌的两个是(a)之前包含的代码可能会修改界面文本的含义(例如,通过宏),以及(b)每次重新处理文本迅速占据构建时间。 4 | 5 | 模块是一种特性,其允许库接口编译成编译器特定的格式,然后这些接口可以“导入”到翻译单元中,而不需要进行宏扩展,也不需要因为偶然出现的添加声明而修改代码的含义。此外,编译器可以安排只读取已编译模块文件中与外部代码相关的部分,从而大大加快编译过程。 6 | 7 | 模块定义可能是这样: 8 | 9 | \begin{lstlisting}[style=styleCXX] 10 | module MyLib; 11 | 12 | void helper() { 13 | ... 14 | } 15 | 16 | export inline void libFunc() { 17 | ... 18 | helper(); 19 | ... 20 | } 21 | \end{lstlisting} 22 | 23 | 这个模块导出了一个函数libFunc(),可以在外部代码中使用,如下所示: 24 | 25 | \begin{lstlisting}[style=styleCXX] 26 | import MyLib; 27 | int main() { 28 | libFunc(); 29 | } 30 | \end{lstlisting} 31 | 32 | libFunc()对外部代码可见,但函数helper()不是,即使编译的模块文件可能包含helper()的信息以启用内联。 33 | 34 | 向C++添加模块的提议正在进行中,标准化委员会的目标是在C++17之后将其集成入语言。在开发这样一个提议时,需要考虑的问题是如何从头文件过渡到模块。已经有一些工具在某种程度上实现了这一点(例如,包含头文件而不使其内容成为模块的一部分的能力),以及其他仍在讨论中的功能(例如,从模块导出宏的能力)。 35 | 36 | 模块对于模板库特别有用,因为模板几乎总是在头文件中定义。即使包含一个像这样的基本标准头文件,也相当于处理数万行C++代码(即使只有该头文件中少量的声明会使用)。其他流行库会将其代码量增加一个数量级。对于处理大型复杂代码库的C++开发者来说,避免所有这些编译成本会让他们很感兴趣。 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 | -------------------------------------------------------------------------------- /content/2/chapter17/2.tex: -------------------------------------------------------------------------------- 1 | 对非类型模板参数的限制中,可能最让初学者和高级模板编写者感到惊讶的是不能提供字符串字面值作为模板参数。 2 | 3 | 看看下面的例子: 4 | 5 | \begin{lstlisting}[style=styleCXX] 6 | template 7 | class Diagnoser { 8 | public: 9 | void print(); 10 | }; 11 | 12 | int main() 13 | { 14 | Diagnoser<"Surprise!">().print(); 15 | } 16 | \end{lstlisting} 17 | 18 | 然而,其中有一些问题。标准C++中,当两个Diagnostic实例具有相同的参数时,其类型相同。参数是一个指针值,是一个地址。但出现在不同源位置的两个相同字符串,并不要求具有相同的地址。可能会尴尬的发现,Diagnoser<"X">和Diagnoser<"X">实际上是两种不同且不兼容的类型!(请注意,"X"的类型是char const[2],但当作为模板参数传递时,会衰变为char const*) 19 | 20 | 由于这些(以及相关的)情况,C++标准禁止将字符串字面值作为模板的参数。然而,一些实现确实将该工具作为扩展提供。通过在模板实例的内部表示中使用实际的字符串文字内容来实现这一点。尽管是可行的,但一些C++语言评论者认为,可以用字符串字面值替换的非类型模板参数,声明方式应该与可以用地址替换的非类型模板参数不同。一种可能是在字符参数包中捕获字符串字面值,例如: 21 | 22 | \begin{lstlisting}[style=styleCXX] 23 | template 24 | class Diagnoser { 25 | public: 26 | void print(); 27 | }; 28 | 29 | int main() 30 | { 31 | // instantiates Diagnoser<’S’,’u’,’r’,’p’,’r’,’i’,’s’,’e’,’!’> 32 | Diagnoser<"Surprise!">().print(); 33 | } 34 | \end{lstlisting} 35 | 36 | 还应该注意到另一个技术问题。考虑下面的模板声明,假设语言已经扩展为接受字符串字面值作为模板参数: 37 | 38 | \begin{lstlisting}[style=styleCXX] 39 | template 40 | class Bracket { 41 | public: 42 | static char const* address(); 43 | static char const* bytes(); 44 | }; 45 | 46 | template 47 | char const* Bracket::address() 48 | { 49 | return str; 50 | } 51 | 52 | template 53 | char const* Bracket::bytes() 54 | { 55 | return str; 56 | } 57 | \end{lstlisting} 58 | 59 | 前面的代码中,除了名称之外,两个成员函数是相同的——这种情况很常见。想象一下,实现使用类似于宏展开的方式实例化Bracket<"X">:若两个成员函数在不同的翻译单元中实例化,可能返回不同的值。有趣的是,对目前提供此扩展的一些C++编译器的测试表明,确实受到了这种行为的影响。 60 | 61 | 相关的问题是提供浮点字面量(和简单的浮点常量表达式)作为模板参数的能力。例如: 62 | 63 | \begin{lstlisting}[style=styleCXX] 64 | template 65 | class Converter { 66 | public: 67 | static double convert (double val) { 68 | return val*Ratio; 69 | } 70 | }; 71 | 72 | using InchToMeter = Converter<0.0254>; 73 | \end{lstlisting} 74 | 75 | 这也由一些C++实现提供,并且不存在严重的技术挑战(不像字符串字面量参数)。 76 | 77 | C++11引入了文字类类型的概念:这种类类型可以在编译时计算的常量值(包括通过constexpr函数进行的复杂计算)。当这种类类型可用,就需要将其用于非类型模板参数。但出现了与上面描述的字符串字面量参数类似的问题。两个类类型值的“相等”不简单,因为通常是由operator==确定。这种相等性决定两个实例化是否等价,但在实践中,链接器必须通过比较损坏的名称来检查这种等价性。解决方法可能是选择将某些文字类标记为具有简单的相等标准,相当于对类的标量成员进行比较。只有具有这种简单的相等条件的类类型,才允许作为非类型模板参数类型。 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 | -------------------------------------------------------------------------------- /content/2/chapter17/3.tex: -------------------------------------------------------------------------------- 1 | 2 | 第16章中,讨论了类模板如何偏特化,函数模板则可以重载。这两种机制有些不同。 3 | 4 | 偏特化不会引入新模板,其为现有模板(主模板)的扩展。当查找类模板时,首先考虑主模板。若选择了主模板后,发现该模板的偏特化具有与实例化匹配的模板参数模式时,则实例化其定义,而非主模板的定义。(与全特化模板的工作方式完全相同) 5 | 6 | 相反,重载函数模板是完全独立的模板。在实例化模板时,将所有重载模板放在一起,重载解析尝试选择最适合的模板。乍一看,这似乎有充足的选择,但在实践中会有一些限制: 7 | 8 | \begin{itemize} 9 | \item 10 | 可以特化类的成员模板,而不需要更改该类的定义,但添加重载成员确实需要更改类的定义。这不是一个好选择,因为可能不能这样做。此外,C++标准目前不允许向std命名空间添加新模板,但它允许对该命名空间模板进行特化。 11 | 12 | \item 13 | 要重载函数模板,其函数参数必须在某些重要方面有所不同。考虑一个函数模板R convert(T const\&),其中R和T是模板参数。我们可能很想特化R = void的模板,但这不能使用重载来完成。 14 | 15 | \item 16 | 对于非重载函数的代码,在函数重载时不再有效。具体来说,给定两个函数模板f(T)和g(T)(其中T是一个模板参数),表达式g(\&f)只有在f没有重载时才有效(否则,无法确定f是哪个)。 17 | 18 | \item 19 | 友元声明指的是特定函数模板或特定函数模板的实例化。函数模板的重载版本不会自动拥有原始模板的权限。 20 | \end{itemize} 21 | 22 | 列表共同构成了一个强有力的论据,支持函数模板的偏特化构造。 23 | 24 | 偏特化函数模板的一种自然语法是泛化类模板表示法: 25 | 26 | \begin{lstlisting}[style=styleCXX] 27 | template 28 | T const& max (T const&, T const&); // primary template 29 | 30 | template 31 | T* const& max (T* const&, T* const&); // partial specialization 32 | \end{lstlisting} 33 | 34 | 一些语言设计人员担心这种偏特化方法与函数模板重载的交互。例如: 35 | 36 | \begin{lstlisting}[style=styleCXX] 37 | template 38 | void add (T& x, int i); // a primary template 39 | 40 | template 41 | void add (T1 a, T2 b); // another (overloaded) primary template 42 | 43 | template 44 | void add (T*&, int); // Which primary template does this specialize? 45 | \end{lstlisting} 46 | 47 | 然而,我们希望将这样的情况视为错误,不会对功能的使用产生重大影响。 48 | 49 | 这个扩展在C++11的标准化过程中简要地讨论过,但是最后却没有引起多少人的兴趣。不过,因为其巧妙地解决了一些常见的编程问题,这个话题偶尔还会出现。这个特性也许会在未来的C++标准中采纳。 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /content/2/chapter17/4.tex: -------------------------------------------------------------------------------- 1 | 第21.4节描述了一种技术,其允许为特定参数提供非默认模板参数,而不必指定可以使用默认值的其他模板参数,但需要大量的工作才能得到相对简单的效果。因此,很自然的会想到使用一种语言机制来命名模板参数。 2 | 3 | 类似的扩展(有时称为关键字参数)是由Roland Hartinger在C++标准化过程中提出的(参见[StroustrupDnE]第6.5.1节)。虽然在技术上合理,但该提议最终没有接受。我们不知道命名模板参数会是否会成为C++的一部分,但是这个话题会在委员会的讨论中经常出现。 4 | 5 | 为了完整起见,这里说一个已经讨论过的语法概念: 6 | 7 | \begin{lstlisting}[style=styleCXX] 8 | template, 10 | typename Copy = defaultCopy, 11 | typename Swap = defaultSwap, 12 | typename Init = defaultInit, 13 | typename Kill = defaultKill> 14 | class Mutator { 15 | ... 16 | }; 17 | 18 | void test(MatrixList ml) 19 | { 20 | mySort (ml, Mutator ); 21 | } 22 | \end{lstlisting} 23 | 24 | 参数名称前的句点用来表示我们通过名称引用模板参数,这种语法类似于1999年C标准中引入的“指定初始化式”语法: 25 | 26 | \begin{lstlisting}[style=styleCXX] 27 | struct Rectangle { int top, left, width, height; }; 28 | struct Rectangle r = { .width = 10, .height = 10, .top = 0, .left = 0 }; 29 | \end{lstlisting} 30 | 31 | 当然,引入命名模板参数意味着模板的模板参数名称现在是该模板的公共接口的一部分,不能随意更改。这可以通过更明确的、可选择的语法来解决,例如: 32 | 33 | \begin{lstlisting}[style=styleCXX] 34 | template, 36 | Copy: typename C = defaultCopy, 37 | Swap: typename S = defaultSwap, 38 | Init: typename I = defaultInit, 39 | Kill: typename K = defaultKill> 40 | class Mutator { 41 | ... 42 | }; 43 | 44 | void test(MatrixList ml) 45 | { 46 | mySort (ml, Mutator ); 47 | } 48 | \end{lstlisting} 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 | -------------------------------------------------------------------------------- /content/2/chapter17/5.tex: -------------------------------------------------------------------------------- 1 | 完全可以想象类模板可以在其模板参数上重载。例如,创建一系列数组模板,其中包含动态和静态的数组: 2 | 3 | \begin{lstlisting}[style=styleCXX] 4 | template 5 | class Array { 6 | // dynamically sized array 7 | ... 8 | }; 9 | 10 | template 11 | class Array { 12 | // fixed size array 13 | ... 14 | }; 15 | \end{lstlisting} 16 | 17 | 重载并不一定局限于模板参数的数量,参数类型也可以改变: 18 | 19 | \begin{lstlisting}[style=styleCXX] 20 | template 21 | class Pair { 22 | // pair of fields 23 | ... 24 | }; 25 | 26 | template 27 | class Pair { 28 | // pair of constant integer values 29 | ... 30 | }; 31 | \end{lstlisting} 32 | 33 | 虽然语言设计者已经非正式地讨论过了这个想法,但是还没有正式地提交给C++标准委员会。 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 | -------------------------------------------------------------------------------- /content/2/chapter17/6.tex: -------------------------------------------------------------------------------- 1 | 只有当包展开发生或参数列表的末尾时,包展开的模板参数推导才有效。从列表中提取第一个元素相当简单: 2 | 3 | \begin{lstlisting}[style=styleCXX] 4 | template 5 | struct Front; 6 | 7 | template 8 | struct Front { 9 | using Type = FrontT; 10 | }; 11 | \end{lstlisting} 12 | 13 | 在第16.4节中描述的对偏特化的限制,不能简单地提取列表的最后一个元素: 14 | 15 | \begin{lstlisting}[style=styleCXX] 16 | template 17 | struct Back; 18 | 19 | template 20 | struct Back { // ERROR: pack expansion not at the end of 21 | using Type = BackT; // template argument list 22 | }; 23 | \end{lstlisting} 24 | 25 | 可变参数函数模板的模板参数推导也受到类似的限制。关于包展开和偏特化的模板参数推导的规则将放宽,允许包展开发生在模板参数列表的任何地方,这使得操作更加简单。此外,推论允许在同一个参数列表中进行多个包扩展(尽管可能性较小): 26 | 27 | \begin{lstlisting}[style=styleCXX] 28 | template class Tuple { 29 | }; 30 | 31 | template 32 | struct Split; 33 | 34 | template 35 | struct Split { 36 | using before = Tuple; 37 | using after = Tuple; 38 | }; 39 | \end{lstlisting} 40 | 41 | 允许多个包扩展会带来额外的复杂性。例如,Split是在T第一次出现时分开,在T最后一次出现时分开,还是在两者之间分开?推导过程的复杂性达到多少时,编译器才会放弃推导? 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 | -------------------------------------------------------------------------------- /content/2/chapter17/7.tex: -------------------------------------------------------------------------------- 1 | 编写模板时,规则性是一种优点:若单一构造可以覆盖所有情况,会使模板更简单。我们的程序有一点不规则,那就是类型。例如,考虑以下情况: 2 | 3 | \begin{lstlisting}[style=styleCXX] 4 | auto&& r = f(); // error if f() returns void 5 | \end{lstlisting} 6 | 7 | 这适用于f()返回的所有类型,除了void。同样的问题发生在使用decltype(auto): 8 | 9 | \begin{lstlisting}[style=styleCXX] 10 | decltype(auto) r = f(); // error if f() 11 | \end{lstlisting} 12 | 13 | void并不是唯一的非规范类型:函数类型和引用类型在某些方面也经常出现异常行为。然而,void会使模板变得复杂。例如,第11.1.3节如何使完美的std::invoke()包装器实现复杂化的示例。 14 | 15 | 可以将void定义为具有唯一值的普通值类型(如std::nullptr\_t表示nullptr)。为了向后兼容的目的,仍需要为函数声明保留一个特殊的情况: 16 | 17 | \begin{lstlisting}[style=styleCXX] 18 | void g(void); // same as void g(); 19 | \end{lstlisting} 20 | 21 | 在大多数方式中,void会成为一个完整的值类型。可以声明void变量和引用: 22 | 23 | \begin{lstlisting}[style=styleCXX] 24 | void v = void{}; 25 | void&& rrv = f(); 26 | \end{lstlisting} 27 | 28 | 更重要的是,许多模板不再需要专门处理void。 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 | -------------------------------------------------------------------------------- /content/2/chapter17/8.tex: -------------------------------------------------------------------------------- 1 | 2 | 使用模板编程的复杂性,很大程度上来源于编译器无法在本地检查模板定义是否正确。相反,模板的大多数检查发生在模板实例化期间,此时模板定义上下文和模板实例化上下文交织在一起。这种不同上下文的混合使得很难进行分配。这是模板定义的错误,因为不正确地使用了模板参数?还是模板用户的错误,还是提供的模板参数不符合模板的要求?这个问题可以用一个简单的例子来说明,我们用一个常规编译器产生的错误信息进行了注释: 3 | 4 | \begin{lstlisting}[style=styleCXX] 5 | template 6 | T max(T a, T b) 7 | { 8 | return b < a ? a : b; // ERROR: “no match for operator < 9 | // (operator types are ’X’ and ’X’)” 10 | } 11 | 12 | struct X { 13 | }; 14 | bool operator> (X, X); 15 | 16 | int main() 17 | { 18 | X a, b; 19 | X m = max(a, b); // NOTE: “in instantiation of function template specialization 20 | // ’max’ requested here” 21 | } 22 | \end{lstlisting} 23 | 24 | 实际的错误(缺少合适的小于操作符)是在函数模板max()的定义中检测到的。这可能是真正的错误——也许max()应该使用大于操作符代替?但编译器还提供了一个信息,指出了max实例化的位置,这可能才是真正的错误——也许max()在文档中需要小于操作符?无法回答这个问题的话,会出现在第9.4节中提到的“错误小说”,其中编译器提供了从初始化到检测到错误模板定义的整个模板实例化历史。然后,开发者需要确定哪个模板定义(或者模板的原始使用)出现了错误。 25 | 26 | 模板类型检查背后的思想是,在模板本身中描述模板的需求。这样,编译器就可以在编译失败时判断是模板定义问题,还是模板使用出了问题。解决这个问题的方法是使用概念将模板的需求描述为模板签名的一部分: 27 | 28 | \begin{lstlisting}[style=styleCXX] 29 | template requires LessThanComparable 30 | T max(T a, T b) 31 | { 32 | return b < a ? a : b; 33 | } 34 | 35 | struct X { }; 36 | bool operator> (X, X); 37 | 38 | int main() 39 | { 40 | X a, b; 41 | X m = max(a, b); // ERROR: X does not meet the LessThanComparable requirement 42 | } 43 | \end{lstlisting} 44 | 45 | 通过描述模板参数T上的需求,编译器能够确保函数模板max()只使用用户期望提供的T上的操作(LessThanComparable描述了小于操作符的需求)。使用模板时,编译器可以检查所提供的模板参数是否提供了max()函数模板正常工作所需的行为。通过分离类型检查问题,编译器可以更容易地提供准确的诊断信息。 46 | 47 | 上面的例子中,LessThanComparable称为概念:表示编译器可以检查的类型上的约束(更一般的情况下,是一组类型上的约束)。概念系统可以以不同的方式进行设计。 48 | 49 | C++11标准制定过程中,一个精心设计并实现的系统的概念足够强大,可以检查模板的实例化点和模板的定义。上面的例子中,前者意味着main()中的错误可以更早的发现,诊断出X不满足LessThanComparable的约束。后者在处理max()模板时,编译器检查是否使用了LessThanComparable概念不允许的操作(若违反了该约束,则会发出诊断信息)。出于各种实际的考虑,C++11提案最终从语言规范中删除了概念(例如,仍然有许多小的规范问题,这些问题的解决会威胁到已经延迟发布的标准)。 50 | 51 | C++11最终发布后,委员会成员提出了一个新的提案(最初称为精简概念)并进行了开发。这个系统的目的不是基于模板所附带的约束来检查模板的正确性,它只关注实例化点。因此,若在我们的示例中使用大于操作符实现max(),此时不会发出错误。但是,由于X不满足LessThanComparable的约束,在main()中仍然会发出错误。新版概念提案实现,并在所谓的概念TS(TS代表技术规范)中指定,称为概念的C++扩展。 52 | 53 | \begin{tcolorbox}[colback=webgreen!5!white,colframe=webgreen!75!black] 54 | \hspace*{0.75cm}例如,2017年初的概念TS版本的N4641。 55 | \end{tcolorbox} 56 | 57 | 目前,该技术规范的基本已经集成到下一个标准的草案中(预计进入C++20)。附录E涵盖了本书付印时草稿中规定的语言特性。 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 | -------------------------------------------------------------------------------- /content/2/chapter17/9.tex: -------------------------------------------------------------------------------- 1 | 编程上下文中,反射指的是以编程方式检查程序特性的能力(例如,回答诸如类型是整数吗?或类类型包含哪些非静态数据成员?)元编程是“对程序进行编程”的技巧,相当于以编程方式生成新代码。因此,反射式元编程是一种自动合成代码,使其适应程序的现有属性(通常是类型)的技术。 2 | 3 | 本书的第三部分中,我们将探讨模板如何实现一些简单形式的反射和元编程(某种意义上,模板实例化是元编程的一种形式,因为它会导致新代码的合成)。然而,当涉及到反射时,C++17模板的能力是相当有限的(例如,不可能回答这个问题:一个类类型包含哪些非静态数据成员?),元编程的选项不是很方便(特别是,语法变得笨拙,性能令人失望)。 4 | 5 | 认识到这一领域新特性的潜力,C++标准化委员会创建了一个研究小组(SG7),以探索更强大的反射选项。组织的章程后来也扩展到元编程,以下是正在考虑的选项的一个例子: 6 | 7 | \begin{lstlisting}[style=styleCXX] 8 | template void report(T p) { 9 | constexpr { 10 | std::meta::info infoT = reflexpr(T); 11 | for (std::meta::info : std::meta::data_members(infoT)) { 12 | -> { 13 | std::cout << (: std::meta::name(info) :) 14 | << ": " << p.(.info.) << ’\n’; 15 | } 16 | } 17 | } 18 | // code will be injected here 19 | } 20 | \end{lstlisting} 21 | 22 | 这段代码中有很多新内容。首先,constexpr{…}构造强制其中的语句在编译时进行求值,但若出现在模板中,则只在模板实例化时进行求值。其次,reflexpr()操作符会生成一个不透明类型std::meta::info的表达式,这是一个句柄,用于反映关于其参数的信息(在本例中是T的类型)。标准元函数库允许查询此元信息。标准元函数之一是std::meta::data\_members,会生成一个std::meta::info序列,描述其操作数的直接非静态数据成员。所以上面的for循环实际上是对p的非静态数据成员的循环。 23 | 24 | 该系统元编程能力的核心是在各种范围内“注入”代码的能力。构造\texttt{->}{…}在启动constexpr求值的语句或声明之后,注入语句和/或声明。本例中,在constexpr{…}构造。注入的代码片段可以包含特定的模式,这些模式可以使用计算值替换。这个例子中,(:…:)产生了一个字符串字面值(表达式std::meta::name(info)产生了一个类似字符串的对象,表示实体数据成员的非限定名,在这个例子中由info表示)。类似地,表达式(.info.)产生一个标识符,命名info所表示的实体。还提出了用于生成类型、模板参数列表等的其他模式。 25 | 26 | 这些就绪之后,实例化类型的函数模板report(): 27 | 28 | \begin{lstlisting}[style=styleCXX] 29 | struct X { 30 | int x; 31 | std::string s; 32 | }; 33 | \end{lstlisting} 34 | 35 | 会产生类似于 36 | 37 | \begin{lstlisting}[style=styleCXX] 38 | template<> void report(X const& p) { 39 | std::cout << "x" << ": " << "p.x" << ’\n’; 40 | std::cout << "s" << ": " << "p.s" << ’\n’; 41 | } 42 | \end{lstlisting} 43 | 44 | 该函数自动生成一个函数来输出类类型的非静态数据成员值。 45 | 46 | 这些类型的功能有许多应用程序。虽然类似的东西很可能最终会采纳到语言中,但还不清楚什么时候采用。在撰写本文时已经演示了一些实验性实现。(出版这本书之前,SG7同意使用constexpr求值和类似std::meta::info的值类型来处理反射式元编程。然而,这里提出的注入机制没有达成一致意见,可能会采取另一种方式) 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 | -------------------------------------------------------------------------------- /content/3/Section.tex: -------------------------------------------------------------------------------- 1 | 程序通常使用所选编程语言提供的设计模式来构造。因为模板引入了全新的语言机制,所以不难发现其需要新的设计模式。我们将在这一部分探讨这些模式,C++标准库包含或使用了其中的几个。 2 | 3 | 模板与传统的语言不同,其允许我们参数化代码的类型和常量。当与(1)偏特化和(2)递归实例化相结合时,会带来惊人的效果。 4 | 5 | 我们不仅旨在列出各种有用的设计,还要传达启发此类设计的原则,以便创造新的技术。因此,下面的章节阐述了大量的设计技巧,包括: 6 | 7 | \begin{itemize} 8 | \item 9 | 多态性 10 | 11 | \item 12 | 具有特征的泛型编程 13 | 14 | \item 15 | 处理重载和继承 16 | 17 | \item 18 | 元编程 19 | 20 | \item 21 | 异构结构和算法 22 | 23 | \item 24 | 表达式模板 25 | \end{itemize} 26 | 27 | 还提供了一些注释来帮助调试模板。 -------------------------------------------------------------------------------- /content/3/chapter18/0.tex: -------------------------------------------------------------------------------- 1 | 多态性是将不同的行为与单个泛型表示法关联起来的能力。 2 | 3 | \begin{tcolorbox}[colback=webgreen!5!white,colframe=webgreen!75!black] 4 | \hspace*{0.75cm}多态性字面上指的是具有多种形式或形状的情况(来自希腊语polymorphos)。 5 | \end{tcolorbox} 6 | 7 | 多态性也是面向对象泛型编程的基础,C++中通过类继承和虚函数来支持多态。因为这些机制(至少部分)在运行时处理,所以这里讨论动态多态性。通常所说的多态性,指的就是动态多态性。然而,模板还允许将不同的特定行为与单个泛型表示关联起来,但这种关联通常在编译时进行处理,称之为静态多态性。本章中,我们回顾了多态的两种形式,并讨论哪种形式适合于哪种情况。 8 | 9 | 注意,介绍和讨论了一些设计问题之后,第22章将讨论一些处理多态性的方法。 -------------------------------------------------------------------------------- /content/3/chapter18/3.tex: -------------------------------------------------------------------------------- 1 | 2 | 先对这两种形式的多态性进行分类和比较。 3 | 4 | \hspace*{\fill} \\ %插入空行 5 | \noindent 6 | \textbf{术语} 7 | 8 | 动态和静态多态为不同的C++编程习惯提供了支持: 9 | 10 | \begin{tcolorbox}[colback=webgreen!5!white,colframe=webgreen!75!black] 11 | \hspace*{0.75cm}有关多态术语的详细讨论,请参见[CzarneckiEiseneckerGenProg]的第6.5到6.7节。 12 | \end{tcolorbox} 13 | 14 | \begin{itemize} 15 | \item 16 | 通过继承实现的多态是有界和动态的: 17 | 18 | \begin{itemize} 19 | \item[-] 20 | 有界意味着参与多态行为的类型的接口,是由公共基类的设计预先确定的(这个概念的其他术语描述是invasive和intrusive)。 21 | 22 | \item[-] 23 | 动态意味着接口的绑定在运行时(动态)完成。 24 | \end{itemize} 25 | 26 | \item 27 | 通过模板实现的多态性是无界和静态的: 28 | 29 | \begin{itemize} 30 | \item[-] 31 | 无界意味着参与多态行为的类型接口不是预先确定的(该概念的其他术语描述是noninvasive和nonintrusive)。 32 | 33 | \item[-] 34 | 静态意味着接口的绑定在编译时完成(静态)。 35 | \end{itemize} 36 | \end{itemize} 37 | 38 | 严格地说,在C++语言中,动态多态性和静态多态性是有界动态多态性和无界静态多态性的快捷方式。其他语言中,也存在其他组合(例如,Smalltalk提供了无界的动态多态性)。在C++的上下文中,动态多态和静态多态这两个术语不会引起混淆。 39 | 40 | \hspace*{\fill} \\ %插入空行 41 | \noindent 42 | \textbf{优势与不足} 43 | 44 | C++中的动态多态性展示了以下优点: 45 | 46 | \begin{itemize} 47 | \item 48 | 可以优雅地处理异构集合。 49 | 50 | \item 51 | 可执行代码的体积更小(只需要一个多态函数,而必须生成不同的模板实例来处理不同的类型)。 52 | 53 | \item 54 | 代码可以完全编译;因此不需要发布实现源(发布模板库通常需要发布模板实现的源代码)。 55 | \end{itemize} 56 | 57 | C++中的静态多态有这些优点: 58 | 59 | \begin{itemize} 60 | \item 61 | 内置类型的集合很容易实现,接口通用性不需要通过公共基类来表示。 62 | 63 | \item 64 | 生成的代码可能更快(因为不需要指针,而且可以更频繁地内联非虚函数)。 65 | 66 | \item 67 | 若应用程序只执行了部分接口,仍然可以使用只提供部分接口的具体类型。 68 | \end{itemize} 69 | 70 | 静态多态性通常认为比动态多态性更类型安全,因为所有绑定都在编译时检查。例如,模板实例化的容器中插入错误类型的对象没有危险。然而,需要指向公共基类的指针的容器中,这些指针有可能无意中指向不同类型的对象。 71 | 72 | 实践中,不同的语义假设隐藏在相同的接口后面时,模板实例化也会造成一些麻烦。当假定关联加法操作符的模板实例化了一个与该操作符无关的类型时,可能会发生意外。这种语义不匹配在基于继承的层次结构中很少发生,可能是因为更明确地指定了接口规范。 73 | 74 | \hspace*{\fill} \\ %插入空行 75 | \noindent 76 | \textbf{两种形式结合} 77 | 78 | 当然,可以结合这两种形式的多态性。例如,可以从公共基类派生不同种类的几何对象,以便能够处理几何对象的异构集合。但仍然可以使用模板,为特定类型的几何对象编写代码。 79 | 80 | 继承和模板的结合将在第21章中进一步介绍,将看到如何将成员函数的虚态参数化,以及如何使用基于继承的奇异递归模板模式(CRTP),为静态多态性提供灵活性。 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 | -------------------------------------------------------------------------------- /content/3/chapter18/4.tex: -------------------------------------------------------------------------------- 1 | 反对模板静态多态的一个论点是,接口的绑定通过实例化相应的模板来完成,没有公共接口(类)用来编程。若所有实例化的代码都有效,那么模板就可以工作。若不是,可能会导致难以理解的错误消息,甚至导致有效但意外的行为。 2 | 3 | 由于这个原因,C++语言设计人员致力于为模板参数显式提供(和检查)接口的能力。这样的接口在C++中称为概念,表示模板参数必须满足一组约束后,才能成功实例化模板。 4 | 5 | 尽管各路开发者这些年在这个领域做了许多工作,但是概念直到C++17,才成为标准C++的一部分。一些编译器提供了对这种特性的实验性支持,但这些概念可能会成为C++17之后标准的一部分。 6 | 7 | \begin{tcolorbox}[colback=webgreen!5!white,colframe=webgreen!75!black] 8 | \hspace*{0.75cm}例如,GCC 7提供了-fconcepts选项。 9 | \end{tcolorbox} 10 | 11 | 概念可以理解为静态多态的一种“接口”: 12 | 13 | \hspace*{\fill} \\ %插入空行 14 | \noindent 15 | \textit{poly/conceptsreq.hpp} 16 | \begin{lstlisting}[style=styleCXX] 17 | #include "coord.hpp" 18 | template 19 | concept GeoObj = requires(T x) { 20 | { x.draw() } -> void; 21 | { x.center_of_gravity() } -> Coord; 22 | ... 23 | }; 24 | \end{lstlisting} 25 | 26 | 这里,使用关键字概念来定义概念GeoObj,约束类型具有可调用成员draw()和center\_of\_gravity(),并具有适当的结果类型。 27 | 28 | 现在,可以重写一些示例模板,以包含一个require子句,用GeoObj概念约束模板参数: 29 | 30 | \hspace*{\fill} \\ %插入空行 31 | \noindent 32 | \textit{poly/conceptspoly.hpp} 33 | \begin{lstlisting}[style=styleCXX] 34 | #include "conceptsreq.hpp" 35 | #include 36 | 37 | // draw any GeoObj 38 | template 39 | requires GeoObj 40 | void myDraw (T const& obj) 41 | { 42 | obj.draw(); // call draw() according to type of object 43 | } 44 | 45 | // compute distance of center of gravity between two GeoObjs 46 | template 47 | requires GeoObj && GeoObj 48 | Coord distance (T1 const& x1, T2 const& x2) 49 | { 50 | Coord c = x1.center_of_gravity() - x2.center_of_gravity(); 51 | return c.abs(); // return coordinates as absolute values 52 | } 53 | 54 | // draw homogeneous collection of GeoObjs 55 | template 56 | requires GeoObj 57 | void drawElems (std::vector const& elems) 58 | { 59 | for (std::size_type i=0; i头文件的基础。这些特征中的许多特性都可以通过本章描述的技术实现,但其他的(用于检测POD的std::is\_pod)需要编译器的支持,就像SGI编译器提供的\_\_type\_traits特化一样。 10 | 11 | 第一次标准化工作期间描述类型推演和替代规则时,就注意到SFINAE原则用于类型分类的目的。然而,这没有正式的文档记录,后来花费了大量的精力创建本章中描述的一些技术。本书的第一版是这种技术最早的来源之一,介绍了术语SFINAE。这一领域的另一位著名的早期贡献者是Andrei Alexandrescu,他普及了sizeof操作符来确定重载解析的结果。使这种技术变得流行,以至于2011年的标准将SFINAE的范围从简单的类型错误,扩展为函数模板的错误(参见[SpicerSFINAE])。这个扩展结合了decltype、右值引用和可变参数模板的添加,极大地扩展了在特征中测试特定属性的能力。 12 | 13 | 使用像isValid这样的泛型Lambda来提取SFINAE条件,是Louis Dionne在2015年引入的一种技术,Boost.Hana使用了这种技术(参见[boostana],一个适合在编译时对类型和值进行计算的元编程库)。 14 | 15 | 策略类是由许多开发者开发的。Andrei Alexandrescu使策略类这个术语流行起来,他的书《现代C++设计》比我们的简短介绍(参见[AlexandrescuDesign])更加详细地介绍了策略类。 -------------------------------------------------------------------------------- /content/3/chapter20/0.tex: -------------------------------------------------------------------------------- 1 | 2 | 函数重载允许在多个函数中使用相同的函数名,需要通过参数类型区分这些函数即可: 3 | 4 | \begin{lstlisting}[style=styleCXX] 5 | void f(int); 6 | void f(char const*); 7 | \end{lstlisting} 8 | 9 | 使用函数模板,可以重载类型模式,如T的指针或Array: 10 | 11 | \begin{lstlisting}[style=styleCXX] 12 | template void f(T*); 13 | template void f(Array); 14 | \end{lstlisting} 15 | 16 | 鉴于类型特征的存在(第19章),希望使用基于模板参数的属性重载函数模板。例如: 17 | 18 | \begin{lstlisting}[style=styleCXX] 19 | template void f(Number); // only for numbers 20 | template void f(Container); // only for containers 21 | \end{lstlisting} 22 | 23 | 但C++目前没有提供直接的方法来表示基于类型属性的重载,上面的两个f函数模板实际上是同一个函数模板的声明,因为在比较两个函数模板时,模板参数的名称会忽略。 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 | -------------------------------------------------------------------------------- /content/3/chapter20/1.tex: -------------------------------------------------------------------------------- 1 | 2 | 函数模板重载的一个常见动机是,基于所涉及的类型知识提供算法的更特化版本。使用简单的swap()来交换两个值: 3 | 4 | \begin{lstlisting}[style=styleCXX] 5 | template 6 | void swap(T& x, T& y) 7 | { 8 | T tmp(x); 9 | x = y; 10 | y = tmp; 11 | } 12 | \end{lstlisting} 13 | 14 | 这个实现涉及三个复制操作。对于某些类型,可以提供更有效的swap()操作,例如Array将其数据存储为指向数组内容和长度的指针: 15 | 16 | \begin{lstlisting}[style=styleCXX] 17 | template 18 | void swap(Array& x, Array& y) 19 | { 20 | swap(x.ptr, y.ptr); 21 | swap(x.len, y.len); 22 | } 23 | \end{lstlisting} 24 | 25 | swap()的两个实现都将交换两个Array对象的内容。但后一种实现更高效,因为它使用了Array的属性(特别是ptr和len及其各种成员),而这些属性对于其他类型不可用。 26 | 27 | \begin{tcolorbox}[colback=webgreen!5!white,colframe=webgreen!75!black] 28 | \hspace*{0.75cm}swap()的一个更好的选择是使用std::move()来避免在主模板中进行复制,这里提出的替代方案适用范围更广。 29 | \end{tcolorbox} 30 | 31 | 因此,后一个函数模板(在概念上)比前一个更特化,前一个函数模板接受的类型子集执行相同的操作。基于函数模板的部分排序规则,第二个函数模板也更加特化了(参见16.2.2节),因此编译器将选择更特化(因此更高效)的函数模板(例如,Array参数),而当更特化的版本不适用时,会回退到更通用(可能更低效)的算法。 32 | 33 | 为通用算法引入更特化的设计和优化方法称为算法特化。更特化的版本应用于泛型算法的有效输入子集,根据特定的类型或类型的属性可以识别这个子集,其通常比泛型算法的一般实现更有效。 34 | 35 | 对于实现算法特化至关重要,当有更特化的变量适用时,调用者无需了解这些变量的存在就会自动选择。swap()的示例中,这是通过使用通用函数模板(第一个swap())重载(概念上)更特化的函数模板(第二个swap())来实现的,并确保更特化的函数模板根据C++的部分排序规则也更特化。 36 | 37 | 并不是所有概念上更特化的算法版本,都可以直接转换为提供正确的部分排序行为的函数模板。对于下一个示例,advanceIter()函数(类似于C++标准库中的std::advance()),将迭代器x向前移动n步。这个通用算法可以操作输入迭代器: 38 | 39 | \begin{lstlisting}[style=styleCXX] 40 | template 41 | void advanceIter(InputIterator& x, Distance n) 42 | { 43 | while (n > 0) { // linear time 44 | ++x; 45 | --n; 46 | } 47 | } 48 | \end{lstlisting} 49 | 50 | 对于提供随机访问操作的特定迭代器类,可以提供更高效的实现: 51 | 52 | \begin{lstlisting}[style=styleCXX] 53 | template 54 | void advanceIter(RandomAccessIterator& x, Distance n) { 55 | x += n; // constant time 56 | } 57 | \end{lstlisting} 58 | 59 | 定义这两个函数模板将导致编译器报错,因为仅根据模板参数名称,而不同的函数模板不可重载。本章的其余部分将讨论模拟重载这些函数模板所需的技术。 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /content/3/chapter20/2.tex: -------------------------------------------------------------------------------- 1 | 2 | 算法特化的方法是通过唯一“标记”来识别不同的实现,为了处理advanceIter()的问题,可以使用标准库的迭代器类别标签类型(定义如下)来识别advanceIter()算法的两个实现变体: 3 | 4 | \begin{lstlisting}[style=styleCXX] 5 | template 6 | void advanceIterImpl(Iterator& x, Distance n, std::input_iterator_tag) 7 | { 8 | while (n > 0) { // linear time 9 | ++x; 10 | --n; 11 | } 12 | } 13 | 14 | template 15 | void advanceIterImpl(Iterator& x, Distance n, 16 | std::random_access_iterator_tag) { 17 | x += n; // constant time 18 | } 19 | \end{lstlisting} 20 | 21 | advanceIter()函数模板只是简单地转发参数和相应的标签: 22 | 23 | \begin{lstlisting}[style=styleCXX] 24 | template 25 | void advanceIter(Iterator& x, Distance n) 26 | { 27 | advanceIterImpl(x, n, 28 | typename 29 | std::iterator_traits::iterator_category()); 30 | } 31 | \end{lstlisting} 32 | 33 | 特征类std::iterator\_traits通过其成员类型iterator\_category为迭代器提供了一个类别。迭代器类别是前面提到的\_tag类型之一,其指定了迭代器类型的类型。C++标准库中,可用的标签定义如下,使用继承来反映标签描述类别是从另一个标签派生而来的: 34 | 35 | \begin{tcolorbox}[colback=webgreen!5!white,colframe=webgreen!75!black] 36 | \hspace*{0.75cm}类别反映了概念,概念的继承称为精炼。在附录E中详细介绍了概念和精炼。 37 | \end{tcolorbox} 38 | 39 | \begin{lstlisting}[style=styleCXX] 40 | namespace std { 41 | struct input_iterator_tag { }; 42 | struct output_iterator_tag { }; 43 | struct forward_iterator_tag : public input_iterator_tag { }; 44 | struct bidirectional_iterator_tag : public forward_iterator_tag { }; 45 | struct random_access_iterator_tag : public bidirectional_iterator_tag { }; 46 | } 47 | \end{lstlisting} 48 | 49 | 利用标签调度的关键在于标签之间的关系。advanceIterImpl()的两个变体标记为std::input\_iterator\_tag和std::random\_access\_iterator\_tag,并且std::random\_access\_iterator\_tag继承自std::input\_iterator\_tag,所以使用随机访问迭代器调用advanceIterImpl()时,普通函数都会优先使用更特化的算法版本(使用std::random\_access\_iterator\_tag)。标签调度依赖于从单一的、主要的函数模板委派到一组\_impl变体,对这些变体进行标记,这样普通的函数重载将选择适用于给定模板参数最特化的算法。 50 | 51 | 当算法使用的属性具有体系结构,以及提供这些标记值的现有特征集时,标签调度工作得很好。当算法特化依赖于特定的类型属性时,就不那么方便了,比如类型T是否具有普通的复制赋值操作符。因此,我们需要更强大的技术。 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 | -------------------------------------------------------------------------------- /content/3/chapter20/6.tex: -------------------------------------------------------------------------------- 1 | C++标准库为输入、输出、前向、双向和随机访问迭代器标签提供了迭代器标签,在本文中已经使用了这些标签。这些迭代器标签是标准迭代器特征(std::iterator\_traits)和放置在迭代器上的要求的一部分,所以可以安全地用于标签调度。 2 | 3 | C++11标准库std::enable\_if类模板提供了与EnableIfT类模板相同的功能。唯一的区别是标准使用了名为type的小写成员类型,而不是大写类型。 4 | 5 | C++标准库的很多地方都使用了特化算法。std::advance()和std::distance()都有几个基于其迭代器参数类别的版本。尽管最近一些已经采用std::enable\_if来实现这种特化算法,但大多数标准库实现倾向于使用标签调度。许多C++标准库实现,也在内部使用这些技术来实现各种标准算法的特化算法。std::copy()可以实现std::memcpy()或std::memmove(),当迭代器指向连续的内存并且它们的值类型有普通的复制赋值操作符时。可以对std::fill()进行优化,以实现std::memset(),并且当已知类型具有普通析构函数时,各种算法可以避免调用析构函数。这些特化算法不像std::advance()或std::distance()那样由C++标准强制规定,但是实现者出于效率的原因会有选择的进行提供。 6 | 7 | 正如在第8.4节中介绍的那样,C++标准库还在其要求中使用std::enable\_if<>或类似的基于SFINAE的技术。std::vector有一个构造函数模板,允许从迭代器序列构建vector: 8 | 9 | \begin{lstlisting}[style=styleCXX] 10 | template 11 | vector(InputIterator first, InputIterator second, 12 | allocator_type const& alloc = allocator_type()); 13 | \end{lstlisting} 14 | 15 | 要求“若构造函数调用的,是不符合输入迭代器条件的InputIterator类型,那么构造函数将不参与重载解析”(参见[C++11]23.2.3第14段)。这个措辞非常模糊,可以使用当时最有效的技术来实现它,在将它添加到标准中时,应该会使用std::enable\_if<>实现。 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 | -------------------------------------------------------------------------------- /content/3/chapter20/7.tex: -------------------------------------------------------------------------------- 1 | 标签调度在C++中存在很久了,其用于STL的原始实现中(参见[StepanovLeeSTL]),并且经常与特征一起使用。SFINAE和EnableIf比较新:本书的第一版(参见[VandevoordeJosuttisTemplates1st])介绍了术语SFINAE,并用于检测成员类型的存在。 2 | 3 | “enable if”技术和术语最早是由Jaakko Jarvi、Jeremiah Will-cock、Howard Hinnant和Andrew Lumsdaine在[OverloadingProperties]中提出的,描述了EnableIf模板,如何用EnableIf(和DisableIf)对实现函数重载,以及如何用类模板偏特化来使用EnableIf。从那时起,EnableIf和类似的技术在高级模板库(包括C++标准库)的实现中无处不在。此外,这些技术的流行激发了C++11扩展SFINAE的行为(参见15.7节)。Peter Dimov是第一个注意到函数模板的默认模板参数(另一个C++11特性)使得可以在构造函数模板中使用EnableIf,而无需引入另一个函数参数。 4 | 5 | 概念语言特性(附录E中描述)有望在C++17后的C++标准中出现,其可使许多涉及EnableIf的技术过时。与此同时,C++17的constexpr if语句(参见第8.5节和第20.3.3节)也在逐渐减少它们在现代模板库中的存在感。 -------------------------------------------------------------------------------- /content/3/chapter21/0.tex: -------------------------------------------------------------------------------- 1 | 可能没有理由认为模板和继承会以友善的方式进行交互。若有什么区别的话,可以在第13章了解到,从依赖基类的派生迫使我们小心地处理非限定名称。然而,一些有趣的技术结合了这两个特性,包括奇异递归模板模式(CRTP)和混合类。本章中,将介绍其中的一些。 -------------------------------------------------------------------------------- /content/3/chapter21/5.tex: -------------------------------------------------------------------------------- 1 | Bill Gibbons是将EBCO引入C++编程语言的主要发起人。Nathan Myers使它流行起来,并提出了一个类似于BaseMemberPair的模板,从而可以更好地利用它。Boost库包含相当复杂的模板,称为compressed\_pair,其解决了我们在本章中MyClass模板的一些问题。boost::compressed\_pair也可以用来代替BaseMemberPair。 2 | 3 | CRTP至少从1991年开始使用。然而,James Coplien是第一个将它们正式描述为模式的人(参见[CoplienCRTP])。从那时起,CRTP的许多应用已经公开发表,参数化继承有时错误地等同于CRTP。CRTP根本不要求对派生进行参数化,而且许多形式的参数化继承不符合CRTP。因为Barton和Nackman技巧经常将CRTP与友元名注入结合使用(后者是Barton-Nackman技巧的重要组成部分),所以CRTP有时会与Barton-Nackman技巧相混淆(参见第21.2.1节)。使用CRTP和Barton-Nackman技巧来提供操作符实现,与Boost.Operators库([BoostOperators])使用相同的实现方法,提供了一组通用的操作符定义。类似地,我们对迭代器门面的处理与Boost.Iterator 库([BoostIterator])类似,它为派生类型提供了丰富的、符合标准库的迭代器接口,派生类型提供了一些核心迭代器操作(相等、解引用、移动),还解决了涉及代理迭代器的棘手问题(为了保证示例的简洁,我们在这里没有解决这个问题)。我们的ObjectCounter示例与Scott Meyers在[MeyersCounting]中使用的技术完全相同。 4 | 5 | 至少从1986年([MoonFlavors])开始,混合的概念就出现在面向对象编程中,作为一种将小块功能引入OO类的方式。在第一个C++标准发布后不久,在C++中使用混合类模板就开始流行起来,两篇论文([SmaragdakisBatoryMixins]和[EiseneckerBlinnCzarnecki])描述了目前常用的混合类方法。从那时起,混合类成为C++库设计中的一种流行技术。 6 | 7 | 命名模板参数用于简化Boost库中的某些类模板。Boost使用元编程创建具有与PolicySelector类似属性的类型(但不使用虚继承),这里介绍的更简单的替代方案是由Vandevoorde编写。 -------------------------------------------------------------------------------- /content/3/chapter21/images/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Cpp-Templates-2nd/579a1728a33ba4e1f410354a4a427e1668f37190/content/3/chapter21/images/1.png -------------------------------------------------------------------------------- /content/3/chapter21/images/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Cpp-Templates-2nd/579a1728a33ba4e1f410354a4a427e1668f37190/content/3/chapter21/images/2.png -------------------------------------------------------------------------------- /content/3/chapter21/images/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Cpp-Templates-2nd/579a1728a33ba4e1f410354a4a427e1668f37190/content/3/chapter21/images/3.png -------------------------------------------------------------------------------- /content/3/chapter21/images/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Cpp-Templates-2nd/579a1728a33ba4e1f410354a4a427e1668f37190/content/3/chapter21/images/4.png -------------------------------------------------------------------------------- /content/3/chapter22/0.tex: -------------------------------------------------------------------------------- 1 | 第18章描述了C++中静态多态(通过模板)和动态多态(通过继承和虚函数)。这两种多态为编写程序提供了强大的抽象能力,但也有各自的缺点:静态多态提供了与非多态代码相同的性能,可以在运行时使用的类型集在编译时需要已知。另一方面,通过继承的动态多态性允许多态函数在编译时处理不确定的类型,但是因为类型必须从公共基类继承,所以灵活性较低。 2 | 3 | 本章介绍了如何在C++中搭建静态和动态多态性之间的桥梁,在18.3节中讨论了每个模型中优点:更小的可执行代码大小和(几乎)完全编译的动态多态性的特性,以及静态多态性的接口灵活性,允许内置类型无缝衔接的工作。作为示例,我们将构建标准库function<>模板的简化版本。 -------------------------------------------------------------------------------- /content/3/chapter22/1.tex: -------------------------------------------------------------------------------- 1 | 2 | 函数对象对于为模板提供可定制的行为,下面的函数模板枚举从0到某个值的整数值,并将每个值提供给给定的函数对象f: 3 | 4 | \hspace*{\fill} \\ %插入空行 5 | \noindent 6 | \textit{bridge/forupto1.cpp} 7 | \begin{lstlisting}[style=styleCXX] 8 | #include 9 | #include 10 | 11 | template 12 | void forUpTo(int n, F f) 13 | { 14 | for (int i = 0; i != n; ++i) 15 | { 16 | f(i); // call passed function f for i 17 | } 18 | } 19 | 20 | void printInt(int i) 21 | { 22 | std::cout << i << ’ ’; 23 | } 24 | 25 | int main() 26 | { 27 | std::vector values; 28 | 29 | // insert values from 0 to 4: 30 | forUpTo(5, 31 | [&values](int i) { 32 | values.push_back(i); 33 | }); 34 | 35 | // print elements: 36 | forUpTo(5, 37 | printInt); // prints 0 1 2 3 4 38 | std::cout << ’\n’; 39 | } 40 | \end{lstlisting} 41 | 42 | forUpTo()函数模板可以与函数对象一起使用,包括Lambda、函数指针、函数操作符或转换为函数指针/引用的类,每次使用forUpTo()都可能产生不同的实例化。示例函数模板非常小,但若模板很大,这些实例化可能会增加代码量。 43 | 44 | 限制代码量增加的方法,是将函数模板转换为不需要实例化的非模板。可以尝试使用函数指针: 45 | 46 | \hspace*{\fill} \\ %插入空行 47 | \noindent 48 | \textit{bridge/forupto2.hpp} 49 | \begin{lstlisting}[style=styleCXX] 50 | void forUpTo(int n, void (*f)(int)) 51 | { 52 | for (int i = 0; i != n; ++i) 53 | { 54 | f(i); // call passed function f for i 55 | } 56 | } 57 | \end{lstlisting} 58 | 59 | 虽然这个实现在传递printInt()时正常工作,但在传递Lambda时将产生错误: 60 | 61 | \begin{lstlisting}[style=styleCXX] 62 | forUpTo(5, 63 | printInt); // OK: prints 0 1 2 3 4 64 | 65 | forUpTo(5, 66 | [&values](int i) { // ERROR: lambda not convertible to a function pointer 67 | values.push_back(i); 68 | }); 69 | \end{lstlisting} 70 | 71 | 标准库的类模板std::function<>允许使用forUpTo()的替代表达式: 72 | 73 | \hspace*{\fill} \\ %插入空行 74 | \noindent 75 | \textit{bridge/forupto3.hpp} 76 | \begin{lstlisting}[style=styleCXX] 77 | #include 78 | void forUpTo(int n, std::function f) 79 | { 80 | for (int i = 0; i != n; ++i) 81 | { 82 | f(i); // call passed function f for i 83 | } 84 | } 85 | \end{lstlisting} 86 | 87 | std::function<>的模板参数是一个函数类型,描述了函数对象将接收的参数类型和应该产生的返回类型,就像函数指针描述参数和结果类型一样。 88 | 89 | forUpTo()提供了静态多态的一些方面——使用合适的函数操作符处理一组无界类型的能力,包括函数指针、Lambda和类——而其本身仍然是非模板函数,只有一个实现。可以使用称为类型擦除的技术来实现,这种技术弥合了静态和动态多态之间的差异。 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 | -------------------------------------------------------------------------------- /content/3/chapter22/3.tex: -------------------------------------------------------------------------------- 1 | FunctorBridge类模板负责底层函数对象的所有权和操作,实现为一个抽象基类,是FunctionPtr动态多态性的基础: 2 | 3 | \hspace*{\fill} \\ %插入空行 4 | \noindent 5 | \textit{bridge/functorbridge.hpp} 6 | \begin{lstlisting}[style=styleCXX] 7 | template 8 | class FunctorBridge 9 | { 10 | public: 11 | virtual ~FunctorBridge() { 12 | } 13 | virtual FunctorBridge* clone() const = 0; 14 | virtual R invoke(Args... args) const = 0; 15 | }; 16 | \end{lstlisting} 17 | 18 | FunctorBridge提供了通过虚函数操作存储函数对象所需的基本操作:析构函数、执行复制的clone()操作和调用。操作调用基础函数对象。不要忘记将clone()和invoke()定义为const成员函数。 19 | 20 | \begin{tcolorbox}[colback=webgreen!5!white,colframe=webgreen!75!black] 21 | \hspace*{0.75cm}将invoke()设为const,为的是避免通过const函数ptr对象调用非const操作符()重载,这与开发者的预期不符。 22 | \end{tcolorbox} 23 | 24 | 使用这些虚函数,可以实现FunctionPtr的复制构造函数和函数调用操作符: 25 | 26 | \hspace*{\fill} \\ %插入空行 27 | \noindent 28 | \textit{bridge/functionptr-cpinv.hpp} 29 | \begin{lstlisting}[style=styleCXX] 30 | template 31 | FunctionPtr::FunctionPtr(FunctionPtr const& other) 32 | : bridge(nullptr) 33 | { 34 | if (other.bridge) { 35 | bridge = other.bridge->clone(); 36 | } 37 | } 38 | 39 | template 40 | R FunctionPtr::operator()(Args... args) const 41 | { 42 | return bridge->invoke(std::forward(args)...); 43 | } 44 | \end{lstlisting} 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 | -------------------------------------------------------------------------------- /content/3/chapter22/4.tex: -------------------------------------------------------------------------------- 1 | FunctorBridge的每个实例都是抽象类,因此其派生类负责提供虚函数的实现。为了支持潜在函数对象的完整范围(无界集合),需要无界的派生类。可以通过对派生类存储的函数对象类型,进行参数化来实现: 2 | 3 | \hspace*{\fill} \\ %插入空行 4 | \noindent 5 | \textit{bridge/specificfunctorbridge.hpp} 6 | \begin{lstlisting}[style=styleCXX] 7 | template 8 | class SpecificFunctorBridge : public FunctorBridge { 9 | Functor functor; 10 | public: 11 | template 12 | SpecificFunctorBridge(FunctorFwd&& functor) 13 | : functor(std::forward(functor)) { 14 | } 15 | virtual SpecificFunctorBridge* clone() const override { 16 | return new SpecificFunctorBridge(functor); 17 | } 18 | virtual R invoke(Args... args) const override { 19 | return functor(std::forward(args)...); 20 | } 21 | }; 22 | \end{lstlisting} 23 | 24 | 每个SpecificFunctorBridge实例存储一个函数对象的副本(类型是Functor),可以调用、复制或销毁(析构函数中隐式进行)。当一个FunctionPtr初始化为新的函数对象时,特定的functorbridge实例将创建,完成FunctionPtr示例: 25 | 26 | \hspace*{\fill} \\ %插入空行 27 | \noindent 28 | \textit{bridge/functionptr-init.hpp} 29 | \begin{lstlisting}[style=styleCXX] 30 | template 31 | template 32 | FunctionPtr::FunctionPtr(F&& f) 33 | : bridge(nullptr) 34 | { 35 | using Functor = std::decay_t; 36 | using Bridge = SpecificFunctorBridge; 37 | bridge = new Bridge(std::forward(f)); 38 | } 39 | \end{lstlisting} 40 | 41 | 虽然FunctionPtr构造函数在函数对象类型F上模板化的,但该类型只有特定的SpecificFunctorBridge特化才知晓(由Bridge类型别名描述)。当为新Bridge实例分配数据成员bridge,因为类型从Bridge *到FunctorBridge *,所以关于特定类型F的信息将丢失。 42 | 43 | \begin{tcolorbox}[colback=webgreen!5!white,colframe=webgreen!75!black] 44 | \hspace*{0.75cm}虽然类型可以通过dynamic\_cast查询,FunctionPtr类使bridge指针私有化,所以FunctionPtr的外部代码不能访问类型本身。 45 | \end{tcolorbox} 46 | 47 | 这种类型信息的丢失解释了,为什么使用术语类型擦除,来描述静态和动态多态之间的桥接技术。 48 | 49 | 该实现的特点是使用std::decay(请参阅第D.4节)来产生Functor类型,这使得推导类型F适合存储,例如:通过将对函数类型的引用转换成函数指针类型,并移除const、volatile和引用类型的限定。 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 | -------------------------------------------------------------------------------- /content/3/chapter22/6.tex: -------------------------------------------------------------------------------- 1 | 类型擦除提供了静态多态和动态多态的一些优点,使用类型擦除生成的代码的性能更接近于动态多态,因为两者都通过虚函数使用动态分配。因此,静态多态的一些传统优势,比如:编译器内联调用的能力,可能会失去。这种性能损失是否明显取决于具体应用,但通常可以通过调用函数中的工作量,相对于虚函数调用的耗时来判断:若两者接近(使用FunctionPtr简单地将两个整数相加),类型擦除的执行速度可能比静态多态版本慢得多。另一方面,若函数调用执行大量的工作——查询数据库、对容器进行排序或更新用户接口——则类型擦除的开销可能无法评估。 -------------------------------------------------------------------------------- /content/3/chapter22/7.tex: -------------------------------------------------------------------------------- 1 | Kevlin Henney在C++中推广了类型擦除,引入了any类型[HenneyValuedConversions],该类型后来成为Boost库[BoostAny],并成为C++17标准的一部分。在Boost.Function[BoostFunction]中对该技术进行了一些改进,其应用了各种性能和代码量优化,最终成为std::function<>。然而,每一个早期库只处理一组操作:any是一个简单的值类型,只有一个复制和强制转换操作;函数添加了调用。 2 | 3 | 之后的工作,比如Boost.TypeErasure库[BoostTypeErasure]和Adobe的Poly库[AdobePoly],使用模板元编程技术,允许用户形成具有特定功能列表的类型擦除值。例如,下面的类型(使用Boost.TypeErasure库)处理复制构造、类类型操作和打印输出流: 4 | 5 | \begin{lstlisting}[style=styleCXX] 6 | using AnyPrintable = any, 7 | typeid_<>, 8 | ostreamable<> 9 | >>; 10 | \end{lstlisting} -------------------------------------------------------------------------------- /content/3/chapter23/0.tex: -------------------------------------------------------------------------------- 1 | 元编程由“程序编程”构成,也就是我们编写编程系统执行的代码,以生成实现真正想要的功能的新代码。通常,术语元编程意味着一种自反属性:元编程组件是程序的一部分,其可生成一段代码(即,程序的另一段或不同的代码)。 2 | 3 | 为什么使用元编程?与大多数其他编程技术一样,目标是用更少的工作实现更多的功能,其中的工作可以通过代码大小、维护成本等来衡量。元编程的特点是在转换时产生一些用户定义的计算。潜在的动机通常是性能(转换时计算的东西通常可以优化掉)或接口简单(元程序通常比它扩展的要短)或两者兼有。 4 | 5 | 元编程通常依赖于特征和类型函数的概念,如第19章所述。因此,建议在研究这一章之前先熟悉一下第19章。 -------------------------------------------------------------------------------- /content/3/chapter23/2.tex: -------------------------------------------------------------------------------- 1 | 前面描述了基于constexpr计算的值元编程和基于递归模板实例化的类型元编程,两项在现代C++中都有使用,涉及不同的方法来驱动计算。值元编程也可以通过递归模板实例化来驱动,C++11中引入constexpr函数之前,这是首选的机制。下面的代码使用递归实例化计算整数的平方根: 2 | 3 | \hspace*{\fill} \\ %插入空行 4 | \noindent 5 | \textit{bridge/sqrt1.hpp} 6 | \begin{lstlisting}[style=styleCXX] 7 | // primary template to compute sqrt(N) 8 | template 9 | struct Sqrt { 10 | // compute the midpoint, rounded up 11 | static constexpr auto mid = (LO+HI+1)/2; 12 | // search a not too large value in a halved interval 13 | static constexpr auto value = (N::value 14 | : Sqrt::value; 15 | }; 16 | 17 | // partial specialization for the case when LO equals HI 18 | template 19 | struct Sqrt { 20 | static constexpr auto value = M; 21 | }; 22 | \end{lstlisting} 23 | 24 | 这个元程序使用了与第23.1.1节中整数平方根constexpr函数使用相同的算法,连续将已知包含平方根的区间减半。元函数的输入是非类型模板参数,跟踪区间边界的“局部变量”也改为非类型模板参数。显然,这是一种远不如constexpr函数友好的方法,但我们仍会分析这段代码,以检查它如何使用编译器的资源。 25 | 26 | 我们可以看到元编程的计算引擎可能有很多选择,这并不是可以考虑的唯一方面。相反,我们倾向于选择C++元编程解决方案必须在以下三个方面做出权衡: 27 | 28 | \begin{itemize} 29 | \item 30 | 计算 31 | 32 | \item 33 | 反射 34 | 35 | \item 36 | 生成 37 | \end{itemize} 38 | 39 | 反射是一种以编程方式检查程序特性的能力。生成是指为程序生成代码的能力。 40 | 41 | 已经看到了两个计算选项:递归实例化和constexpr计算,我们在类型特征中找到了部分解决方案(参见第19.6.1节)。尽管可用的特性支持相当多的高级模板技术,但远不能涵盖语言中反射功能所需的功能。给定一个类类型,应用程序希望以编程的方式探索该类的成员。当前的特性基于模板实例化,C++可以提供语言工具或“固有”库组件 42 | 43 | \begin{tcolorbox}[colback=webgreen!5!white,colframe=webgreen!75!black] 44 | \hspace*{0.75cm}C++标准库中提供的一些特性已经依赖于编译器的某些合作(通过非标准的“固有”操作符)。参见19.10节。 45 | \end{tcolorbox} 46 | 47 | 生成在编译时包含反射信息的类模板实例,这种方法适合基于递归模板实例化的计算。但类模板实例占用大量编译器存储空间,而这些存储空间直到编译结束时才会释放(否则会导致编译时间大大增加)。另一种选择是引入一种新的标准类型来表示反射信息,该选择有望与“计算”维度的constexpr评估选项配对。17.9节讨论了这个选项(C++标准化委员会正在积极调研)。 48 | 49 | 第17.9节还展示了实现强大代码生成的未来方法。在现有C++语言中创建一个灵活的、通用的、开发者友好的代码生成机制,仍然是一个挑战。实例化模板是一种“代码生成”机制,编译器在扩展对内联小函数的调用方面已经足够可靠,这种机制可以用作代码生成的载体。这些观察结果正是上面DotProductT示例的基础,结合更强大的反射功能,现有技术已经可以完全实现元编程。 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 | -------------------------------------------------------------------------------- /content/3/chapter23/4.tex: -------------------------------------------------------------------------------- 1 | Sqrt<>示例演示了模板元程序可以有: 2 | 3 | \begin{itemize} 4 | \item 5 | 状态变量:模板参数 6 | 7 | \item 8 | 循环构造:通过递归 9 | 10 | \item 11 | 执行路径选择:通过使用条件表达式或特化 12 | 13 | \item 14 | 整数运算 15 | \end{itemize} 16 | 17 | 若不限制递归实例化的数量和允许状态变量的数量,这足以进行任何计算,但使用模板可能不方便这样做。此外,由于模板实例化需要大量的编译器资源,大量的递归实例化会降低编译器的处理速度,甚至耗尽可用的资源。C++标准建议(但不是强制)至少允许1024级递归实例化,这对于大多数(但肯定不是所有)模板元编程任务来说已经足够了。 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 | -------------------------------------------------------------------------------- /content/3/chapter23/6.tex: -------------------------------------------------------------------------------- 1 | 2 | 早期的C++,枚举值是在类声明中创建“真正的常量”(称为常量表达式)作为命名成员的唯一机制,可以定义一个Pow3元程序来计算3的幂: 3 | 4 | \hspace*{\fill} \\ %插入空行 5 | \noindent 6 | \textit{meta/pow3enum.hpp} 7 | \begin{lstlisting}[style=styleCXX] 8 | // primary template to compute 3 to the Nth 9 | template 10 | struct Pow3 { 11 | enum { value = 3 * Pow3::value }; 12 | }; 13 | 14 | // full specialization to end the recursion 15 | template<> 16 | struct Pow3<0> { 17 | enum { value = 1 }; 18 | }; 19 | \end{lstlisting} 20 | 21 | C++98的标准化引入了类内静态常量初始化器的概念,因此Pow3元程序可以这样写: 22 | 23 | \hspace*{\fill} \\ %插入空行 24 | \noindent 25 | \textit{meta/pow3const.hpp} 26 | \begin{lstlisting}[style=styleCXX] 27 | // primary template to compute 3 to the Nth 28 | template 29 | struct Pow3 { 30 | static int const value = 3 * Pow3::value; 31 | }; 32 | 33 | // full specialization to end the recursion 34 | template<> 35 | struct Pow3<0> { 36 | static int const value = 1; 37 | }; 38 | \end{lstlisting} 39 | 40 | 这个版本有一个缺点:静态常量成员是左值(参见附录B) 41 | 42 | \begin{lstlisting}[style=styleCXX] 43 | void foo(int const&); 44 | \end{lstlisting} 45 | 46 | 将元程序的结果进行传递: 47 | 48 | \begin{lstlisting}[style=styleCXX] 49 | foo(Pow3<7>::value); 50 | \end{lstlisting} 51 | 52 | 编译器必须传递Pow3<7>::value的地址,这迫使编译器实例化和分配静态成员的定义,计算不再局限于纯粹的“编译时”效应。 53 | 54 | 枚举值不是左值(没有地址),当通过引用传递时,没有使用静态内存。就像将计算值作为文字传递一样。因此,本书的第一版在这类应用程序中更倾向于使用枚举常量。 55 | 56 | 然而,C++11引入了constexpr静态数据成员,而且这些成员不限于整型。它们没有解决上面提出的地址问题,尽管有这个缺点,不过目前是生成元程序结果的常用方法。其优点是具有正确的类型(与手动enum类型相反),并且当使用auto类型说明符声明静态成员时,可以推导出该类型。C++17添加了内联静态数据成员,这确实解决了上面提出的地址问题,并且可以与constexpr一起使用。 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /content/3/chapter24/0.tex: -------------------------------------------------------------------------------- 1 | 编程通常需要使用各种数据结构,元编程也不例外。对于类型元编程,主要数据结构是类型列表,是一个包含类型的列表。模板元程序可以对这些类型列表进行操作,最终生成可执行程序的一部分。本章中,将讨论如何使用类型列表。本章大多数涉及到类型列表的操作都使用模板元编程,所以建议先了解一下第23章的元编程。 -------------------------------------------------------------------------------- /content/3/chapter24/1.tex: -------------------------------------------------------------------------------- 1 | 类型列表是表示列表的类型,可以由模板元程序操作。提供了与列表相关联的操作:迭代列表中的元素(类型)、添加元素或删除元素。类型列表与大多数运行时数据结构(如std::list)不同,其不允许修改。向std::list中添加一个元素会改变列表本身,而这个改变会让程序中有权访问该列表的操作观察到。另一方面,向类型列表添加一个元素不会改变原始的打字员:向现有的类型列表员添加一个元素会创建一个新的类型列表,而非对原始类型列表的修改。熟悉函数式编程语言(如Scheme、ML和Haskell)的读者可能会感觉到,使用C++中的类型列表和使用这些语言中的列表非常相似。 2 | 3 | 类型列表通常实现为一个类模板特化,该特化在其模板参数中编码类型列表的内容(即它所包含的类型及其顺序)。类型列表的直接实现对参数包中的元素进行编码: 4 | 5 | \hspace*{\fill} \\ %插入空行 6 | \noindent 7 | \textit{typelist/typelist.hpp} 8 | \begin{lstlisting}[style=styleCXX] 9 | template 10 | class Typelist 11 | { 12 | }; 13 | \end{lstlisting} 14 | 15 | 类型列表的元素可以直接作为模板参数。空的类型列表表示为<>,只包含int的类型列表表示为,依此类推。下面是一个包含所有带符号整型的类型列表: 16 | 17 | \begin{lstlisting}[style=styleCXX] 18 | using SignedIntegralTypes = 19 | Typelist; 20 | \end{lstlisting} 21 | 22 | 操作这个类型列表通常需要将类型列表拆分成几个部分,通常是将列表中的第一个元素(头)与列表中的其余元素(尾)分开。从Front元函数从类型表中提取第一个元素: 23 | 24 | \hspace*{\fill} \\ %插入空行 25 | \noindent 26 | \textit{typelist/typelistfront.hpp} 27 | \begin{lstlisting}[style=styleCXX] 28 | template 29 | class FrontT; 30 | 31 | template 32 | class FrontT> 33 | { 34 | public: 35 | using Type = Head; 36 | }; 37 | 38 | template 39 | using Front = typename FrontT::Type; 40 | \end{lstlisting} 41 | 42 | 因此,FrontT::Type(简化地写成Front)将生成signed char。类似地,PopFront元函数从列表中删除第一个元素,实现将列表元素分为头和尾,然后从尾中的元素形成一个新的类型列表特化。 43 | 44 | \hspace*{\fill} \\ %插入空行 45 | \noindent 46 | \textit{typelist/typelistpopfront.hpp} 47 | \begin{lstlisting}[style=styleCXX] 48 | template 49 | class PopFrontT; 50 | 51 | template 52 | class PopFrontT> { 53 | public: 54 | using Type = Typelist; 55 | }; 56 | 57 | template 58 | using PopFront = typename PopFrontT::Type; 59 | \end{lstlisting} 60 | 61 | PopFront会产生类型列表: 62 | 63 | \begin{lstlisting}[style=styleCXX] 64 | Typelist 65 | \end{lstlisting} 66 | 67 | 还可以将所有元素捕获到模板参数包中,然后创建一个包含所有这些元素的新类型列表特化,从而将元素插入到类型列表的前面: 68 | 69 | \hspace*{\fill} \\ %插入空行 70 | \noindent 71 | \textit{typelist/typelistpushfront.hpp} 72 | \begin{lstlisting}[style=styleCXX] 73 | template 74 | class PushFrontT; 75 | 76 | template 77 | class PushFrontT, NewElement> { 78 | public: 79 | using Type = Typelist; 80 | }; 81 | 82 | template 83 | using PushFront = typename PushFrontT::Type; 84 | \end{lstlisting} 85 | 86 | 如我们所期望, 87 | 88 | \begin{lstlisting}[style=styleCXX] 89 | PushFront 90 | \end{lstlisting} 91 | 92 | 生成了: 93 | 94 | \begin{lstlisting}[style=styleCXX] 95 | Typelist 96 | \end{lstlisting} 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /content/3/chapter24/4.tex: -------------------------------------------------------------------------------- 1 | 包扩展(在第12.4.1节中有详细描述)是一种机制,可将类型列表迭代的工作转移给编译器。因为需要对列表中的每个元素应用相同的操作(进行包扩展),这里可以使用在第24.2.5节中开发的Transform算法。这为类型列表的Transform提供了一个特化算法(通过偏特化): 2 | 3 | \hspace*{\fill} \\ %插入空行 4 | \noindent 5 | \textit{typelist/variadictransform.hpp} 6 | \begin{lstlisting}[style=styleCXX] 7 | template class MetaFun> 8 | class TransformT, MetaFun, false> 9 | { 10 | public: 11 | using Type = Typelist::Type...>; 12 | }; 13 | \end{lstlisting} 14 | 15 | 这个实现将类型列表元素捕获到一个参数包elements中,使用模式typename MetaFun::Type进行包扩展,将元功能应用到Elements中的每个类型,并形成一个类型列表。因为它不需要递归,所以这个实现更简单,并且以一种相当直接的方式使用语言特性。因为只需要实例化Transform模板的一个实例,所以需要更少的模板实例化。该算法仍然需要线性数量的MetaFun实例化,这些实例化是算法的基础。 16 | 17 | 其他算法间接受益于使用包扩展,在第24.2.4节中描述的反向算法需要线性数量的PushBack实例化。在那一节中描述的类型列表PushBack的包扩展形式(需要单个实例化),Reverse是线性的。然而,描述的Reverse更一般的递归实现本身在实例化数量上是线性的,使Reverse的复杂度与数量成平方关系(2次方)! 18 | 19 | 选择给定索引列表中的元素以生成新的类型列表时,可以使用Pack扩展。Select元函数接受一个类型列表和一个包含该类型列表索引的值列表,然后生成一个包含由值列表指定元素的新类型列表: 20 | 21 | \hspace*{\fill} \\ %插入空行 22 | \noindent 23 | \textit{typelist/select.hpp} 24 | \begin{lstlisting}[style=styleCXX] 25 | template 26 | class SelectT; 27 | 28 | template 29 | class SelectT> 30 | { 31 | public: 32 | using Type = Typelist...>; 33 | }; 34 | 35 | template 36 | using Select = typename SelectT::Type; 37 | \end{lstlisting} 38 | 39 | 索引在参数包Indices中捕获,该参数包扩展以生成索引到给定类型列表的一系列NthElement类型,并在新参数列表中捕获结果。下面的例子说明了如何使用Select来反转输入列表: 40 | 41 | \begin{lstlisting}[style=styleCXX] 42 | using SignedIntegralTypes = 43 | Typelist; 44 | 45 | using ReversedSignedIntegralTypes = 46 | Select>; 47 | // produces Typelist 48 | \end{lstlisting} 49 | 50 | 包含另一个列表索引的非类型类型列表,称为索引列表(或索引序列),允许简化或消除递归计算。索引列表在25.3.4节有详细的描述。 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 | -------------------------------------------------------------------------------- /content/3/chapter24/6.tex: -------------------------------------------------------------------------------- 1 | 类型列表似乎在1998年第一个C++标准发布后不久就出现了。Krysztof Czarnecki和Ulrich Eisenecker在[CzarneckiEiseneckerGenProg]中引入了受LISP启发的Cons风格的整型常量列表,不过他们并没有考虑过泛型类型列表。 2 | 3 | Alexandrescu在他极具影响力的著作《现代C++设计》([AlexandrescuDesign])中使类型列表流行起来。最重要的是,Alexandrescu展示了类型列表的更多功能,可以用模板元编程和类型列表解决有趣的设计问题,并能使C++开发者可以使用这些技术。 4 | 5 | Abrahams和Gurtovoy在[AbrahamsGurtovoyMeta]中为元编程提供了所需的结构,描述了从C++标准库中提取的对类型列表、类型列表算法和相关组件的抽象:序列、迭代器、算法和(元)函数。库Boost.MPL([BoostMPL])可用来操作类型列表。 -------------------------------------------------------------------------------- /content/3/chapter25/0.tex: -------------------------------------------------------------------------------- 1 | 本书中,我们经常使用同构容器和类数组类型来说明模板的强大功能。这种同构结构扩展了C/C++数组的概念,在大多数应用程序中普遍存在。C++(和C)也有非同构的容器设施:类(或结构体),本章探讨元组类似于类和结构的方式聚合数据,包含int、double和std::string的元组类似于包含int、double和std::string成员的结构体,只是元组的元素是按位置引用的(如0、1、2),而不是通过名称引用。位置接口和从类型列表轻松构造元组的能力,使元组比结构体更适合与模板元编程技术一起使用。 2 | 3 | 元组的另一种方式是在可执行程序中显示类型列表,Typelist描述了可以在编译时操作的包含int, double和std::string的类型序列,Tuple描述了可以在运行时操作的int, double和std::string的存储。例如,下面的程序创建这样一个元组的实例: 4 | 5 | \begin{lstlisting}[style=styleCXX] 6 | template 7 | class Tuple { 8 | ... // implementation discussed below 9 | }; 10 | 11 | Tuple t(17, 3.14, "Hello, World!"); 12 | \end{lstlisting} 13 | 14 | 通常使用模板元编程和类型列表来生成可用于存储数据的元组,即使在上面的例子中选择了int、double和std::string作为元素类型,也可以用元程序创建元组中存储的类型集。 15 | 16 | 本章,我们将探索Tuple类模板的实现和操作,是std::Tuple类模板的简化版本。 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 | -------------------------------------------------------------------------------- /content/3/chapter25/2.tex: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | \subsubsubsection{25.2.1\hspace{0.2cm}比较} 5 | 6 | 元组是包含其他值的结构类型。要比较两个元组,比较元素就足够了,可以实现operator==的定义来比较两个定义的元素: 7 | 8 | \hspace*{\fill} \\ %插入空行 9 | \noindent 10 | \textit{typelist/tupleeq.hpp} 11 | \begin{lstlisting}[style=styleCXX] 12 | // basis case: 13 | bool operator==(Tuple<> const&, Tuple<> const&) 14 | { 15 | // empty tuples are always equivalent 16 | return true; 17 | } 18 | 19 | // basis case: 20 | bool operator==(Tuple<> const&, Tuple<> const&) 21 | { 22 | // empty tuples are always equivalent 23 | return true; 24 | } 25 | \end{lstlisting} 26 | 27 | 与许多关于类型列表和元组的算法一样,元素比较先访问头部元素,然后递归访问尾部元素。操作符!=、<、>、<=和>=的顺序类似。 28 | 29 | \subsubsubsection{25.2.2\hspace{0.2cm}输出} 30 | 31 | 本章将创建新的元组类型,因此能够在执行程序中看到这些元组是很有用的。以下操作符<{}<可以打印任何可打印的元组元素类型: 32 | 33 | \hspace*{\fill} \\ %插入空行 34 | \noindent 35 | \textit{typelist/tupleio.hpp} 36 | \begin{lstlisting}[style=styleCXX] 37 | #include 38 | 39 | void printTuple(std::ostream& strm, Tuple<> const&, bool isFirst = true) 40 | { 41 | strm << ( isFirst ? ’(’ : ’)’ ); 42 | } 43 | 44 | template 45 | void printTuple(std::ostream& strm, Tuple const& t, 46 | bool isFirst = true) 47 | { 48 | strm << ( isFirst ? "(" : ", " ); 49 | strm << t.getHead(); 50 | printTuple(strm, t.getTail(), false); 51 | } 52 | 53 | template 54 | std::ostream& operator<<(std::ostream& strm, Tuple const& t) 55 | { 56 | printTuple(strm, t); 57 | return strm; 58 | } 59 | \end{lstlisting} 60 | 61 | 现在,创建和显示元组就很容易: 62 | 63 | \begin{lstlisting}[style=styleCXX] 64 | std::cout << makeTuple(1, 2.5, std::string("hello")) << ’\n’; 65 | \end{lstlisting} 66 | 67 | 输出为 68 | 69 | \begin{tcblisting}{commandshell={}} 70 | (1, 2.5, hello) 71 | \end{tcblisting} 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /content/3/chapter25/4.tex: -------------------------------------------------------------------------------- 1 | 元组用于将一组相关值存储到单个值中,而不管这些值是什么类型,或有多少个相关值。某些情况下,可能需要解包这样的元组,例如:将其元素作为单独的参数传递给函数。举个简单的例子,可以取一个元组,并将其元素传递给变量print()操作,如第12.4节所述: 2 | 3 | \begin{lstlisting}[style=styleCXX] 4 | Tuple t("Pi", "is roughly", 5 | 3, ’\n’); 6 | print(t...); // ERROR: cannot expand a tuple; it isn’t a parameter pack 7 | \end{lstlisting} 8 | 9 | 如示例中所述,因为不是参数包,所以解包元组的尝试不会成功。可以使用索引列表实现相同的方法。下面的函数模板apply()接受一个函数和一个元组,然后用未打包的元组元素调用该函数: 10 | 11 | \hspace*{\fill} \\ %插入空行 12 | \noindent 13 | \textit{tuples/apply.hpp} 14 | \begin{lstlisting}[style=styleCXX] 15 | template 16 | auto applyImpl(F f, Tuple const& t, 17 | Valuelist) 18 | ->decltype(f(get(t)...)) 19 | { 20 | return f(get(t)...); 21 | } 22 | 23 | template 25 | auto apply(F f, Tuple const& t) 26 | ->decltype(applyImpl(f, t, MakeIndexList())) 27 | { 28 | return applyImpl(f, t, MakeIndexList()); 29 | } 30 | \end{lstlisting} 31 | 32 | applyImpl()函数模板接受给定的索引列表,并将元组中的元素展开为其函数对象参数f的参数列表。面向用户的apply()只负责构造初始索引列表,其允许将一个元组扩展为print()的参数: 33 | 34 | \begin{lstlisting}[style=styleCXX] 35 | Tuple t("Pi", "is roughly", 36 | 3, ’\n’); 37 | apply(print, t); // OK: prints Pi is roughly 3 38 | \end{lstlisting} 39 | 40 | C++17提供了一个类似的函数,可用于类元组类型。 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 | -------------------------------------------------------------------------------- /content/3/chapter25/7.tex: -------------------------------------------------------------------------------- 1 | 2 | 元组构造模板的实现,是对模板应用的一个实例。Boost.Tuple库[BoostTuple]是C++中最流行的一种元组的实现方式,并最终发展成C++11中的std::tuple。 3 | 4 | C++11之前,许多元组的实现都是基于递归对结构的思想,本书的第一版[VandevoordeJosuttisTemplates1st]通过“递归组合”阐明了这种方法。Andrei Alexandrescu在[AlexandrescuDesign]中提出了一个有趣的替代方案,他用类型列表的概念(如第24章所讨论的)作为元组的基础,将元组中的类型列表和字段列表清晰地分离开来。 5 | 6 | C++11引入了可变参数模板,其中参数包可以捕获元组的类型列表,消除了递归对的需要。包扩展和索引列表的概念[GregorJarviPowellVariadicTemplates]使递归模板实例化为更为简单、更有效的模板实例,从而使元组的使用门槛更低。索引列表对元组和类型列表算法的性能具有关键性影响,以至于编译器有一个内部别名模板,如\_\_make\_integer\_seq,会扩展为S,不需要额外的模板实例化,从而让std::make\_index\_sequence和make\_integer\_sequence使用起来更简单。 7 | 8 | Tuple是使用最广泛的异构容器,但它不唯一。Boost.Fusion库[BoostFusion]为通用容器提供了异构对应,如异构list、deque、set和map。提供了一个为异构集合编写算法的框架,使用与C++标准库本身相同的抽象类型和术语(例如,迭代器、序列和容器)。 9 | 10 | Boost.Hana[BoostHana]采纳了Boost中出现的许多想法。MPL Boost.MPL[BoostMPL]和Boost.Fusion,在C++11实现之前就设计和实现了,并且用C++11(和C++14)新的语言特性重新进行了设计,从而产生了一个优雅的库,其为异构计算提供了强大的和可组合的组件。 -------------------------------------------------------------------------------- /content/3/chapter26/0.tex: -------------------------------------------------------------------------------- 1 | 前一章中的元组将一些类型列表的值聚合为单个类值,使它们具有与简单结构体相同的功能。这自然就会想知道,联合的对应类型是什么:其将包含单个值,但该值将具有从一些可能类型中选择的类型。例如,一个数据库字段可能包含一个整数、浮点值、字符串或二进制对象,但在相应的时间内,其只能表示这些类型中的一种。 2 | 3 | 本章中,我们开发了一个类模板Variant,可以动态存储给定的一组值类型中的一个值,类似于C++17标准库的std::variant<>。Variant是可辨识联合,从而其值的类型是动态的,并提供了比C++联合更好的类型安全性。Variant本身是一个可变参数模板,可接受动态值可能具有的类型列表。 4 | 5 | \begin{lstlisting}[style=styleCXX] 6 | Variant field; 7 | \end{lstlisting} 8 | 9 | 可以存储整型、双精度或字符串,但只能存储其中一个值。 10 | 11 | \begin{tcolorbox}[colback=webgreen!5!white,colframe=webgreen!75!black] 12 | \hspace*{0.75cm}类型列表在声明Variant时是固定的,所以Variant是封闭的可辨识联合。开放的可辨识联合允许在创建时不知道有哪些类型的值存储在联合中。第22章中讨论的FunctionPtr类可以看作是一种开放可识别联合。 13 | \end{tcolorbox} 14 | 15 | 下面的程序演示了Variant的行为: 16 | 17 | \hspace*{\fill} \\ %插入空行 18 | \noindent 19 | \textit{variant/variant.cpp} 20 | \begin{lstlisting}[style=styleCXX] 21 | #include "variant.hpp" 22 | #include 23 | #include 24 | 25 | int main() 26 | { 27 | Variant field(17); 28 | if (field.is()) { 29 | std::cout << "Field stores the integer " 30 | << field.get() << ’\n’; 31 | } 32 | field = 42; // assign value of same type 33 | field = "hello"; // assign value of different type 34 | std::cout << "Field now stores the string ’" 35 | << field.get() << "’\n"; 36 | } 37 | \end{lstlisting} 38 | 39 | 输出: 40 | 41 | \begin{tcblisting}{commandshell={}} 42 | Field stores the integer 17 43 | Field now stores the string "hello" 44 | \end{tcblisting} 45 | 46 | 可以赋值给Variant任何类型的值,可以使用成员函数is()来测试Variant当前是否包含类型为T的值,然后使用成员函数get()获取存储值。 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 | -------------------------------------------------------------------------------- /content/3/chapter26/3.tex: -------------------------------------------------------------------------------- 1 | 对于Variant类型最基本的查询是,咨询动态值的类型是否是特定的类型T,并在其类型已知时访问动态值。下面定义的is()成员函数,可以确定Variant当前是否存储T类型的值: 2 | 3 | \hspace*{\fill} \\ %插入空行 4 | \noindent 5 | \textit{variant/variantis.hpp} 6 | \begin{lstlisting}[style=styleCXX] 7 | template 8 | template 9 | bool Variant::is() const 10 | { 11 | return this->getDiscriminator() == 12 | VariantChoice::Discriminator; 13 | } 14 | \end{lstlisting} 15 | 16 | 给定变量v, v.is()将确定v的动态值是否为int类型。检查很简单,将变量存储中的discriminator与对应VariantChoice基类的Discriminator值进行比较。 17 | 18 | 若正在寻找的类型(T)在列表中没有找到,VariantChoice基类将无法进行实例化,因为FindIndexOfT将不包含值成员,从而导致is()中的(故意的)编译失败。这可以防止用户在请求不可能存储在变体中的类型时,报错进行提示。 19 | 20 | get()成员函数提取对存储值的引用。必须提供要提取的类型(例如,v.get()),并且只有当变量的动态值确实为该类型时才有效: 21 | 22 | \hspace*{\fill} \\ %插入空行 23 | \noindent 24 | \textit{variant/variantget.hpp} 25 | \begin{lstlisting}[style=styleCXX] 26 | #include 27 | 28 | class EmptyVariant : public std::exception { 29 | }; 30 | 31 | template 32 | template 33 | T& Variant::get() & { 34 | if (empty()) { 35 | throw EmptyVariant(); 36 | } 37 | 38 | assert(is()); 39 | return *this->template getBufferAs(); 40 | } 41 | \end{lstlisting} 42 | 43 | 当Variant不存储值(标识值是0)时,get()会抛出EmptyVariant异常。由于异常,discriminator可以是0,在第26.4.3节中描述。从错误类型的Variant获取值的尝试,会通过断言进行检查。 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 | -------------------------------------------------------------------------------- /content/3/chapter26/7.tex: -------------------------------------------------------------------------------- 1 | Andrei Alexandrescu在一系列文章[alexandrescuatedunions]中详细介绍了可辨别联合。我们对Variant的处理使用了相同的技术,比如使用对齐缓冲区,就地存储和访问来提取值。一些差异是由于基础语言造成的:Andrei使用的是C++98,因此不能使用可变参数模板或继承构造函数。Andrei还花了相当多的时间来计算对齐,C++11直接引入了对齐,使得这项工作变得很简单。设计差异在于对辨别器的处理:虽然我们选择使用整型辨别器来指示,当前存储在变体中的是哪种类型,但Andrei使用了一种“静态虚函数表”的方法,使用函数指针来构造、复制、查询和销毁底层元素类型。有趣的是,这种静态虚函数表方法作为开放可辨别联合的优化技术影响更大,比如在第22.2节中开发的FunctionPtr模板,它是std::function实现的一种常见优化,以消除虚函数的使用。Boost的any类型([BoostAny])是另一种开放可辨别联合类型。C++17的标准库中,引入了std::any。 2 | 3 | 后来,Boost库([Boost])引入了几种可辨别联合类型,包括一种变体类型([BoostVariant]),它影响了本章中开发的联合类型。Boost.Variant([BoostVariant])的设计文档,包含了关于变量赋值异常安全的问题(称为“永不为空的约定”)和各种不完全令人满意的解决方案讨论记录。当标准库在C++17引入std::variant时,放弃了永不空的约定:通过允许std::variant状态可以变成valueless\_by\_exception,从而消除了为备份分配堆存储的需要,而std::variant状态会赋值给它抛出的新值,我们用空变量对这一行为进行了建模。 4 | 5 | 与我们的Variant模板不同,std::variant允许多个相同的模板参数(例如,std::variant)。在Variant中启用该功能需要在设计方面进行大量修改,包括添加一个方法来消除VariantChoice基类的歧义,以及在26.2节中描述的嵌套包扩展的替代方法。 6 | 7 | 本章描述的visit()操作变体在结构上与Andrei Alexandrescu在[AlexandrescuAdHocVisitor]中描述的临时访问器模式相同。Alexandrescu的特别访问器旨在简化针对一组已知派生类(描述为类型列表)检查某个公共基类指针的过程。该实现使用dynamic\_cast来针对类型列表中的每个派生类测试指针,当发现匹配时,使用派生类指针调用访问器。 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /content/3/chapter27/0.tex: -------------------------------------------------------------------------------- 1 | 本章中,我们将探索一种叫做表达式模板的模板编程技术,最初是为了支持数字数组类而发明的。 2 | 3 | 数字数组类支持对整个数组对象进行数字操作,可以将两个数组相加,结果包含的元素是参数数组中相应值的和。类似地,整个数组可以乘以一个标量,这意味着数组的每个元素都要扩展。自然,保留内置标量类型所熟悉的操作符表示法是可行的: 4 | 5 | \begin{lstlisting}[style=styleCXX] 6 | Array x(1000), y(1000); 7 | ... 8 | x = 1.2*x + x*y; 9 | \end{lstlisting} 10 | 11 | 对于专注于数据分析的开发者来说,在运行代码的平台上高效地计算这些表达式是至关重要的。使用本例中的运算符表示法来实现这一点,并不是一项简单的任务,但是表达式模板可以帮助我们。 12 | 13 | 表达式模板让人想起模板元编程,因为表达式模板有时依赖于深度嵌套的模板实例化,这与模板元程序中遇到的递归实例化相同。这两种技术最初都是为了支持高性能数组操作而开发的(请参阅第23.1.3节中使用模板展开循环的示例)。当然,技术是互补的。元编程对于固定大小的小型数组很方便,而表达式模板对于运行时大小为中型到大型数组的操作非常有效。 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 | -------------------------------------------------------------------------------- /content/3/chapter27/3.tex: -------------------------------------------------------------------------------- 1 | 为了证明表达式模板思想的复杂性,我们已经在数组操作上提高了性能。在跟踪表达式模板时,会发现许多小型内联函数相互调用,并且在调用堆栈上分配了许多小型表达式模板对象。优化器必须执行完整的内联和消除小对象,以产生与手动编码循环一样有效的代码。本书的第一版中,我们说过很少有编译器能够实现这样的优化。但从那时起,情况有了很大的改善,部分原因是因为该技术很受欢迎。 2 | 3 | 表达式模板技术不能解决涉及数组数值操作的所有问题。不适用于这种形式的矩阵-向量乘法 4 | 5 | x = A*x; 6 | 7 | 其中x是一个大小为n的列向量,a是一个n × n的矩阵。问题是必须使用一个临时元素,因为结果中的每个元素都依赖于原始x中的每个元素。但表达式模板循环会更新x的第一个元素,然后使用新计算的元素来计算第二个元素,这是错误的。另一个稍有不同的表达 8 | 9 | x = A*y; 10 | 11 | 另一方面,若x和y不是彼此的别名,则不需要临时变量,从而解决方案必须在运行时知道操作数的关系。这里建议创建一个表示表达式运行时的树型结构,而不是用表达式模板的类型对树型结构进行编码。这种方法是由Robert Davies的NewMat库首创的(见[NewMat])。早在表达式模板开发出来之前,这种方式就已经很出名了。 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 | -------------------------------------------------------------------------------- /content/3/chapter27/4.tex: -------------------------------------------------------------------------------- 1 | 表达式模板由Todd Veldhuizen和David Vandevoorde(Todd创造了这个词)独立开发,当时成员模板还不是C++编程语言的一部分(而且在当时看来,似乎永远不会添加到C++中)。这在实现赋值操作符时,出现了一些问题:无法为表达式模板对其进行参数化。解决这一问题的技术是在表达式模板中引入一个到复制类的转换操作符,该复制类用表达式模板参数化,但继承了一个只在元素类型中参数化的基类。然后,这个基类提供了赋值操作符,从而可以引用的(虚)copy\_to接口。 2 | 3 | 下面是这个机制的一个概述(以及本章中使用的模板名称): 4 | 5 | \begin{lstlisting}[style=styleCXX] 6 | template 7 | class CopierInterface { 8 | public: 9 | virtual void copy_to(Array>&) const; 10 | }; 11 | 12 | template 13 | class Copier : public CopierInterface { 14 | public: 15 | Copier(X const& x) : expr(x) { 16 | } 17 | virtual void copy_to(Array>&) const { 18 | // implementation of assignment loop 19 | ... 20 | } 21 | private: 22 | X const& expr; 23 | }; 24 | 25 | template> 26 | class Array { 27 | public: 28 | // delegated assignment operator 29 | Array& operator=(CopierInterface const& b) { 30 | b.copy_to(rep); 31 | }; 32 | ... 33 | }; 34 | 35 | template 36 | class A_mult { 37 | public: 38 | operator Copier>(); 39 | ... 40 | }; 41 | \end{lstlisting} 42 | 43 | 这给表达式模板增加了另一个层次的复杂性和运行时成本,但产生的性能优势在当时还是令人印象深刻。 44 | 45 | C++标准库包含了一个类模板valarray,可以证明本章开发的Array模板所使用的技术正确。valarray的前身设计出来是为了让面向科学计算市场的编译器能够识别数组类型,并使用高度优化的内部代码进行操作,编译器在某种意义上已经“理解”了这些类型。然而,这种情况从未发生(部分原因是所涉及的市场相对较小,部分原因是随着valarray成为模板,问题变得越来越复杂)。在表达式模板技术发现后的一段时间,我们中的一个(Vandevoorde)向C++委员会提交了一份提案,将valarray转变为开发的Array模板(受现有valarray功能的启发,有很多花哨的功能),该提议首次记录了Rep参数的概念。在此之前,实际存储的数组和表达式模板伪数组是不同的模板。当外部代码引入接受数组的函数foo()时——例如, 46 | 47 | \begin{lstlisting}[style=styleCXX] 48 | double foo(Array const&); 49 | \end{lstlisting} 50 | 51 | 调用foo(1.2*x)强制将表达式模板转换为具有实际存储空间的数组,即使应用于该参数的操作不需要临时变量。若表达式模板内嵌在Rep参数中,则可以声明 52 | 53 | \begin{lstlisting}[style=styleCXX] 54 | template 55 | double foo(Array const&); 56 | \end{lstlisting} 57 | 58 | 除非确实需要,否则不会发生转换。 59 | 60 | valarray提案出现在C++标准化过程的后期,实际上重写了标准中所有关于valarray的内容。结果被拒绝了,取而代之的是对现有内容进行了一些调整,允许基于表达式模板的实现,但使用这种方式仍然比这里讨论的要麻烦得多。撰写本文时,还不知道存在这样的实现,标准valarray在执行其设计的操作时效率非常低。 61 | 62 | 最后,本章介绍的许多开创性技术,以及后来称为STL的技术, 63 | 64 | \begin{tcolorbox}[colback=webgreen!5!white,colframe=webgreen!75!black] 65 | \hspace*{0.75cm}标准模板库(STL)彻底改变了C++的世界,后来成为C++标准库的一部分(参见[JosuttisStdLib])。 66 | \end{tcolorbox} 67 | 68 | 最初都是在同一个编译器上实现(Borland的C++编译器) 69 | 70 | \begin{tcolorbox}[colback=webgreen!5!white,colframe=webgreen!75!black] 71 | \hspace*{0.75cm}Jaakko在开发核心语言特性方面发挥了重要作用。 72 | \end{tcolorbox} 73 | 74 | 这可能是第一个使模板编程在C++编程社区中广泛使用的编译器。 75 | 76 | 表达式模板最初主要应用于对类数组类型的操作。几年后,发现了新的应用场景。其中最具开创性的是Jaakko J{\"a}rvi和Gary Powell的Boost.Lambda库(参见[LambdaLib]),在Lambda表达式成为核心语言特性之前提供了一个可用的Lambda表达式工具,以及Eric Niebler的Boost.Proto库,是一个元程序表达式模板库,目标是在C++中创建嵌入式领域特定语言。其他Boost库,比如Boost.Fusion和Boost.Hana中也使用了高级的表达式模板。 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 | -------------------------------------------------------------------------------- /content/3/chapter27/images/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Cpp-Templates-2nd/579a1728a33ba4e1f410354a4a427e1668f37190/content/3/chapter27/images/1.png -------------------------------------------------------------------------------- /content/3/chapter28/0.tex: -------------------------------------------------------------------------------- 1 | 调试模板时,会遇到两类挑战。对于模板编写者来说,有一类挑战无疑是一个问题:如何确保所编写的模板对满足条件的模板参数都能起作用?另一类问题几乎完全相反:当模板的行为与文档中描述的不一致时,模板的用户如何找出不满足哪些模板参数要求? 2 | 3 | 深入讨论这些问题之前,考虑一下可能施加在模板参数上的各种约束。本章中,我们主要处理导致编译错误的约束,称其为语法约束。语法约束包括特定类型构造函数的存在,特定函数调用的无歧义性等。另一种约束称之为语义约束,要验证这些约束条件非常困难。通常,这样做甚至可能不实际,可能要求在模板类型参数上定义一个小于操作符(这是一种语法约束),但通常也会要求操作符实际上在其域上定义某种排序(这是一种语义约束)。 4 | 5 | 术语概念通常用来表示模板库中需要的一组约束,C++标准库依赖于随机访问迭代器和默认可构造函数等概念。有了这个术语,调试模板代码包含了大量确定在模板实现,及其使用中如何违反概念的工作。本章深入探讨了设计和调试技术,这些技术可以让模板的作者和用户更容易地使用模板。 -------------------------------------------------------------------------------- /content/3/chapter28/1.tex: -------------------------------------------------------------------------------- 1 | 当模板错误发生时,问题通常在实例化后发现,从而会有冗长的错误消息,就像在第9.4节中那样。 2 | 3 | \begin{tcolorbox}[colback=webgreen!5!white,colframe=webgreen!75!black] 4 | \hspace*{0.75cm}无疑在写代码时会遇到一些错误消息,会使最初的示例看起来很乏味! 5 | \end{tcolorbox} 6 | 7 | 为了说明这一点,请考虑以下的手写代码: 8 | 9 | \begin{lstlisting}[style=styleCXX] 10 | template 11 | void clear (T& p) 12 | { 13 | *p = 0; // assumes T is a pointer-like type 14 | } 15 | 16 | template 17 | void core (T& p) 18 | { 19 | clear(p); 20 | } 21 | 22 | template 23 | void middle (typename T::Index p) 24 | { 25 | core(p); 26 | } 27 | 28 | template 29 | void shell (T const& env) 30 | { 31 | typename T::Index i; 32 | middle(i); 33 | } 34 | \end{lstlisting} 35 | 36 | 这个例子阐明了软件开发的分层:像shell()这样的高级函数模板依赖于像middle()这样的组件,而这些组件本身会使用像core()这样的功能。当实例化shell()时,下面的层也需要实例化。这个例子中,有一个问题:core()实例化为int类型(在middle()中使用Client::Index),并试图错误的解引用该类型。 37 | 38 | 该错误仅在实例化时可检测到。例如: 39 | 40 | \begin{lstlisting}[style=styleCXX] 41 | class Client 42 | { 43 | public: 44 | using Index = int; 45 | }; 46 | 47 | int main() 48 | { 49 | Client mainClient; 50 | shell(mainClient); 51 | } 52 | \end{lstlisting} 53 | 54 | 好的通用诊断包括导致问题的所有级别的跟踪,但是获得这么多信息,也会让我们感觉手足无措。 55 | 56 | 在[StroustrupDnE]中可以找到围绕这个问题核心思想的讨论,Bjarne Stroustrup确定了两类方法来更早地确定模板参数是否满足一组约束:通过语言扩展或更早的参数使用。在第17.8节和附录E中介绍了前一种选择,后一种选择包括在浅层实例化中强制错误。这通过插入未使用的代码来实现,若代码使用的模板参数不满足更深层模板的要求,就会触发错误。 57 | 58 | 前面的例子中,可以在shell()中添加代码,尝试对T::Index类型的值解引用。例如: 59 | 60 | \begin{lstlisting}[style=styleCXX] 61 | template 62 | void ignore(T const&) 63 | { } 64 | 65 | template 66 | void shell (T const& env) 67 | { 68 | class ShallowChecks 69 | { 70 | void deref(typename T::Index ptr) { 71 | ignore(*ptr); 72 | } 73 | }; 74 | typename T::Index i; 75 | middle(i); 76 | } 77 | \end{lstlisting} 78 | 79 | 若T是不能解引用T::Index的类型,则会在局部类ShallowChecks上出现编译错误。因为没有使用局部类,所以添加的代码不会影响shell()函数的运行时间,但许多编译器会警告说没有使用ShallowChecks(成员也是如此)。可以使用ignore()模板等技巧来抑制此类警告,但也会增加代码的复杂性。 80 | 81 | \hspace*{\fill} \\ %插入空行 82 | \noindent 83 | \textbf{概念检查} 84 | 85 | 显然,示例中的代码开发可能会变得与实现模板的实际功能代码一样复杂。为了控制这种复杂性,可以尝试在某种类型的库中收集各种代码片段。这样的库可以包含宏,当模板参数替换违反该特定参数的概念时,这些宏可以扩展为触发适当错误的代码。这类库中最流行的是Concept Check库,是Boost发行版的一部分(参见[BCCL])。但这种技术的可移植性不是特别好(不同编译器诊断错误的方式不同),有时还会掩盖在更高级别上无法捕获错误的问题。 86 | 87 | 当C++中有了概念(参见附录E),就有了其他的方法来支持需求和预期行为的定义。 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 | -------------------------------------------------------------------------------- /content/3/chapter28/2.tex: -------------------------------------------------------------------------------- 1 | assert()宏通常在C++代码中用于检查程序执行过程中,是否存在某些特定条件。若断言失败,程序将停止运行,以便开发者修复问题。 2 | 3 | C++11中引入的static\_assert关键字具有相同的目的,但会在编译时进行求值:若条件(必须是常量表达式)求值为false,编译器将发出错误消息。错误消息将包括一个字符串(它是static\_assert本身的一部分),告诉开发者哪里出错了。下面的静态断言确保我们在一个带有64位指针的平台上编译: 4 | 5 | \begin{lstlisting}[style=styleCXX] 6 | static_assert(sizeof(void*) * CHAR_BIT == 64, "Not a 64-bit platform"); 7 | \end{lstlisting} 8 | 9 | 当模板参数不满足模板的约束时,静态断言可用于提供有用的错误消息。使用第19.4节描述的技术,可以创建一个类型特征来确定类型是否可解引用: 10 | 11 | \hspace*{\fill} \\ %插入空行 12 | \noindent 13 | \textit{debugging/hasderef.hpp} 14 | \begin{lstlisting}[style=styleCXX] 15 | #include // for declval() 16 | #include // for true_type and false_type 17 | 18 | template 19 | class HasDereference { 20 | private: 21 | template struct Identity; 22 | template static std::true_type 23 | test(Identity())>*); 24 | template static std::false_type 25 | test(...); 26 | public: 27 | static constexpr bool value = decltype(test(nullptr))::value; 28 | }; 29 | \end{lstlisting} 30 | 31 | 可以在shell()中引入一个静态断言,若上一节中的shell()模板实例化时使用了不可解引用的类型,该断言会提供更好的诊断信息: 32 | 33 | \begin{lstlisting}[style=styleCXX] 34 | template 35 | void shell (T const& env) 36 | { 37 | static_assert(HasDereference::value, "T is not dereferenceable"); 38 | typename T::Index i; 39 | middle(i); 40 | } 41 | \end{lstlisting} 42 | 43 | 通过这些更改,编译器会产生简单扼要的诊断信息,从而表明类型T不可解引用。 44 | 45 | 静态断言可以使错误消息更短、更简单,从而极大地改善使用模板库时的用户体验。 46 | 47 | 也可以将其应用于类模板,并使用附录D中的类型特征: 48 | 49 | \begin{lstlisting}[style=styleCXX] 50 | template 51 | class C { 52 | static_assert(HasDereference::value, "T is not dereferenceable"); 53 | static_assert(std::is_default_constructible::value, 54 | "T is not default constructible"); 55 | ... 56 | }; 57 | \end{lstlisting} 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 | -------------------------------------------------------------------------------- /content/3/chapter28/5.tex: -------------------------------------------------------------------------------- 1 | 跟踪器相对简单和有效,我们仅针对特定的输入数据和相关功能的特定行为跟踪模板的执行。比较操作符必须满足哪些条件才能使排序算法有意义(或正确),但在示例中只测试了一个与小于操作符(整数)行为完全相同的比较操作符。 2 | 3 | 跟踪程序的扩展在一些圈子中称为oracle(或运行时分析oracle),是连接到推理引擎的跟踪器——一个程序,可以记录关于断言和推断某些结论的原因。 4 | 5 | 某些情况下,Oracle允许动态地验证模板算法,而无需完全指定替代模板参数(oracle就是参数)或输入数据(当推理引擎遇到问题时,可能会请求某种输入假设)。然而,可以用这种方式分析算法的复杂性还好(因为推理引擎的限制),但工作量相当大。出于这些原因,我们不深入研究oracle的发展史,有兴趣的读者可以查看后记中提到的出版物(和其中的参考资料)。 -------------------------------------------------------------------------------- /content/3/chapter28/6.tex: -------------------------------------------------------------------------------- 1 | 在Jeremy Siek的概念检查库(参见[BCCL])中可以找到通过在高级模板中添加虚设代码来改进C++编译器诊断的相当系统的尝试,是Boost库的一部分(参见[Boost])。 2 | 3 | Robert Klarer和John Maddock提出了static\_assert特性来帮助开发者在编译时检查条件,后来成为C++11的特性之一。在此之前,通常表示为一个库或宏,使用的技术类似于第28.1节中描述的那些技术。Boost.StaticAssert库就是这样一种实现。 4 | 5 | MELAS系统为C++标准库的某些部分提供了oracle,允许对其一些算法进行验证。这个系统在[MusserWangDynaVeri]中进行了讨论。 6 | 7 | \begin{tcolorbox}[colback=webgreen!5!white,colframe=webgreen!75!black] 8 | \hspace*{0.75cm}其中一位作者David Musser也是C++标准库开发的关键人物。除此之外,他还设计并实现了第一个关联容器。 9 | \end{tcolorbox} 10 | -------------------------------------------------------------------------------- /content/About-This-Book.tex: -------------------------------------------------------------------------------- 1 | \begin{flushright} 2 | \zihao{1} 关于本书 3 | \end{flushright} 4 | 5 | 本书第一版出版于15年前,目标是一本C++模板权威指南。本书得到了不少读者的认可,也多次推荐为参考书目,屡获好评。 6 | 7 | 第一版中有不少现代C++的内容,但C++也在发展,从C++11到C++14,再到C++17,对第一版内容的修订势在必行。 8 | 9 | 第二版,初心不改:既是一本内容全面的参考书,也是一本简易的教程。因为C++已经不是原来的C++了,这次只针对现代C++。 10 | 11 | 目前的C++编程环境要好于本书第一版发布时,这期间有一些深入探讨模板应用的书籍。可以从互联网上获取更多的C++模板知识,以及基于模板的编程技术和应用实例。第二版中,将重点关注那些可以泛化的技术。 12 | 13 | 现代C++为相同功能提供了更简单的方法,所以第一版中的部分内容已经过时。因此有一部分在第二版中删除,而现代C++中的更新会进行填充。 14 | 15 | 尽管C++模板已经出现了20多年了,但目前C++开发者社区中仍然会出现其在软件开发中的新理解。本书的目标是和读者分享这些内容,当然也希望能够启发读者产生新的理解。 -------------------------------------------------------------------------------- /content/Acknowledgments-for-the-First.tex: -------------------------------------------------------------------------------- 1 | \begin{flushright} 2 | \zihao{1} 第一版的致谢 3 | \end{flushright} 4 | 5 | 这本书提供了一些想法、概念和解决方案,还有许多例子。感谢所有在过去几年中帮助和支持我们的人和企业。 6 | 7 | 首先,要感谢所有的审稿人和所有给早期手稿意见的人员,他们保证了这本书的品质:Kyle Blaney,Thomas Gschwind,Dennis Mancl,Patrick Mc Killen和Jan Christiaan van Winkel。特别感谢Dietmar K{\"u}hl一丝不苟地审阅和编辑了本书,他的反馈对这本书的质量贡献巨大。 8 | 9 | 还要感谢给我们机会在不同平台上用不同编译器测试示例的朋友和企业。非常感谢爱迪生设计集团,感谢他们的编译器和支持。还要感谢GNU和egcs编译器的开发者(Jason Merrill),以及微软为Visual C++提供的评估版本(Jonathan Caves,Herb Sutter和Jason Shirk是我们的联系人)。 10 | 11 | 许多现有的“C++智慧”是由线上C++社区创造的,大部分都来自Usenet组的comp.lang.c++和comp.std.c++。因此,特别感谢这些小组的积极主持,使讨论保持有益和建设性。我们也非常感谢所有花时间来描述和解释想法的人们,这让我们受益匪浅。 12 | 13 | Addison-Wesley团队做了一件伟大的工作。非常感谢Addison-Wesley(我们的编辑)对这本书的温柔鼓励、良好建议和不懈支持。感谢Tyrrell Albaugh,Bunny Ames,Melanie Buck, Jacquelyn Doucette,Chanda Leary-Coutu,Catherine Ohala和Marty Rabinowitz。很感谢Marina Lang,她是第一个赞助这本书的Addison-Wesley成员。Susan Winer参与了早期的编辑工作,帮助我们塑造了后来的成品。 14 | 15 | \hspace*{\fill} \\ %插入空行 16 | \noindent\textbf{Nico的致谢} 17 | 18 | 首先感谢我的家人:Ulli,Lucas,Anica和Frederic。感谢对我的耐心,并体贴和鼓励支持这本书的写作。 19 | 20 | 另外,感谢David。他非常非常非常的专业,而且非常有耐心(有时我会问一些傻乎乎的问题),和他一起工作感觉棒极了。 21 | 22 | \hspace*{\fill} \\ %插入空行 23 | \noindent\textbf{David的致谢} 24 | 25 | 我的妻子Karina在本书的写作中发挥了重要作用,感激她在我生命中扮演的角色。当“业余时间”变得不稳定时,Karina会帮助我管理时间表,教会我说“不”,以便腾出时间用于写作。最重要的是,她对这个项目非常支持,我每天都在享受她赐予我的友谊和爱意。 26 | 27 | 感谢能够与Nico合作。他对文书的贡献巨大,其经验和纪律性将我们凌乱的涂鸦,变成了组织良好的作品。 28 | 29 | John“Mr. Template”Spicer和Steve“Mr. Overload”Adamczyk是好朋友和好同事,他们(在一起)就是C++语言的权威,他们指出了本书描述的许多问题。若本书在C++语言描述中出现了错误,肯定是我们忘了向他们咨询。 30 | 31 | 最后,我想对那些支持这个项目的人表示感谢(啦啦队的力量也不可低估)。首先是我的父母:他们对我的爱和鼓励让一切变得不同。还有无数的朋友问:“书写得怎么样了?”他们也是完成本书的力量源泉:Michael Beckmann,Brett和Julie Beene,Jarran Carr,Simon Chang,Ho和Sarah Cho、Christophe De Dinechin、Ewa Deelman、Neil Eberle、Sassan Hazeghi、Vikram Kumar、Jim和Lindsay Long、R.J. Morgan、Mike Puritano、Ragu Raghavendra、Jim和Phuong Sharp、Gregg Vaughn和John Wiegley。 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /content/Acknowledgments-for-the-Second.tex: -------------------------------------------------------------------------------- 1 | \begin{flushright} 2 | \zihao{1} 第二版的致谢 3 | \end{flushright} 4 | 5 | 写书很难,维护更难。过去的十年里,我们花了五年多的时间完成了第二版,如果没有其他人的支持和耐心,这将是不可能完成的任务。 6 | 7 | 首先,感谢C++社区和C++标准化委员会。除了添加新的语言标准和库特性之外,还耐心和热情地向我们解释和讨论他们的工作。 8 | 9 | 过去15年里,社区成员们为第一版的错误和可能的改进提供反馈。人数实在太多,无法在这里逐个感谢,真的很感谢你们花时间写下自己的想法和观察。有时的回复不够及时,还望能谅解。 10 | 11 | 感谢本书的审阅者们,为本书提供了宝贵的反馈和说明。这些评论对于本书的质量至关重要,这也再次证明了好东西需要许多“聪明人”。在此,非常感谢Steve Dewhurst,Howard Hinnant,Mikael Kilpel{\"a}inen,Dietmar K{\"u}hl,Daniel Kr{\"u}bler,Nevin Liber,Andreas Neiser,Eric Niebler,Richard Smith,Andrew Sutton,Hubert Tong和Ville Voutilainen。 12 | 13 | 当然,还要感谢Addison-Wesley/Pearson出版社的所有人,不能再把专业人士对书籍作者的支持视为理所当然。他们很有耐心,在适当的时候还会给出建议,在需要专业知识的时候也会提供帮助。在此,非常感谢Peter Gordon,Kim Boedigheimer,Greg Doench,Julie Nahil,Dana 14 | Wilson和Carol Lallier。 15 | 16 | 特别感谢LaTeX社区提供的文本系统,并感谢Frank Mittelbach解决了关于 \LaTeX 的问题(我们几个这方面真的太菜了)。 17 | 18 | \hspace*{\fill} \\ %插入空行 19 | \noindent\textbf{David的致谢} 20 | 21 | 第二版让大家久等了,完成最后的润色时,我由衷的感激那些让这本书出现的人。首先是我的妻子(Karina)和女儿们(Alessandra和Cassandra),允许我从“家庭日程”中抽出大量时间完成这本书,尤其是在工作的最后一年。我的父母也对这本书很感兴趣,每当去看望他们时,都要了解这个写作项目的情况。 22 | 23 | 显然,这是一本技术书籍,内容是关于编程的知识和经验。然而,仅有“经验”还是远远不够的。这里,非常感谢Nico承担了“管理”和“生产”工作(以及他所有的贡献)。如果这本书对你有用,并且有一天遇到了Nico,一定要感谢他的付出。还要感谢Doug的加入,并在日程安排艰难的时刻坚持本书的编纂。 24 | 25 | 多年来,C++社区的许多开发者也分享了宝贵的见解,感谢! 这里,要特别感谢Richard Smith,多年来一直使用“神秘的”技巧高效地回复我的邮件。同样,感谢我的同事John Spicer,Mike Miller和Mike Herrick,他们也分享了他们的知识和经验,让我们学到了更多。 26 | 27 | \hspace*{\fill} \\ %插入空行 28 | \noindent\textbf{Nico的致谢} 29 | 30 | 首先,感谢两位专家,David和Doug,从他们那里学到了很多。我作为一名应用程序程序员和库使用者,经常会问出一些傻乎乎的问题。现在,我想成为一个真正的专家(当然,直到下一个问题的出现),想想就有些小激动呢! 31 | 32 | 还要感谢Jutta Eckstein。Jutta能够推动和支持人们实现理想、想法和目标。大多数人在IT行业遇到她或者和她一起工作的时候才会有这样的体验,而我却有幸能在日常生活中得到她的帮助。想想已经好多年了,希望今后也能一直这样。 33 | 34 | \hspace*{\fill} \\ %插入空行 35 | \noindent\textbf{Doug的致谢} 36 | 37 | 衷心感谢我的妻子Amy,以及我们的两个女儿Molly和Tessa。他们的爱和陪伴给我带来了日常的快乐和应对生活和工作中最大挑战的信心。还要感谢我的父母,感谢他们教会了我热爱学习,以及对我的鼓励。 38 | 39 | 和David和Nico工作是一件很愉快的事,他们性格迥异,却又能很好地互补。David具有非常清晰的技术写作能力,他的描述即精确又带有启发性。Nico,除了出色的组织能力(使其他两位合著者不至于陷入混乱)外,还有一种独特的能力,可以分解复杂的技术讨论,使其变得简单、容易理解,以及清晰。 40 | 41 | 42 | -------------------------------------------------------------------------------- /content/Appendix/A/0.tex: -------------------------------------------------------------------------------- 1 | 定义规则称为ODR,是C++程序结构的基石。ODR最常见的(很容易记住)应用:所有文件中只定义一次非内联函数或对象,每个翻译单元中最多定义一次类、内联函数和内联变量,确保对同一实体的所有定义相同。 2 | 3 | 问题在于细节,当与模板实例化结合使用时,这些细节可能会令人生畏。本附录旨在为感兴趣的读者提供ODR的全面概述。在正文中还指出了相关的问题。 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 | -------------------------------------------------------------------------------- /content/Appendix/A/1.tex: -------------------------------------------------------------------------------- 1 | 实践中,通过用“代码”填充文件来编写C++程序,文件在ODR上下文中并不是特别重要,重要的是翻译单位。本质上,翻译单元是将预处理器应用于提供给编译器的文件的结果,预处理器删除没有通过条件编译指令(\#if,\#ifdef和友元)选择的代码段,删除注释,插入\#include的文件(递归),并展开宏。 2 | 3 | 就ODR而言,以下两个文件 4 | 5 | \begin{lstlisting}[style=styleCXX] 6 | // header.hpp: 7 | #ifdef DO_DEBUG 8 | #define debug(x) std::cout << x << ’\n’ 9 | #else 10 | #define debug(x) 11 | #endif 12 | 13 | void debugInit(); 14 | 15 | // myprog.cpp: 16 | #include "header.hpp" 17 | int main() 18 | { 19 | debugInit(); 20 | debug("main()"); 21 | } 22 | \end{lstlisting} 23 | 24 | 等价于以下单个文件: 25 | 26 | \begin{lstlisting}[style=styleCXX] 27 | // myprog.cpp: 28 | void debugInit(); 29 | int main() 30 | { 31 | debugInit(); 32 | } 33 | \end{lstlisting} 34 | 35 | 跨翻译单元的连接,是通过在两个翻译单元中具有相应的外部链接声明(例如,两个全局函数debugInit()的声明)来建立的。 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 | -------------------------------------------------------------------------------- /content/Appendix/A/2.tex: -------------------------------------------------------------------------------- 1 | 术语声明和定义在常见的“开发者对话”中经常交替使用。在ODR的范围内,这些词的含义很重要。 2 | 3 | \begin{tcolorbox}[colback=webgreen!5!white,colframe=webgreen!75!black] 4 | \hspace*{0.75cm}交换关于C和C++的观点时,仔细处理术语是一个好习惯。我们在整本书中都是这样做的。 5 | \end{tcolorbox} 6 | 7 | 声明是一种C++构造,(通常) 8 | 9 | \begin{tcolorbox}[colback=webgreen!5!white,colframe=webgreen!75!black] 10 | \hspace*{0.75cm}有些结构(如static\_assert)不引入名称,但在语法上视为声明。 11 | \end{tcolorbox} 12 | 13 | 程序中引入或重新引入一个名称。声明也可以是定义,这取决于它引入了哪个,以及如何引入: 14 | 15 | \begin{itemize} 16 | \item 17 | \textbf{命名空间和命名空间别名:} 18 | 命名空间及其别名的声明也需要定义,尽管术语定义在此上下文中并不常见,因为命名空间的成员列表可以在以后进行“扩展”(例如,与类和枚举类型不同)。 19 | 20 | \item 21 | \textbf{类、类模板、函数、函数模板、成员函数和成员函数模板: } 22 | 当声明包含与名称相关联的大括号正文时,声明就是定义。该规则包括联合、操作符、成员操作符、静态成员函数、构造函数和析构函数,以及这些东西的模板版本的显式特化(即任何类实体和函数实体)。 23 | 24 | \item 25 | \textbf{枚举:} 26 | 当声明包含用大括号括起来的枚举数列表时,该声明是定义。 27 | 28 | \item 29 | \textbf{局部变量和非静态数据成员:} 30 | 这些可以视为定义(尽管区别很少),函数定义中的函数参数声明本身就是一个定义,因为它表示一个局部变量,但函数声明中的函数参数不是定义。 31 | 32 | \item 33 | \textbf{全局变量:} 34 | 若声明之前没有使用关键字extern,或者有初始化式,那么全局变量的声明也是该变量的定义。否则,就不是定义。 35 | 36 | \item 37 | \textbf{静态数据成员: } 38 | 当声明出现在成员的类或类模板外部,或者在类或类模板中内联或constexpr声明时,声明就是定义。 39 | 40 | \item 41 | \textbf{显式和偏特化:} 42 | 若template<>或template<…>本身是一个定义,只是静态数据成员或静态数据成员模板的显式特化,只有在包含初始化式时才是定义。 43 | 44 | \end{itemize} 45 | 46 | 其他声明不是定义。这包括类型别名(带有typedef或using)、using声明、using指令、模板参数声明、显式实例化指令、static\_assert声明等。 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /content/Appendix/B/0.tex: -------------------------------------------------------------------------------- 1 | 表达式是C++语言的基石,提供了表达计算的主要机制。每个表达式都有一个类型,该类型描述其计算产生的值的静态类型。表达式7是int类型,表达式5 + 2也是。若x是int类型的变量,则表达式x是int类型。每个表达式也有一个值类别,描述值如何形成,以及如何影响表达式的行为。 -------------------------------------------------------------------------------- /content/Appendix/B/1.tex: -------------------------------------------------------------------------------- 1 | 2 | 历史上,只有两种值类别:左值和右值。左值是指存储在内存或机器寄存器中实际值的表达式,例如表达式x,其中x是变量名。这些表达式可以修改,从而允许更新存储的值。若x是一个int类型的变量,下面的赋值会将x的值替换为7: 3 | 4 | \begin{lstlisting}[style=styleCXX] 5 | x = 7; 6 | \end{lstlisting} 7 | 8 | 术语左值来自于这些表达式在赋值中可能扮演的角色:字母“l”代表“左手边”,因为(在C中)只有左值可能出现在赋值的左手边。相反,右值(“r”代表“右手边”)只能出现在赋值表达式的右手边。 9 | 10 | 1989年C语言标准化后,情况发生了变化:当int const仍然是存储在内存中的值时,不能出现在赋值操作的左侧: 11 | 12 | \begin{lstlisting}[style=styleCXX] 13 | int const x; // x is a nonmodifiable lvalue 14 | x = 7; // ERROR: modifiable lvalue required on the left 15 | \end{lstlisting} 16 | 17 | C++进一步改变了这一点:类的右值可以出现在赋值的左侧。这样的赋值实际上是对类的适当赋值操作符的函数调用,而不是对标量类型的“简单”赋值,因此遵循(单独的)成员函数调用规则。 18 | 19 | 由于所有这些变化,术语左值现在有时称为本地化值。引用变量的表达式并不是唯一一种左值表达式。另一类左值表达式包括指针解引用操作(例如,*p)和类对象成员的表达式(例如,p\texttt{->}data)。解引用操作指向存储在指针引用地址的值。即使调用返回用\&声明的“传统”左值引用类型的函数也是左值。例如(详见第B.4节): 20 | 21 | \begin{lstlisting}[style=styleCXX] 22 | std::vector v; 23 | v.front() // yields an lvalue because the return type is an lvalue reference 24 | \end{lstlisting} 25 | 26 | 字符串字面值也是(不可修改的)左值。 27 | 28 | 右值是纯粹的数学值(如7或字符'a'),不一定有相关的存储;它们的存在就为了计算,但当使用它们时就不能再引用。除了字符串字面量(例如,7,'a', true, nullptr)之外的字面值都是右值,就像许多内置算术计算(例如,x + 5对于整数类型的x)和调用按值返回结果的函数一样,所有的临时变量都是右值(不过,这不适用于引用它们的命名引用)。 29 | 30 | \subsubsubsection{B.1.1\hspace{0.2cm}左值到右值的转换} 31 | 32 | 由于右值的短暂性,只能出现在赋值语句(“简单”)的右侧:赋值语句7 = 8是没有意义的,因为不允许重新定义数学上的7。另一方面,左值似乎没有同样的限制:当x和y是兼容类型的变量时,可以计算赋值x = y,即使表达式x和y都是左值。 33 | 34 | 赋值x = y可行,因为右边的表达式y进行了隐式转换,称为左值到右值转换。顾名思义,左值可以转换为右值,并通过从与左值相关联的存储或寄存器中读取该左值,来生成相同类型的右值。因此,这种转换完成了两件事:第一,确保左值可以在任何需要右值的地方使用(作为赋值的右手边或在数学表达式中,如x + y)。第二,确定了在程序中(优化之前)编译器发出“load”指令从内存中读取值的位置。 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /content/Appendix/B/3.tex: -------------------------------------------------------------------------------- 1 | 使用关键字decltype(在C++11中引入),可以检查C++表达式的值类别。对于任意表达式x, decltype((x))(注意双括号)会产生: 2 | 3 | \begin{itemize} 4 | \item 5 | x是类型,则为prvalue 6 | 7 | \item 8 | x是左值引用,则为lvalue 9 | 10 | \item 11 | x是右值引用,则为xvalue 12 | \end{itemize} 13 | 14 | decltype((x))中需要双括号,以避免在表达式x确实命名了实体的情况下,产生命名实体的声明类型(其他情况下,括号不起作用)。若表达式x只是将一个变量命名为v,那么不带圆括号的构造就变成了decltype(v),其生成变量v的类型,而不是反映引用该变量的表达式x的值类别的类型。 15 | 16 | 因此,对任意表达式e使用类型特征,可以使用如下方法检查其值的类别: 17 | 18 | \begin{lstlisting}[style=styleCXX] 19 | if constexpr (std::is_lvalue_reference::value) { 20 | std::cout << "expression is lvalue\n"; 21 | } 22 | else if constexpr (std::is_rvalue_reference::value) { 23 | std::cout << "expression is xvalue\n"; 24 | } 25 | else { 26 | std::cout << "expression is prvalue\n"; 27 | } 28 | \end{lstlisting} 29 | 30 | 详见第15.10.2节。 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /content/Appendix/B/4.tex: -------------------------------------------------------------------------------- 1 | C++中的引用类型(如int\&)以两种重要的方式与值类别交互。第一个是引用可能会限制,可以绑定到的表达式的值类别。int\&类型的非const左值引用,只能用int类型的左值表达式初始化。类似地,int类型的右值引用,只能用int类型的右值表达式初始化。 2 | 3 | 值类别与引用交互的第二种方式是与函数的返回类型交互,其中使用引用类型作为返回类型会影响对该函数调用的值类别。特别是: 4 | 5 | \begin{itemize} 6 | \item 7 | 对返回类型为左值引用的函数,将生成左值。 8 | 9 | \item 10 | 若函数的返回类型,是对象类型的右值引用,调用该函数会产生xvalue(对函数类型的右值引用会产生左值)。 11 | 12 | \item 13 | 调用返回非引用类型的函数会产生prvalue。 14 | \end{itemize} 15 | 16 | 下面的示例中,将演示引用类型和值类别之间的交互。 17 | 18 | \begin{lstlisting}[style=styleCXX] 19 | int& lvalue(); 20 | int&& xvalue(); 21 | int prvalue(); 22 | \end{lstlisting} 23 | 24 | 给定表达式的值类别和类型可以通过decltype来确定。如第15.10.2节所述,使用引用类型来描述表达式何时为左值或xvalue: 25 | 26 | \begin{lstlisting}[style=styleCXX] 27 | std::is_same_v // yields true because result is lvalue 28 | std::is_same_v // yields true because result is xvalue 29 | std::is_same_v // yields true because result is prvalue 30 | \end{lstlisting} 31 | 32 | 因此,可以进行以下的调用: 33 | 34 | \begin{lstlisting}[style=styleCXX] 35 | int& lref1 = lvalue(); // OK: lvalue reference can bind to an lvalue 36 | int& lref3 = prvalue(); // ERROR: lvalue reference cannot bind to a prvalue 37 | int& lref2 = xvalue(); // ERROR: lvalue reference cannot bind to an xvalue 38 | 39 | int&& rref1 = lvalue(); // ERROR: rvalue reference cannot bind to an lvalue 40 | int&& rref2 = prvalue(); // OK: rvalue reference can bind to a prvalue 41 | int&& rref3 = xvalue(); // OK: rvalue reference can bind to an xrvalue 42 | \end{lstlisting} 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /content/Appendix/B/images/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Cpp-Templates-2nd/579a1728a33ba4e1f410354a4a427e1668f37190/content/Appendix/B/images/1.png -------------------------------------------------------------------------------- /content/Appendix/C/0.tex: -------------------------------------------------------------------------------- 1 | 重载解析是为给定调用表达式选择要调用函数的过程。看看下面这个简单的例子: 2 | 3 | \begin{lstlisting}[style=styleCXX] 4 | void display_num(int); // #1 5 | void display_num(double); // #2 6 | 7 | int main() 8 | { 9 | display_num(399); // #1 matches better than #2 10 | display_num(3.99); // #2 matches better than #1 11 | } 12 | \end{lstlisting} 13 | 14 | 例子中,函数名display\_num()重载,当在调用中使用这个名称时,C++编译器必须使用更多信息来区分不同的候选名称;大多数情况下,这些信息是调用参数的类型。我们的例子中,当函数调用一个整型参数时调用整型版本,当函数提供一个浮点参数时调用双精度浮点版本就很简单。尝试对这种简单选择建模的流程就是重载解析。 15 | 16 | 指导重载解析规则背后的思想很简单,但在C++标准化过程中,细节变得相当复杂。这种复杂性主要是由支持各种实际示例愿望所驱使,这些示例似乎具有“明显的最佳匹配”,但试图将这种直觉形式化时,各种微妙的问题就出现了。 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 | -------------------------------------------------------------------------------- /content/Appendix/C/1.tex: -------------------------------------------------------------------------------- 1 | 重载解析只是函数调用完整处理的一部分,并不是每个函数调用的一部分。首先,通过函数指针和通过指向成员函数的指针进行的调用不受重载解析的影响,因为要调用的函数完全(在运行时)由指针决定。其次,类函数宏不能重载,因此不需要重载解析。 2 | 3 | 在高层次上,对命名函数的调用可以按以下方式处理: 4 | 5 | \begin{itemize} 6 | \item 7 | 查找名称以形成初始重载集。 8 | 9 | \item 10 | 若有必要,这个集合会以各种方式进行调整(发生模板参数推导和替换,这会导致丢弃一些函数模板候选)。 11 | 12 | \item 13 | 完全不匹配的候选(考虑隐式转换和默认参数之后)将从重载集中删除。这就产生了一组匹配的候选函数。 14 | 15 | \item 16 | 执行重载解析以找到最佳候选。如果有,则选中;否则,调用具有歧义。 17 | 18 | \item 19 | 选中的候选需要检查,若是一个已删除函数(即用=delete定义的函数)或一个不可访问的私有成员函数,则编译器会发出错误信息。 20 | \end{itemize} 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 | -------------------------------------------------------------------------------- /content/Appendix/D/0.tex: -------------------------------------------------------------------------------- 1 | C++标准库主要由模板组成,其中许多模板依赖于本书中介绍和讨论的技术。因为标准库定义了几个模板来用通用代码实现库,所以一些技术“标准化”了。这些类型实用工具(类型特征和其他帮助工具)在本章中列出并解释。 2 | 3 | 一些类型特征需要编译器的支持,而另一些类型特征可以使用现有语言特性进行实现(在第19章讨论了其中一些)。 -------------------------------------------------------------------------------- /content/Appendix/D/7.tex: -------------------------------------------------------------------------------- 1 | 2 | 3 | C++标准库提供了一些其他实用工具,这些工具对于编写可移植的泛型代码非常有用。 4 | 5 | \begin{table}[H] 6 | \begin{center} 7 | \begin{tabular}{l|l} 8 | \hline 9 | \textbf{特性} & \textbf{作用} \\ \hline 10 | declval\textless{}T \textgreater{}() & 生成一个不构造类型的“对象”(右值引用) \\ \hline 11 | addressof(r) & 生成对象或函数的地址 \\ \hline 12 | \end{tabular} 13 | \end{center} 14 | \end{table} 15 | 16 | \begin{center} 17 | 表D.9. 用于元编程的其他工具 18 | \end{center} 19 | 20 | \textbf{std::declval ()} 21 | 22 | \begin{itemize} 23 | \item 24 | 头文件中定义。 25 | 26 | \item 27 | 生成类型的“对象”或函数,而不使用构造函数或初始化。 28 | 29 | \item 30 | 若T为void,则返回类型为void。 31 | 32 | 可以用于处理未求值表达式中的对象或函数类型。 33 | 34 | \item 35 | 其简单定义如下: 36 | \begin{lstlisting}[style=styleCXX] 37 | template 38 | add_rvalue_reference_t declval() noexcept; 39 | \end{lstlisting} 40 | 41 | 因此: 42 | 43 | \begin{itemize} 44 | \item[-] 45 | 若T是普通类型或右值引用,则生成T\&\&。 46 | 47 | \item[-] 48 | 若T是一个左值引用,它会产生一个T\&。 49 | 50 | \item[-] 51 | 若T是void,它就产生void。 52 | \end{itemize} 53 | 54 | \item 55 | 参阅第19.3.4节和第11.2.3节,以及第D.5节中common\_type<>类型特性,可以看看它的示例。 56 | \end{itemize} 57 | 58 | \textbf{std::addressof(r)} 59 | 60 | \begin{itemize} 61 | \item 62 | 头文件中定义。 63 | 64 | \item 65 | 即使为对象或函数的类型重载了操作符\&,也会产生对象或函数r的地址。 66 | 67 | \item 68 | 详见第11.2.2节。 69 | \end{itemize} 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /content/Appendix/D/images/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Cpp-Templates-2nd/579a1728a33ba4e1f410354a4a427e1668f37190/content/Appendix/D/images/1.png -------------------------------------------------------------------------------- /content/Appendix/E/0.tex: -------------------------------------------------------------------------------- 1 | 多年来,C++语言设计人员一直在探索如何约束模板的参数。例如,我们的原型max()模板中,希望预先声明,对于不能使用小于操作符进行比较的类型,不应该调用。其他模板可能希望使用有效的“迭代器”类型(对于该术语的一些正式定义)或有效的“算术”类型(可能比内置算术类型集更广泛的概念)进行实例化。 2 | 3 | 概念是一个或多个模板参数的命名约束集。开发C++11标准的过程中,为概念设计了一个丰富的系统。但将该特性集成到语言规范中的话,需要太多的委员会资源,概念最终从C++11中删除了。过了一段时间,就有一种不同的特性设计提出,概念似乎将以某种形式进入语言。就在这本书将要印刷之际,标准化委员会投票决定将新设计的概念集成到C++20的草案中。这里,我们将介绍这种新设计的概念。 4 | 5 | 本书的主要章节中,已经提出并展示了一些概念的应用: 6 | 7 | \begin{itemize} 8 | \item 9 | 第6.5节说明了如何使用需求和概念来启用构造函数,只有当模板参数可转换为字符串时(以避免意外地将构造函数用作复制构造函数)。 10 | 11 | \item 12 | 第18.4节展示了如何使用概念,来指定和要求用于表示几何对象类型的约束。 13 | \end{itemize} -------------------------------------------------------------------------------- /content/Appendix/E/2.tex: -------------------------------------------------------------------------------- 1 | 概念很像bool类型的constexpr变量模板,但类型没有显式指定: 2 | 3 | \begin{lstlisting}[style=styleCXX] 4 | template concept LessThanComparable = ... ; 5 | \end{lstlisting} 6 | 7 | 这里的“…”可以用一个表达式来代替,该表达式使用各种特征来确定,类型T是否确实可以使用小于操作符进行比较。但是概念提供了一个工具可简化这个任务:requires表达式(与上面描述的requires子句不同)。以下是这个概念的完整定义: 8 | 9 | \begin{lstlisting}[style=styleCXX] 10 | template 11 | concept LessThanComparable = requires(T x, T y) { 12 | { x < y } -> bool; 13 | }; 14 | \end{lstlisting} 15 | 16 | 请注意require表达式如何包含一个可选参数列表:这些参数永远不会用参数替换,可以认为是一组“哑变量”,可用来在require表达式体中表达需求。短语表达了这样的需求 17 | 18 | \begin{lstlisting}[style=styleCXX] 19 | { x < y } -> bool; 20 | \end{lstlisting} 21 | 22 | 这种语法意味着(a)表达式x < y必须在SFINAE意义上有效,(b)表达式的结果必须可转换为bool类型。这种形式的短语中,关键字noexcept可以插入到\texttt{->}标记之前,以表示大括号中的表达式不会抛出异常(即,应用于该表达式的noexcept(…)为true。若不需要这样的约束,短语的隐式转换部分(即\texttt{->}类型)可以省略,若只需要检查表达式的有效性,则可以删除大括号,这样短语就可以简化为表达式。 23 | 24 | \begin{lstlisting}[style=styleCXX] 25 | template 26 | concept Swappable = requires(T x, T y) { 27 | swap(x, y); 28 | }; 29 | \end{lstlisting} 30 | 31 | requires表达式还可以表达对关联类型的需求。考虑前面假设的序列概念:除了要求seq.begin()等表达式的有效性外,还需要相应的序列迭代器类型。可以表示为: 32 | 33 | \begin{lstlisting}[style=styleCXX] 34 | template 35 | concept Sequence = requires(Seq seq) { 36 | typename Seq::iterator; 37 | { seq.begin() } -> Seq::iterator; 38 | ... 39 | }; 40 | \end{lstlisting} 41 | 42 | typename type;表示类型存在的需求(这称为类型需求)。本例中,必须存在的类型是概念模板参数的成员,但不一定总是这样,可以要求存在一个IteratorFor类型,通过require短语实现 43 | 44 | \begin{lstlisting}[style=styleCXX] 45 | ... 46 | typename IteratorFor; 47 | ... 48 | \end{lstlisting} 49 | 50 | 上面的Sequence概念定义展示了,通过逐个列出短语来组合短语。还有第三类需求短语,其只包含调用另一个概念。假设有一个迭代器的概念,希望序列概念不仅要求Seq::iterator是一种类型,而且要求该类型满足iterator概念的约束条件。表达式如下: 51 | 52 | \begin{lstlisting}[style=styleCXX] 53 | template 54 | concept Sequence = requires(Seq seq) { 55 | typename Seq::iterator; 56 | requires Iterator; 57 | { seq.begin() } -> Seq::iterator; 58 | ... 59 | }; 60 | \end{lstlisting} 61 | 62 | 也就是说,可以在requires表达式中添加子句(这种短语视为嵌套需求)。 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /content/Appendix/E/4.tex: -------------------------------------------------------------------------------- 1 | 2 | 尽管C++的概念已经研究了很多年,并且实验性的实现已经出现了十多年,但广泛的使用才刚刚开始。我们希望本书的未来版本能够提供关于如何设计受约束模板库的实用指南,我们先给出了三个观察结果。 3 | 4 | \subsubsubsection{E.4.1\hspace{0.2cm}测试概念} 5 | 6 | 概念是布尔谓词,有效的常量表达式。给定一个概念C和一些类型T1, T2,…模型的概念,可以静态地断言观察: 7 | 8 | \begin{lstlisting}[style=styleCXX] 9 | static_assert(C, "Model failure"); 10 | \end{lstlisting} 11 | 12 | 设计概念时,建议也设计以这种方式测试其简单类型。包括挑战概念界限的类型,则需要回答如下问题: 13 | 14 | \begin{itemize} 15 | \item 16 | 接口和/或算法需要复制和/或移动建模类型的对象吗? 17 | 18 | \item 19 | 哪些转换是可以接受的?需要哪些转换? 20 | 21 | \item 22 | 模板假定的基本操作集唯一么?例如,可以使用*=或*和=操作吗? 23 | \end{itemize} 24 | 25 | 这里,了解概念的原型(参见28.3节)也很有用。 26 | 27 | \subsubsubsection{E.4.2\hspace{0.2cm}概念的粒度} 28 | 29 | 随着概念成为C++语言的一部分,就可以对“概念库”进行构建了,就像我们在这些特性可用时构建类库和模板库一样。与其他库一样,我们也很希望以各种方式对概念进行分层。这里,简要地讨论了迭代器类别的例子,假设可以在这些类别之外构建“范围类别”,或者在这些类别之上构建“序列概念”等。 30 | 31 | 另一方面,会试图在“基本语法”概念的基础上构建所有这些概念。例如: 32 | 33 | \begin{lstlisting}[style=styleCXX] 34 | template 35 | concept Addable = 36 | requires (T x, U y) { 37 | x + y; 38 | } 39 | \end{lstlisting} 40 | 41 | 不建议这样做,因为这是一个没有明确语义的概念。当T和U都是std::string或者当一个类型是指针而另一个是整型,当然还有算术类型时,概念条件就满足了。在这三种情况下,可添加的概念有一些不同的含义(分别是连接、迭代器位移和算术加法)。因此,引入这样的概念将导致库接口模糊,并可能引起歧义。 42 | 43 | 相反,概念似乎最适合用于建模问题领域中出现的语义概念。以一种纪律性的方式来做这件事,会改善库的整体设计,因为将给使用者带来一致和明确的接口。当标准模板库(STL)添加到C++标准库中时,情况就是如此。尽管它没有使用基于语言的“概念”,但在设计时在很大程度上考虑了概念的思想(如迭代器和迭代器层次结构),其余的都已成为历史了。 44 | 45 | \subsubsubsection{E.4.3\hspace{0.2cm}二进制兼容性} 46 | 47 | 资深C++开发者知道,当某些实体(特别是函数和成员函数)会在编译为低层机器码时,相关联的名称会将声明名称与实体类型和作用范围相结合。这个名称,通常称为实体的重组名称,是实际为链接器提供实体的引用(例如,来自其他对象文件)的名称。例如,定义为的函数的重组名称 48 | 49 | \begin{lstlisting}[style=styleCXX] 50 | namespace X { 51 | void f() {} 52 | } 53 | \end{lstlisting} 54 | 55 | 使用EvItanium C++ ABI [ItaniumABI]时是\_ZN1X1f,其中的字母X和f分别来自命名空间名和函数名。 56 | 57 | 混乱的名称不能在程序中“冲突”,若两个函数可能在一个程序中共存,那必须具有不同的、混乱的名称。反之,约束必须在函数名中编码(因为模板特化除了约束和函数体之外,其他方面都是相同的,可以出现在不同的翻译单元中)。考虑以下两个翻译单元: 58 | 59 | \begin{lstlisting}[style=styleCXX] 60 | #include 61 | 62 | template 63 | concept HasPlus = requires (T x, T y) { 64 | x+y; 65 | }; 66 | 67 | template int f(T p) requires HasPlus { 68 | std::cout << "TU1\n"; 69 | } 70 | 71 | void g(); 72 | 73 | int main() { 74 | f(1); 75 | g(); 76 | } 77 | \end{lstlisting} 78 | 79 | 和 80 | 81 | \begin{lstlisting}[style=styleCXX] 82 | #include 83 | 84 | template 85 | concept HasMult = requires (T x, T y) { 86 | x*y; 87 | }; 88 | 89 | template int f(T p) requires HasMult { 90 | std::cout << "TU2\n"; 91 | } 92 | 93 | template int f(int); 94 | 95 | void g() { 96 | f(2); 97 | } 98 | \end{lstlisting} 99 | 100 | 程序必须输出 101 | 102 | \begin{tcblisting}{commandshell={}} 103 | TU1 104 | TU2 105 | \end{tcblisting} 106 | 107 | 这意味着f()的两个定义必须以不同的方式处理。 108 | 109 | \begin{tcolorbox}[colback=webgreen!5!white,colframe=webgreen!75!black] 110 | \hspace*{0.75cm}GCC 7.1中对概念的实验实现在这方面有缺陷。 111 | \end{tcolorbox} 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | -------------------------------------------------------------------------------- /content/preface.tex: -------------------------------------------------------------------------------- 1 | 2 | \begin{flushright} 3 | \zihao{1} 前言 4 | \end{flushright} 5 | 6 | C++模板在1990年就在“带注释的C++参考手册”(ARM;参见[ellisstroustrparm])中出现了,至今已经30多年了。在此之前,有更专业的书籍中对其详细论述过。十多年后,我们发现对于这个神秘、复杂、强大的C++特性,相应的基本概念介绍和高级技术文献居然少的可怜。本书的第一版中,我们想要解决这个问题,并决定写一本关于模板的书(有点不够谦逊)。 7 | 8 | 自2002年底发布第一版本后,C++发生了很大的变化。C++标准增加了很多新特性,C++社区不断创新也有助于展示基于模板的新编程技术。因此,本书的第二版保留了与第一版相同的目标,但使用的是“现代C++”。 9 | 10 | 我们以不同背景和不同目的来完成写书的任务。David(又名“Daveed”)是一位经验丰富的编译器作者,也是发展核心语言的C++标准委员会工作组的积极参与者,他对模板的所有功能(和问题)的精确和详细描述很感兴趣。Nico是一名“普通”的程序员,C++标准委员会库工作组的成员,他对模板的技术很感兴趣。Doug是一名模板库开发人员,成为编译器作者和语言设计师后,对收集、分类和评估用于构建模板库的技术很有兴趣。此外,我们希望与读者和整个社区积极分享这些知识,以减少误解、困惑或忧虑。 11 | 12 | 因此,会有示例概念介绍和模板行为的描述。从模板的原则开始,逐步发展到“模板的艺术”,将展现(或重新了解)静态多态性、类型特征、元编程和表达式模板等编程方式。还会更深入地了解C++标准库,其中所有的代码都会涉及模板。 13 | 14 | 写这本书的过程中,我们也学到了很多,也获得了很多快乐。希望在阅读本书时,您也能享受这本书带给您的快乐。 -------------------------------------------------------------------------------- /cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Cpp-Templates-2nd/579a1728a33ba4e1f410354a4a427e1668f37190/cover.jpg --------------------------------------------------------------------------------