├── Cover.png ├── LICENSE ├── Modern-C++-Programming-Cookbook.tex ├── README.md └── book ├── book.tex ├── ccs.tex ├── content ├── Author.tex ├── Foreword.tex ├── Preface.tex ├── Reviewers.tex ├── chapter1 │ ├── 0.tex │ ├── 1.tex │ ├── 10.tex │ ├── 11.tex │ ├── 12.tex │ ├── 13.tex │ ├── 14.tex │ ├── 15.tex │ ├── 2.tex │ ├── 3.tex │ ├── 4.tex │ ├── 5.tex │ ├── 6.tex │ ├── 7.tex │ ├── 8.tex │ └── 9.tex ├── chapter10 │ ├── 0.tex │ ├── 1.tex │ ├── 2.tex │ ├── 3.tex │ ├── 4.tex │ ├── 5.tex │ ├── 6.tex │ ├── 7.tex │ ├── 8.tex │ └── 9.tex ├── chapter11 │ ├── 0.tex │ ├── 1.tex │ ├── 10.tex │ ├── 11.tex │ ├── 12.tex │ ├── 13.tex │ ├── 14.tex │ ├── 2.tex │ ├── 3.tex │ ├── 4.tex │ ├── 5.tex │ ├── 6.tex │ ├── 7.tex │ ├── 8.tex │ └── 9.tex ├── chapter12 │ ├── 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 │ │ ├── 2.png │ │ └── 3.png ├── chapter2 │ ├── 0.tex │ ├── 1.tex │ ├── 10.tex │ ├── 11.tex │ ├── 12.tex │ ├── 13.tex │ ├── 14.tex │ ├── 15.tex │ ├── 16.tex │ ├── 17.tex │ ├── 2.tex │ ├── 3.tex │ ├── 4.tex │ ├── 5.tex │ ├── 6.tex │ ├── 7.tex │ ├── 8.tex │ ├── 9.tex │ └── images │ │ ├── 1.png │ │ ├── 10.png │ │ ├── 11.png │ │ ├── 12.png │ │ ├── 13.png │ │ ├── 14.png │ │ ├── 2.png │ │ ├── 3.png │ │ ├── 4.png │ │ ├── 5.png │ │ ├── 6.png │ │ ├── 7.png │ │ ├── 8.png │ │ ├── 9.png │ │ ├── code-1.png │ │ ├── code-2.png │ │ ├── code-3.png │ │ ├── code-4.png │ │ └── table-1.png ├── chapter3 │ ├── 0.tex │ ├── 1.tex │ ├── 10.tex │ ├── 2.tex │ ├── 3.tex │ ├── 4.tex │ ├── 5.tex │ ├── 6.tex │ ├── 7.tex │ ├── 8.tex │ └── 9.tex ├── chapter4 │ ├── 0.tex │ ├── 1.tex │ ├── 2.tex │ ├── 3.tex │ ├── 4.tex │ ├── 5.tex │ └── 6.tex ├── chapter5 │ ├── 0.tex │ ├── 1.tex │ ├── 10.tex │ ├── 11.tex │ ├── 12.tex │ ├── 2.tex │ ├── 3.tex │ ├── 4.tex │ ├── 5.tex │ ├── 6.tex │ ├── 7.tex │ ├── 8.tex │ ├── 9.tex │ └── images │ │ ├── 1.png │ │ ├── 2.png │ │ ├── 3.png │ │ └── 4.png ├── chapter6 │ ├── 0.tex │ ├── 1.tex │ ├── 10.tex │ ├── 11.tex │ ├── 12.tex │ ├── 13.tex │ ├── 14.tex │ ├── 15.tex │ ├── 16.tex │ ├── 17.tex │ ├── 18.tex │ ├── 19.tex │ ├── 2.tex │ ├── 3.tex │ ├── 4.tex │ ├── 5.tex │ ├── 6.tex │ ├── 7.tex │ ├── 8.tex │ └── 9.tex ├── chapter7 │ ├── 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 ├── chapter8 │ ├── 0.tex │ ├── 1.tex │ ├── 10.tex │ ├── 11.tex │ ├── 12.tex │ ├── 13.tex │ ├── 14.tex │ ├── 2.tex │ ├── 3.tex │ ├── 4.tex │ ├── 5.tex │ ├── 6.tex │ ├── 7.tex │ ├── 8.tex │ ├── 9.tex │ └── images │ │ ├── 1.png │ │ ├── 2.png │ │ ├── 3.png │ │ ├── 4.png │ │ └── 5.png └── chapter9 │ ├── 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 └── index.tex /Cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Modern-CXX-Programming-Cookbook/383ba582e840773422eb2bbe76f420cc98ba6b4e/Cover.png -------------------------------------------------------------------------------- /Modern-C++-Programming-Cookbook.tex: -------------------------------------------------------------------------------- 1 | 2 | \include{book/ccs.tex} 3 | \include{book/book.tex} 4 | 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Modern C++ Programming Cookbook 2 | *Third Edition* 3 | 4 | ***精通现代C++与掌握C++23最新特性的140多种实用技巧*** 5 | 6 | * 作者:Marius Bancila 7 | * 译者:陈晓伟 8 | * Packt Publishing Ltd. (出版于: 2024年2月29日) 9 | 10 | > [!IMPORTANT] 11 | > 翻译是译者用自己的思想,换一种语言,对原作者想法的重新阐释。鉴于我的学识所限,误解和错译在所难免。如果你能买到本书的原版,且有能力阅读英文,请直接去读原文。因为与之相较,我的译文可能根本不值得一读。 12 | > 13 | >

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

14 | 15 | ## 本书概述 16 | 17 | 第三版的更新内容涵盖了 C++23 的最新特性,如堆栈库、expected 和 mdspan 类型、span 缓冲区、格式化库的改进,以及范围库的更新。此外,还探讨了之前未涉及的一些 C++20 特性,例如:同步输出流和 source_location。 18 | 19 | 本书按照实用示例的形式编排,涵盖了很多实际问题,能助各位快速找到所需的解决方案。书中全面覆盖了现代 C++ 编程的所有核心概念,以及从 C++11 到 C++23 的特性和技术,能够通过学习融入最新的语言和库改进,保持领先。 20 | 21 | 除了核心概念和新特性之外,还将探索与性能和最佳实践相关的示例,了解如何实现诸如 pimpl、命名参数、律师-客户模式和工厂模式等模式和惯用法,以及如何使用主流的 C++ 测试库——Boost.Test、Google Test 和 Catch2 完成单元测试。 22 | 23 | 到书末时,凭借这本书提供的全面覆盖,各位读者将拥有构建高效、可扩展,且性能优异的应用程序所需的一切知识。 24 | 25 | ## 作者简介 26 | 27 | **Marius Bancila**是一位拥有二十余年经验的软件工程师,专注于为商务应用及其他领域开发解决方案,也是《Template Metaprogramming with C++》和《The Modern C++ Challenge》的作者。作为一名软件架构师,专注于Microsoft的技术,主要使用C++和C\#开发。Marius对于分享自己的技术专长充满热情,因此自2006年起,他因在C++和开发者技术领域的贡献而被授予Microsoft MVP称号。Marius居住在罗马尼亚,并在多个在线社区中保持活跃。 28 | 29 | 30 | 31 | ## 本书相关 32 | 33 | * github翻译地址:https://github.com/xiaoweiChen/Modern-CXX-Programming-Cookbook 34 | 35 | * 译文的LaTeX 环境配置:https://www.cnblogs.com/1625--H/p/11524968.html 36 | 37 | * 禁用拼写检查:https://blog.csdn.net/weixin_39278265/article/details/87931348 38 | 39 | * 使用xelatex编译时需要添加`-shell-escape`和`-8bit`选项,例如: 40 | 41 | `xelatex -synctex=1 -interaction=nonstopmode -shell-escape -8bit "Modern-CMake-for-C++-2ed".tex` 42 | 43 | * 为了内容中表格和目录索引能正常生成,至少需要连续编译两次 44 | 45 | * Latex中的中文字体([思源宋体](https://github.com/adobe-fonts/source-han-serif/releases))和英文字体([Hack](https://github.com/source-foundry/Hack-windows-installer/releases/tag/v1.6.0)),需要安装后自行配置。如何配置请参考主book/css.tex顶部关于字体的信息。 46 | 47 | * vscode中配置LaTeX:https://blog.csdn.net/Ruins_LEE/article/details/123555016 48 | 49 | -------------------------------------------------------------------------------- /book/book.tex: -------------------------------------------------------------------------------- 1 | %\special{dvipdfmx:config z 0} %取消PDF压缩,加快速度,最终版本生成的时候最好把这句话注释掉 2 | 3 | %实用的工具宏包 syntonly。加载这个宏包后,在导言区使用 \syntaxonly 命令,可令 LATEX 编译后不生成 DVI 或者 PDF 文档,只排查错误,编译速度会快不少: 4 | %\usepackage{syntonly} 5 | %\syntaxonly 6 | 7 | \begin{document} 8 | \renewcommand{\fcolorbox}[4][]{#4} %去掉minted代码块中的红框 9 | \begin{sloppypar} %latex中一行文字出现溢出问题的解决方法 10 | %\maketitle 11 | 12 | \subfile{book/index.tex} 13 | 14 | \end{sloppypar} 15 | \end{document} -------------------------------------------------------------------------------- /book/content/Author.tex: -------------------------------------------------------------------------------- 1 | \noindent 2 | Marius Bancila是一位拥有二十余年经验的软件工程师,专注于为商务应用开发解决方案,也是《Template Metaprogramming with C++》和《The Modern C++ Challenge》的作者。作为一名软件架构师,特别专注于Microsoft的技术,并主要使用C++和C\#开发。Marius对于分享自己的技术专长充满热情,因此自2006年起,他因在C++和开发者技术领域的贡献收获Microsoft MVP的称号。Marius居住在罗马尼亚,并活跃在多个在线社区中。 3 | 4 | 5 | \hspace*{\fill} 6 | 7 | \begin{center} 8 | 9 | \textit{ 10 | 感谢Denim Pinto、Yamini Bhandari、Elliot Dallow,以及Packt出版社的其他工作人员,感谢他们的努力,同时也感谢审稿人,他们的反馈使得这本书更加完善。 11 | } 12 | 13 | \end{center} 14 | 15 | 16 | -------------------------------------------------------------------------------- /book/content/Foreword.tex: -------------------------------------------------------------------------------- 1 | 2 | 在C++的不断发展的中,掌握CMake对于致力于编写高效、可维护和可扩展程序的开发者来说是必不可少的。《Modern CMake for C++》(由Rafał Świdziński撰写)就像一座灯塔,引导开发者们穿梭于CMake之中。 3 | 4 | 这本书不仅仅是一本手册,也是一次旅行。从基础开始,随着章节的深入,读者将掌握高阶技术。本书的独特之处在于其实用的方法,现实例子和最佳实践贯穿全文,确保读者不仅理解概念,而且知道如何在项目中应用。 5 | 6 | 读完这本书后,读者不仅会对CMake有深入的理解,会对驾驭C++有更多的信心。这是有助于编写更干净、更高效代码所需的知识和技能,进而成为熟练的CMake开发者。 7 | 8 | 《Modern CMake for C++》不仅仅是一本书,也是一个工具,将提升读者的C++开发技能。无论是初学者还是专家,这本书都将有助你掌握CMake,使代码更加健壮、可维护和可扩展。 9 | 10 | \begin{center} 11 | \textit{Alexander Kushnir} 12 | 13 | \textit{Biosense Webster 首席软件工程师} 14 | \end{center} 15 | 16 | -------------------------------------------------------------------------------- /book/content/Reviewers.tex: -------------------------------------------------------------------------------- 1 | \noindent 2 | Deák Ferenc最初踏入软件开发领域时,编程还要解读古老的助记符,并将其手动输入到HC-91计算机中。后来转型成为一名专注于安全和保障的软件开发者,目前在该领域拥有超过20年的经验。他擅长底层系统编程、优化以及应用程序安全性分析。Deák Ferenc精通C和C++语言,同时在Go、Java、Python等其他编程语言中也具有深厚的专业知识。工作之余,他喜欢烹制特兰西瓦尼亚的传统美食。 3 | \hspace*{\fill} 4 | 5 | \begin{center} 6 | 7 | \textit{ 8 | 我想要向Packt出版社及作者表达我最深的感激之情,感谢他们让我参与这段激动人心的旅程,并给予我审阅此书的机会。 9 | } 10 | 11 | \end{center} 12 | 13 | \hspace*{\fill} 14 | \hspace*{\fill} 15 | 16 | \noindent 17 | Alex Snape一直对技术充满热情,这促使他投身于软件开发领域,至今已有近三十年。在Alex的职业生涯中,他曾在多个行业领域工作,从视频游戏到金融系统,以及许多其他领域都有所涉猎。目前,Alex在英国公共基础设施领域担任软件架构师。 18 | 19 | \hspace*{\fill} 20 | 21 | \begin{center} 22 | 23 | \textit{ 24 | 我想感谢我的父母,美丽的妻子Zoe,以及可爱的孩子们,Aurelia和Marilla。 25 | } 26 | 27 | \end{center} 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /book/content/chapter1/0.tex: -------------------------------------------------------------------------------- 1 | 随着C++11、C++14、C++17、C++20和C++23的陆续推出,C++经历了多次重大的变革。这些新标准引入了很多新概念,简化并扩展了现有的语法和语义,改变了C++的编程方式。现在,C++11看起来和感觉上就像是全新的语言,使用这些新标准编写的代码可称为“现代C++”代码。本章将介绍C++11引入的一些语言特性,这些特性能够帮助完成许多编程任务。然而,语言的核心详解超出了本章讨论的范畴,其他章节会探讨不同的特性。 2 | 3 | 本章包括以下内容: 4 | 5 | \begin{itemize} 6 | \item 7 | 尽可能使用auto 8 | 9 | \item 10 | 创建类型和模板的别名 11 | 12 | \item 13 | 统一初始化 14 | 15 | \item 16 | 非静态成员的初始化 17 | 18 | \item 19 | 控制和查询对象的对齐状态 20 | 21 | \item 22 | 作用域枚举 23 | 24 | \item 25 | 对虚函数使用override和final 26 | 27 | \item 28 | 基于范围的for循环 29 | 30 | \item 31 | 为自定义类型启用基于范围的for循环 32 | 33 | \item 34 | 避免隐式转换——显式构造函数和转换操作符 35 | 36 | \item 37 | 使用匿名命名空间 38 | 39 | \item 40 | 使用内联命名空间进行API版本控制 41 | 42 | \item 43 | 使用结构化绑定处理多个返回值 44 | 45 | \item 46 | 使用类模板参数推导简化代码 47 | 48 | \item 49 | 使用下标操作符访问集合中的元素 50 | \end{itemize} 51 | 52 | 53 | -------------------------------------------------------------------------------- /book/content/chapter1/11.tex: -------------------------------------------------------------------------------- 1 | 2 | 当程序需要链接到多个翻译单元时,遇到命名冲突的概率就越大。在源文件中声明的、本意只为该翻译单元局部使用的函数或变量,可能与其他翻译单元中声明的类似函数或变量发生冲突。 3 | 4 | 因为所有未声明为static的符号都具有外部链接性,其名称在整个程序中必须唯一。这个问题的传统C语言解决方法是将这些符号声明为static,从而将其链接性从外部改为内部,使得它们成为翻译单元的局部变量。另一种方法是在名字前加上所属模块或库的名字作为前缀。本示例中,将介绍C++对此问题的解决方案。 5 | 6 | \mySubsubsection{}{Getting ready} 7 | 8 | 本示例中,将介绍全局函数和静态函数的概念,以及变量、命名空间和翻译单元。希望读者们对这些概念有一个基本的理解。除此之外,还需要了解内部链接和外部链接的区别;这是本示例的关键。 9 | 10 | \mySubsubsection{}{How to do it...} 11 | 12 | 需要将全局符号声明为static以避免链接问题的情况时,应该优先使用匿名命名空间: 13 | 14 | \begin{enumerate} 15 | \item 16 | 在源文件中声明一个没有名字的命名空间。 17 | 18 | \item 19 | 将全局函数或变量的定义放在匿名命名空间中,而不需要将其声明为static。 20 | \end{enumerate} 21 | 22 | 以下示例展示了两个不同的翻译单元中定义的名为print()的函数;每个函数都在匿名命名空间中定义: 23 | 24 | \begin{cpp} 25 | // file1.cpp 26 | namespace 27 | { 28 | void print(std::string const & message) 29 | { 30 | std::cout << "[file1] " << message << '\n'; 31 | } 32 | } 33 | void file1_run() 34 | { 35 | print("run"); 36 | } 37 | // file2.cpp 38 | namespace 39 | { 40 | void print(std::string const & message) 41 | { 42 | std::cout << "[file2] " << message << '\n'; 43 | } 44 | } 45 | void file2_run() 46 | { 47 | print("run"); 48 | } 49 | \end{cpp} 50 | 51 | \mySubsubsection{}{How it works...} 52 | 53 | 当一个函数在一个翻译单元中声明时,具有外部链接性,所以来自两个不同翻译单元的同名函数会导致链接错误(不能有两个同名符号)。这个问题在C和C++中,通过将函数或变量声明为static来解决,从而改变其链接性从外部变为内部。这种情况下,其名字不再导出到翻译单元之外,链接问题也就得以避免。 54 | 55 | C++中的正确解决方案是使用匿名命名空间,像前面那样定义命名空间时,编译器会将其转换为如下形式: 56 | 57 | \begin{cpp} 58 | // file1.cpp 59 | namespace _unique_name_ {} 60 | using namespace _unique_name_; 61 | namespace _unique_name_ 62 | { 63 | void print(std::string message) 64 | { 65 | std::cout << "[file1] " << message << '\n'; 66 | } 67 | } 68 | void file1_run() 69 | { 70 | print("run"); 71 | } 72 | \end{cpp} 73 | 74 | 首先,声明一个具有唯一名称的命名空间(这个名字是什么以及如何生成,取决于编译器的具体实现)。此时,命名空间为空,这一行的基本目的是建立命名空间。其次,using指令将\_unique\_name\_命名空间中的所有内容引入当前命名空间。最后,使用编译器生成的名称定义命名空间。 75 | 76 | 通过在匿名命名空间中定义翻译单元局部的print()函数,仅具有局部可见性,但由于具有外部唯一的名称,不会因外部链接性而导致链接错误。 77 | 78 | 匿名命名空间还在涉及模板的一个或许更为隐蔽的情况下发挥作用。C++11之前,模板非类型参数不能是具有内部链接性的名称,因此不能使用静态变量。匿名命名空间中的符号具有外部链接性,可以作为模板参数使用。尽管C++11放宽了对模板非类型参数的链接限制,但在最新版本的VC++编译器中,这一限制仍然存在: 79 | 80 | \begin{cpp} 81 | template 82 | class test {}; 83 | static int Size1 = 10; 84 | namespace 85 | { 86 | int Size2 = 10; 87 | } 88 | test t1; 89 | test t2; 90 | \end{cpp} 91 | 92 | t1变量的声明会导致编译错误,非类型参数表达式Size1具有内部链接性。t2变量的声明是正确的,Size2具有外部链接性。(注意,使用Clang和GCC编译此代码段不会出错) 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /book/content/chapter1/12.tex: -------------------------------------------------------------------------------- 1 | C++11标准引入了一种新的命名空间类型——内联命名空间——是一种机制,使得嵌套命名空间中的声明看起来和行为就像是外围命名空间的一部分。内联命名空间通过在命名空间声明中使用inline关键字来定义(匿名命名空间也可以内联)。这对库版本控制非常有用,在本示例中,将介绍如何使用内联命名空间来对符号进行版本控制。通过本示例,可以了解如何使用内联命名空间和条件编译来对源码进行版本管理。 2 | 3 | \mySubsubsection{}{Getting ready} 4 | 5 | 本示例中,将介绍命名空间和嵌套命名空间、模板和模板特化,以及使用预处理器宏进行条件编译。熟悉这些概念是继续进行本示例的前提。 6 | 7 | \mySubsubsection{}{How to do it...} 8 | 9 | 为了提供库的多个版本,并让用户决定使用哪个版本,请按照以下步骤操作: 10 | 11 | \begin{itemize} 12 | \item 13 | 在命名空间内定义库的内容。 14 | 15 | \item 16 | 在内部内联命名空间内定义库的每个版本或部分。 17 | 18 | \item 19 | 使用预处理器宏和\#if指令来启用库的特定版本。 20 | \end{itemize} 21 | 22 | 以下示例展示了有两个版本的库: 23 | 24 | \begin{cpp} 25 | namespace modernlib 26 | { 27 | #ifndef LIB_VERSION_2 28 | inline namespace version_1 29 | { 30 | template 31 | int test(T value) { return 1; } 32 | } 33 | #endif 34 | #ifdef LIB_VERSION_2 35 | inline namespace version_2 36 | { 37 | template 38 | int test(T value) { return 2; } 39 | } 40 | #endif 41 | } 42 | \end{cpp} 43 | 44 | \mySubsubsection{}{How it works...} 45 | 46 | 内联命名空间的成员可视为外围命名空间的成员。这样的成员可以偏特化、显式实例化或显式特化。这是一个传递属性,如果命名空间A包含一个内联命名空间B,而B又包含一个内联命名空间C,则C的成员看起来就像是B和A的成员,而B的成员看起来就像是A的成员。 47 | 48 | 为了更好地理解为什么内联命名空间是有帮助的,来看一个随着时间发展从第一个版本演进到第二个版本(及以后)的库。这个库在名为modernlib的命名空间下定义了所有的类型和函数。在第一个版本中,这个库可能如下所示: 49 | 50 | \begin{cpp} 51 | namespace modernlib 52 | { 53 | template 54 | int test(T value) { return 1; } 55 | } 56 | \end{cpp} 57 | 58 | 库的用户可以进行如下调用并返回值1: 59 | 60 | \begin{cpp} 61 | auto x = modernlib::test(42); 62 | \end{cpp} 63 | 64 | 但用户可能会决定特化模板函数test(): 65 | 66 | \begin{cpp} 67 | struct foo { int a; }; 68 | namespace modernlib 69 | { 70 | template<> 71 | int test(foo value) { return value.a; } 72 | } 73 | auto y = modernlib::test(foo{ 42 }); 74 | \end{cpp} 75 | 76 | 因为调用了特化函数,y的值不再是1而是42。 77 | 78 | 到目前为止一切正常,但作为库开发者,当决定创建库的第二个版本,但仍同时发布第一版和第二版,并让客户端通过宏来控制使用哪一个版本。第二个版本中,提供了新的test()函数实现,不再返回1而是返回2。 79 | 80 | 为了能够同时提供第一版和第二版的实现,将其放入名为version\_1和version\_2的嵌套命名空间中,并使用预处理器宏有条件地编译库: 81 | 82 | \begin{cpp} 83 | namespace modernlib 84 | { 85 | namespace version_1 86 | { 87 | template 88 | int test(T value) { return 1; } 89 | } 90 | #ifndef LIB_VERSION_2 91 | using namespace version_1; 92 | #endif 93 | namespace version_2 94 | { 95 | template 96 | int test(T value) { return 2; } 97 | } 98 | #ifdef LIB_VERSION_2 99 | using namespace version_2; 100 | #endif 101 | } 102 | \end{cpp} 103 | 104 | 无论用户使用的是库的第一个版本还是第二个版本,代码都出错了。这是因为test函数现在位于嵌套命名空间中,而foo的特化是在modernlib命名空间中完成的,实际上应该是完成在modernlib::version\_1或modernlib::version\_2中。这是因为模板的特化要求必须在声明该模板的同一命名空间中完成。 105 | 106 | 这种情况下,用户需要更改代码: 107 | 108 | \begin{cpp} 109 | #define LIB_VERSION_2 110 | #include "modernlib.h" 111 | struct foo { int a; }; 112 | namespace modernlib 113 | { 114 | namespace version_2 115 | { 116 | template<> 117 | int test(foo value) { return value.a; } 118 | } 119 | } 120 | \end{cpp} 121 | 122 | 这是一个问题,库泄露了实现细节,客户需要了解这些细节才能进行模板特化。这些内部细节可以通过内联命名空间以本示例“How to do it...”部分所示的方式隐藏起来。通过定义modernlib库,当在modernlib命名空间中进行模板特化时,用户特化test()函数的代码不再出错,因为无论是version\_1::test()还是version\_2::test()(具体取决于用户实际使用的是哪个版本)都表现得像是外围modernlib命名空间的一部分。现在的实现细节对用户来说不可见,用户只能看到外围命名空间modernlib。 123 | 124 | 然而,std命名空间为标准保留,永远不应该内联。另外,如果一个命名空间在其第一次定义时不是内联,那么也不应该定义为内联。 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | -------------------------------------------------------------------------------- /book/content/chapter1/2.tex: -------------------------------------------------------------------------------- 1 | 2 | C++可以通过typedef创建用作类型名替代的同义词,比如:为类型创建更短或更有意义的名字,或者是为函数指针创建名字。然而,typedef声明不能与模板一起来创建模板类型别名。例如,std::vector不是一个类型(std::vector才是一个类型),而是一种类型族。当类型占位符T替换为实际类型时,就可以创建这些类型。 3 | 4 | 从C++11开始,类型别名是指向另一种已经声明的类型的名称,而别名模板是指向另一种已经声明的模板的名称。这两种类型的别名可以使用using。 5 | 6 | \mySubsubsection{}{How to do it...} 7 | 8 | \begin{itemize} 9 | \item 10 | using identifier = type-id来创建类型别名: 11 | 12 | \begin{cpp} 13 | using byte = unsigned char; 14 | using byte_ptr = unsigned char *; 15 | using array_t = int[10]; 16 | using fn = void(byte, double); 17 | void func(byte b, double d) { /*...*/ } 18 | byte b{42}; 19 | byte_ptr pb = new byte[10] {0}; 20 | array_t a{0,1,2,3,4,5,6,7,8,9}; 21 | fn* f = func; 22 | \end{cpp} 23 | 24 | \item 25 | template identifier = type-id创建别名模板: 26 | 27 | \begin{cpp} 28 | template 29 | class custom_allocator { /* ... */ }; 30 | template 31 | using vec_t = std::vector>; 32 | vec_t vi; 33 | vec_t vs; 34 | \end{cpp} 35 | 36 | \end{itemize} 37 | 38 | 为了保持一致性和可读性: 39 | 40 | \begin{itemize} 41 | \item 42 | 创建别名时不混合使用typedef和using。 43 | 44 | \item 45 | 创建函数指针类型名称时首选using。 46 | \end{itemize} 47 | 48 | \mySubsubsection{}{How it works...} 49 | 50 | typedef声明引入了类型名称的同义词(就是别名),不会引入新类型(像类、结构体、联合或枚举)。通过typedef声明引入的类型名称,遵循与标识符名称相同的隐藏规则。为了引用相同类型的同义词可以重新声明(只要是对同一类型的同义词,就可以在一个翻译单元中进行多次typedef声明): 51 | 52 | \begin{cpp} 53 | typedef unsigned char byte; 54 | typedef unsigned char * byte_ptr; 55 | typedef int array_t[10]; 56 | typedef void(*fn)(byte, double); 57 | template 58 | class foo { 59 | typedef T value_type; 60 | }; 61 | typedef std::vector vint_t; 62 | typedef int INTEGER; 63 | INTEGER x = 10; 64 | typedef int INTEGER; // redeclaration of same type 65 | INTEGER y = 20; 66 | \end{cpp} 67 | 68 | 类型别名声明等价于typedef声明,可以出现在块作用域、类型作用域或命名空间作用域中。根据C++11标准(第9.2.4段,文档版本N4917): 69 | 70 | \begin{myTip} 71 | 类型定义名称也可以通过别名声明来引入。关键字using之后的标识符成为一个类型定义名,紧跟在标识符后面的可选属性说明符序列属于该类型定义名。其语义与通过类型定义说明符引入的一样。特别是,它不定义新的类型,也不应该出现在类型ID中。 72 | \end{myTip} 73 | 74 | 当涉及到为数组类型和函数指针类型创建别名时,别名声明更加易读,也更清晰地表明了别名的实际类型。在“How to do it...”部分的示例中,很容易理解array\_t是包含10个整数数组类型的名称,而fn是接受两个byte和double类型参数并返回void的函数类型的名称,与声明std::function对象的语法相一致(例如,std::function f)。 75 | 76 | 还需要注意以下几点: 77 | 78 | \begin{itemize} 79 | \item 80 | 别名模板不能部分或显式特化。 81 | 82 | \item 83 | 模板参数推导过程中,别名模板永远不会推导为模板参数。 84 | 85 | \item 86 | 特化别名模板时产生的类型,不允许直接或间接地使用自定义类型。 87 | \end{itemize} 88 | 89 | 新语法的主要目的是定义别名模板。这些模板在特化时,等同于将别名模板的模板参数替换到类型ID后得到的结果。 90 | 91 | 92 | 93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /book/content/chapter1/7.tex: -------------------------------------------------------------------------------- 1 | C++ 没有专门用于声明接口(只有纯虚函数的类)的语法,而且在声明虚函数方面也存在一些不足之处。在 C++ 中,虚函数通过 virtual 关键字引入。然而,virtual 关键字在派生类中声明重载方法时是可选的,这在处理大型类或层次结构时可能导致混淆。可能需要遍历整个层次结构,直至基类来确定一个函数是否是虚函数。另一方面,有时确保一个虚函数或派生类不能再重载或进一步派生也可用。本节中,将介绍如何使用 C++11 引入的特殊标识符 override 和 final 来声明虚函数或类型。 2 | 3 | \mySubsubsection{}{Getting ready} 4 | 5 | 应该熟悉 C++ 中的继承和多态概念,以及抽象类型、纯虚函数、虚函数和重载方法的概念。 6 | 7 | \mySubsubsection{}{How to do it...} 8 | 9 | 为了确保正确声明基类和派生类中的虚方法,同时提高可读性,请遵循以下步骤: 10 | 11 | \begin{itemize} 12 | \item 13 | 派生类中声明用于覆盖基类虚函数的虚函数时使用 virtual 关键字。 14 | 15 | \item 16 | 在虚函数声明或定义的声明符部分后面使用 override 特殊标识符: 17 | 18 | \begin{cpp} 19 | class Base 20 | { 21 | virtual void foo() = 0; 22 | virtual void bar() {} 23 | virtual void foobar() = 0; 24 | }; 25 | void Base::foobar() {} 26 | class Derived1 : public Base 27 | { 28 | virtual void foo() override = 0; 29 | virtual void bar() override {} 30 | virtual void foobar() override {} 31 | }; 32 | class Derived2 : public Derived1 33 | { 34 | virtual void foo() override {} 35 | }; 36 | \end{cpp} 37 | \end{itemize} 38 | 39 | \begin{myNotic} 40 | 声明符是指函数类型中除了返回类型的部分。 41 | \end{myNotic} 42 | 43 | 为了确保函数不能再进一步覆盖,或类不能再进一步派生,可以使用 final 标识符: 44 | 45 | \begin{itemize} 46 | \item 47 | 虚函数声明或定义的声明符部分后面使用 final 防止派生类中的覆盖: 48 | 49 | \begin{cpp} 50 | class Derived2 : public Derived1 51 | { 52 | virtual void foo() final {} 53 | }; 54 | \end{cpp} 55 | 56 | \item 57 | 类型声明的类型名后面使用 final 防止类型的派生: 58 | 59 | \begin{cpp} 60 | class Derived4 final : public Derived1 61 | { 62 | virtual void foo() override {} 63 | }; 64 | \end{cpp} 65 | 66 | \end{itemize} 67 | 68 | \mySubsubsection{}{How it works...} 69 | 70 | override 的工作方式非常简单;在虚函数声明或定义中,确保该函数实际上覆盖了基类中的函数;如果不是这样,编译器将报错。 71 | 72 | override 和 final 这两个特殊标识符仅在成员函数声明或定义中具有特殊含义。不是保留关键字,可以在程序的其他地方作为用户定义的标识符使用。 73 | 74 | 使用 override 特殊标识符可以帮助编译器检测虚方法没有覆盖另一个方法的情况,如下例所示: 75 | 76 | \begin{cpp} 77 | class Base 78 | { 79 | public: 80 | virtual void foo() {} 81 | virtual void bar() {} 82 | }; 83 | class Derived1 : public Base 84 | { 85 | public: 86 | void foo() override {} 87 | // 为了提高可读性,建议使用 virtual 关键字 88 | virtual void bar(char const c) override {} 89 | // 错误,Base 中没有 bar(char const) 函数 90 | }; 91 | \end{cpp} 92 | 93 | 如果没有 override 标识符的存在,Derived1 类中的 virtual bar(char const) 将不是一个覆盖函数,而是 Base 类型中 bar() 函数的一个重载。 94 | 95 | 另一个特殊标识符 final 用于成员函数声明或定义中,表明该函数是虚函数并且不能在派生类中覆盖。如果派生类试图覆盖虚函数,编译器将报错: 96 | 97 | \begin{cpp} 98 | class Derived2 : public Derived1 99 | { 100 | virtual void foo() final {} 101 | }; 102 | class Derived3 : public Derived2 103 | { 104 | virtual void foo() override {} // 错误 105 | }; 106 | \end{cpp} 107 | 108 | final 标识符也可以用于类声明中,表明该类不能派生: 109 | 110 | \begin{cpp} 111 | class Derived4 final : public Derived1 112 | { 113 | virtual void foo() override {} 114 | }; 115 | class Derived5 : public Derived4 // 错误 116 | { 117 | }; 118 | \end{cpp} 119 | 120 | 由于 override 和 final 在特定上下文中使用时具有特殊含义,事实上并不是保留关键字,可以在 C++ 代码的其他地方使用。这确保了在 C++11 之前的代码,不会因为这些名称当作标识符而出现问题: 121 | 122 | \begin{cpp} 123 | class foo 124 | { 125 | int final = 0; 126 | void override() {} 127 | }; 128 | \end{cpp} 129 | 130 | 虽然前面的建议提到了在声明重载的虚方法时应同时使用 virtual 和 override,但 virtual 关键字实际上是可选的,可以省略以缩短声明。override 标识符的存在应该足以向读者表明该方法是虚方法。这更多是一个个人偏好问题,并不影响语义。 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | -------------------------------------------------------------------------------- /book/content/chapter1/8.tex: -------------------------------------------------------------------------------- 1 | 2 | 许多编程语言支持一种称为 for-each 的循环——即对集合中的元素重复执行一组语句。直到 C++11,才在核心语言层面支持这种特性,尽管之前标准库中有一个通用算法 std::for\_each,可以将一个函数应用于某个范围内的所有元素。C++11 引入了语言级别的 for-each 支持,称为基于范围的for循环 (Range-Based For Loops)。新的 C++17 标准对这一原始语言特性进行了改进。 3 | 4 | \mySubsubsection{}{Getting ready} 5 | 6 | C++11 中,基于范围的循环语法如下: 7 | 8 | \begin{cpp} 9 | for ( range_declaration : range_expression ) loop_statement 10 | \end{cpp} 11 | 12 | 从 C++20 开始,在范围声明之前可以有一个初始化语句(必须以分号结束): 13 | 14 | \begin{cpp} 15 | for(init-statement range-declaration : range-expression) 16 | loop-statement 17 | \end{cpp} 18 | 19 | 为了举例说明使用基于范围循环的各种方式,将使用以下返回元素序列的函数: 20 | 21 | \begin{cpp} 22 | std::vector getRates() 23 | { 24 | return std::vector {1, 1, 2, 3, 5, 8, 13}; 25 | } 26 | std::multimap getRates2() 27 | { 28 | return std::multimap { 29 | { 1, true }, 30 | { 1, true }, 31 | { 2, false }, 32 | { 3, true }, 33 | { 5, true }, 34 | { 8, false }, 35 | { 13, true } 36 | }; 37 | } 38 | \end{cpp} 39 | 40 | \mySubsubsection{}{How to do it...} 41 | 42 | 基于范围循环可用的多种方式: 43 | 44 | \begin{itemize} 45 | \item 46 | 通过为序列中的元素指定特定类型: 47 | 48 | \begin{cpp} 49 | auto rates = getRates(); 50 | for (int rate : rates) 51 | std::cout << rate << '\n'; 52 | for (int& rate : rates) 53 | rate *= 2; 54 | \end{cpp} 55 | 56 | \item 57 | 不指定类型,让编译器推断: 58 | 59 | \begin{cpp} 60 | for (auto&& rate : getRates()) 61 | std::cout << rate << '\n'; 62 | for (auto & rate : rates) 63 | rate *= 2; 64 | for (auto const & rate : rates) 65 | std::cout << rate << '\n'; 66 | \end{cpp} 67 | 68 | \item 69 | 使用 C++17 中的结构化绑定和分解声明: 70 | 71 | \begin{cpp} 72 | for (auto&& [rate, flag] : getRates2()) 73 | std::cout << rate << '\n'; 74 | \end{cpp} 75 | \end{itemize} 76 | 77 | 78 | \mySubsubsection{}{How it works...} 79 | 80 | "How to do it..."中展示的范围基循环表达式实际上是语法糖,编译器会将其转换成其他东西。C++17 之前,编译器生成的代码为: 81 | 82 | \begin{cpp} 83 | { 84 | auto && __range = range_expression; 85 | for (auto __begin = begin_expr, __end = end_expr; 86 | __begin != __end; ++__begin) { 87 | range_declaration = *__begin; 88 | loop_statement 89 | } 90 | } 91 | \end{cpp} 92 | 93 | begin\_expr 和 end\_expr 是什么取决于范围的类型: 94 | 95 | \begin{itemize} 96 | \item 97 | 对于 C 风格数组:\_\_range 和 \_\_range + \_\_bound(其中 \_\_bound 是数组中的元素数量)。 98 | 99 | \item 100 | 对于带有 begin 和 end 成员的类型(不论其类型和访问权限):\_\_range.begin() 和 \_\_range.end()。 101 | 102 | \item 103 | 对于其他类型,是 begin(\_\_range) 和 end(\_\_range),这些通过参数依赖查找确定。 104 | \end{itemize} 105 | 106 | 需要注意的是,如果一个类型包含名为 begin 或 end 的成员(无论是函数、数据成员还是枚举器),无论其类型和访问权限如何,这些成员都会选作 begin\_expr 和 end\_expr,所以这样的类型不能用于基于范围的循环。 107 | 108 | C++17 开始,编译器生成的代码略有不同: 109 | 110 | \begin{cpp} 111 | { 112 | auto && __range = range_expression; 113 | auto __begin = begin_expr; 114 | auto __end = end_expr; 115 | for (; __begin != __end; ++__begin) { 116 | range_declaration = *__begin; 117 | loop_statement 118 | } 119 | } 120 | \end{cpp} 121 | 122 | 新标准移除了 begin 表达式和 end 表达式必须是同种类型的限制。end 表达式不必是一个真正的迭代器,但必须能够与迭代器进行不等比较。原因是,范围可以由谓词界定。相反,end 表达式只会在首次使用时计算一次,而不是每次循环迭代时都重新计算,这会提高程序的性能。 123 | 124 | C++20 开始,范围声明前可以有一个初始化语句: 125 | 126 | \begin{cpp} 127 | { 128 | init-statement 129 | auto && __range = range_expression; 130 | auto __begin = begin_expr; 131 | auto __end = end_expr; 132 | for (; __begin != __end; ++__begin) { 133 | range_declaration = *__begin; 134 | loop_statement 135 | } 136 | } 137 | \end{cpp} 138 | 139 | 初始化语句可以是空语句、表达式语句、简单声明,或者从 C++23 开始,还可以是别名声明: 140 | 141 | \begin{cpp} 142 | for (auto rates = getRates(); int rate : rates) 143 | { 144 | std::cout << rate << '\n'; 145 | } 146 | \end{cpp} 147 | 148 | C++23 之前,这对于避免范围表达式中的临时对象导致未定义行为很有帮助。范围表达式返回的临时对象的生命期会延长到循环结束。但如果这些临时对象会在范围表达式结束时销毁,则它们的生命周期不会延长。 149 | 150 | 可以通过以下代码段来解释这一点: 151 | 152 | \begin{cpp} 153 | struct item 154 | { 155 | std::vector getRates() 156 | { 157 | return std::vector {1, 1, 2, 3, 5, 8, 13}; 158 | } 159 | }; 160 | item make_item() 161 | { 162 | return item{}; 163 | } 164 | // undefined behavior, until C++23 165 | for (int rate : make_item().getRates()) 166 | { 167 | std::cout << rate << '\n'; 168 | } 169 | \end{cpp} 170 | 171 | 由于 make\_item() 按值返回,在范围表达式中有一个临时对象。这会导致未定义行为,可以通过使用初始化语句来避免: 172 | 173 | \begin{cpp} 174 | for (auto item = make_item(); int rate : item.getRates()) 175 | { 176 | std::cout << rate << '\n'; 177 | } 178 | \end{cpp} 179 | 180 | 这个问题在 C++23 中不再出现,因为标准延长了范围表达式内所有临时对象的生命周期,会到循环结束。 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | -------------------------------------------------------------------------------- /book/content/chapter1/9.tex: -------------------------------------------------------------------------------- 1 | 2 | 基于范围的循环(其他编程语言中称为 for-each)允许迭代一个范围的元素,相对于标准的 for 循环提供了一个简化的语法。但基于范围的循环并不适用于所有表示范围的类型,其要求存在 begin() 和 end() 函数(对于非数组类型),这些函数可以是成员函数也可以是独立函数。本示例中,将介绍如何让自定义类型能够在基于范围的循环中使用。 3 | 4 | \mySubsubsection{}{Getting ready} 5 | 6 | 为了展示如何使表示序列的自定义类型支持基于范围的循环,可以使用以下简单的数组实现: 7 | 8 | \begin{cpp} 9 | template 10 | class dummy_array 11 | { 12 | T data[Size] = {}; 13 | public: 14 | T const & GetAt(size_t const index) const 15 | { 16 | if (index < Size) return data[index]; 17 | throw std::out_of_range("index out of range"); 18 | } 19 | void SetAt(size_t const index, T const & value) 20 | { 21 | if (index < Size) data[index] = value; 22 | else throw std::out_of_range("index out of range"); 23 | } 24 | size_t GetSize() const { return Size; } 25 | }; 26 | \end{cpp} 27 | 28 | 本示例的目标是让如下代码正常工作: 29 | 30 | \begin{cpp} 31 | dummy_array arr; 32 | arr.SetAt(0, 1); 33 | arr.SetAt(1, 2); 34 | arr.SetAt(2, 3); 35 | for(auto&& e : arr) 36 | { 37 | std::cout << e << '\n'; 38 | } 39 | \end{cpp} 40 | 41 | \mySubsubsection{}{How to do it...} 42 | 43 | 要使自定义类型支持给予范围的循环,需要做以下几件事: 44 | 45 | \begin{itemize} 46 | \item 47 | 创建该类型的可变和常量迭代器,这些迭代器必须实现以下操作符: 48 | 49 | \begin{itemize} 50 | \item 51 | operator++ (前置和后置版本)用于递增迭代器。 52 | 53 | \item 54 | operator* 用于解引用迭代器并访问迭代器指向的实际元素。 55 | 56 | \item 57 | operator!= 用于将迭代器与其他迭代器进行不等比较。 58 | \end{itemize} 59 | 60 | \item 61 | 为该类型提供独立的 begin() 和 end() 函数。 62 | \end{itemize} 63 | 64 | 基于上述示例,需要提供以下内容: 65 | 66 | \begin{itemize} 67 | \item 68 | 下面是最小化迭代器的类型实现: 69 | 70 | \begin{cpp} 71 | template 72 | class dummy_array_iterator_type 73 | { 74 | public: 75 | dummy_array_iterator_type(C& collection, 76 | size_t const index) : 77 | index(index), collection(collection) 78 | { } 79 | bool operator!= (dummy_array_iterator_type const & other) const 80 | { 81 | return index != other.index; 82 | } 83 | T const & operator* () const 84 | { 85 | return collection.GetAt(index); 86 | } 87 | dummy_array_iterator_type& operator++() 88 | { 89 | ++index; 90 | return *this; 91 | } 92 | dummy_array_iterator_type operator++(int) 93 | { 94 | auto temp = *this; 95 | ++*this; 96 | return temp; 97 | } 98 | private: 99 | size_t index; 100 | C& collection; 101 | }; 102 | \end{cpp} 103 | 104 | \item 105 | 可变和常量迭代器的别名模板: 106 | 107 | \begin{cpp} 108 | template 109 | using dummy_array_iterator = 110 | dummy_array_iterator_type< 111 | T, dummy_array, Size>; 112 | template 113 | using dummy_array_const_iterator = 114 | dummy_array_iterator_type< 115 | T, dummy_array const, Size>; 116 | \end{cpp} 117 | 118 | \item 119 | 返回相应开始和结束迭代器的独立 begin() 和 end() 函数,包括两种别名模板的重载: 120 | 121 | \begin{cpp} 122 | template 123 | inline dummy_array_iterator begin( 124 | dummy_array& collection) 125 | { 126 | return dummy_array_iterator(collection, 0); 127 | } 128 | template 129 | inline dummy_array_iterator end( 130 | dummy_array& collection) 131 | { 132 | return dummy_array_iterator( 133 | collection, collection.GetSize()); 134 | } 135 | template 136 | inline dummy_array_const_iterator begin( 137 | dummy_array const & collection) 138 | { 139 | return dummy_array_const_iterator( 140 | collection, 0); 141 | } 142 | template 143 | inline dummy_array_const_iterator end( 144 | dummy_array const & collection) 145 | { 146 | return dummy_array_const_iterator( 147 | collection, collection.GetSize()); 148 | } 149 | \end{cpp} 150 | 151 | \end{itemize} 152 | 153 | \mySubsubsection{}{How it works...} 154 | 155 | 有了上述实现,先前展示的基于范围的循环就能编译和执行。当执行依赖查找时,编译器会找到编写的两个 begin() 和 end() 函数(接受 dummy\_array 的引用),因此生成的代码就有效了。 156 | 157 | 上述示例中,定义了迭代器类模板和两个别名模板,分别为 dummy\_array\_iterator 和 dummy\_array\_const\_iterator。begin() 和 end() 函数都有这两种迭代器类型的重载。以便使用基于范围的循环,来处理常量和非常量实例的容器: 158 | 159 | \begin{cpp} 160 | template 161 | void print_dummy_array(dummy_array const & arr) 162 | { 163 | for (auto && e : arr) 164 | { 165 | std::cout << e << '\n'; 166 | } 167 | } 168 | \end{cpp} 169 | 170 | 使自定义类型支持基于范围的循环,一种替代方案是提供成员 begin() 和 end() 函数,这只有在拥有并可以修改源码的情况下才有意义。 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | -------------------------------------------------------------------------------- /book/content/chapter10/0.tex: -------------------------------------------------------------------------------- 1 | 2 | 设计模式是通用的可重用解决方案,可以应用于软件开发中出现的常见问题。惯用法(idioms)是指在一种或多种编程语言中结构化代码的模式、算法或方式。关于设计模式已经有许多书籍出版。本章的目的不是重复这些内容,而是展示如何在现代C++中实现几个有用的设计模式和惯用法,重点关注代码的可读性、性能和健壮性。 3 | 4 | 本章包括以下实例: 5 | 6 | \begin{itemize} 7 | \item 8 | 工厂模式中避免重复的if-else语句 9 | 10 | \item 11 | 实现pimpl惯用法 12 | 13 | \item 14 | 实现命名参数惯用法 15 | 16 | \item 17 | 使用非虚接口惯用法分离接口和实现 18 | 19 | \item 20 | 使用律师-客户惯用法处理友元关系 21 | 22 | \item 23 | 使用奇异递归模板模式实现静态多态 24 | 25 | \item 26 | 使用混入(mixins)为类型添加功能 27 | 28 | \item 29 | 使用类型擦除惯用法以泛型方式处理不相关类型 30 | 31 | \item 32 | 实现线程安全的单例模式 33 | \end{itemize} 34 | -------------------------------------------------------------------------------- /book/content/chapter10/1.tex: -------------------------------------------------------------------------------- 1 | 2 | 编程中,经常发现自己编写重复的 if...else 语句(或等效的 switch 语句),这些语句执行相似的操作,通常只有细微的变化,而且往往是通过复制粘贴再做少量修改来完成的。随着替代条件的数量增加,代码变得既难以阅读又难以维护。重复的 if...else 语句可以通过多种技术来替换,比如多态性。本示例中,将介绍如何使用函数映射来避免工厂模式中的 if...else 语句(工厂是一个用于创建其他实例的函数或对象)。 3 | 4 | \mySubsubsection{}{Getting ready} 5 | 6 | 本示例中,会考虑以下问题:构建一个能够处理各种格式图像文件的系统,如位图、PNG、JPG 等等。显然,细节超出了这个实例的范围;关注的部分是创建处理各种图像格式的对象。为此,将考虑以下类层次结构: 7 | 8 | \begin{cpp} 9 | class Image {}; 10 | class BitmapImage : public Image {}; 11 | class PngImage : public Image {}; 12 | class JpgImage : public Image {}; 13 | \end{cpp} 14 | 15 | 另一方面,定义一个工厂类的接口,可以创建上述类的实例,以及一个使用 if...else 语句的典型实现: 16 | 17 | \begin{cpp} 18 | struct IImageFactory 19 | { 20 | virtual std::unique_ptr Create(std::string_view type) = 0; 21 | }; 22 | struct ImageFactory : public IImageFactory 23 | { 24 | std::unique_ptr 25 | Create(std::string_view type) override 26 | { 27 | if (type == "bmp") 28 | return std::make_unique(); 29 | else if (type == "png") 30 | return std::make_unique(); 31 | else if (type == "jpg") 32 | return std::make_unique(); 33 | return nullptr; 34 | } 35 | }; 36 | \end{cpp} 37 | 38 | \mySubsubsection{}{How to do it...} 39 | 40 | 按照以下步骤重构前面显示的工厂以避免使用 if...else 语句: 41 | 42 | \begin{enumerate} 43 | \item 44 | 实现工厂接口: 45 | 46 | \begin{cpp} 47 | struct ImageFactory : public IImageFactory 48 | { 49 | std::unique_ptr Create(std::string_view type) override 50 | { 51 | // continued with 2. and 3. 52 | } 53 | }; 54 | \end{cpp} 55 | 56 | \item 57 | 定义一个map,其中键是创建对象的类型,值是一个创建这些实例的函数: 58 | 59 | \begin{cpp} 60 | static std::map< 61 | std::string, 62 | std::function()>> mapping 63 | { 64 | { "bmp", []() {return std::make_unique(); } }, 65 | { "png", []() {return std::make_unique(); } }, 66 | { "jpg", []() {return std::make_unique(); } } 67 | }; 68 | \end{cpp} 69 | 70 | \item 71 | 要创建一个对象,可以在map中查找类型,如果找到了,就使用与之关联的函数来创建该类型的实例: 72 | 73 | \begin{cpp} 74 | auto it = mapping.find(type.data()); 75 | if (it != mapping.end()) 76 | return it->second(); 77 | return nullptr; 78 | \end{cpp} 79 | \end{enumerate} 80 | 81 | \mySubsubsection{}{How it works...} 82 | 83 | 最初的实现中,重复的 if...else 语句非常相似——检查类型参数的值,并创建适当图像类的实例。如果要检查的参数是整数类型(例如,枚举类型),if...else 语句序列也可以用 switch 语句的形式编写: 84 | 85 | \begin{cpp} 86 | auto factory = ImageFactory{}; 87 | auto image = factory.Create("png"); 88 | \end{cpp} 89 | 90 | 无论是使用 if...else 语句还是 switch,重构以避免重复检查都相对简单。重构后的代码中,使用了一个键类型为 std::string 的映射来表示类型,即图像格式的名称。值是一个 \verb|std::function()>|。这是一个接受无参数并返回 \verb|std::unique_ptr|(派生类的 \verb|unique_ptr| 可以隐式转换为基类的 \verb|unique_ptr|)的函数的包装器。 91 | 92 | 现在有了这个创建对象的函数映射,工厂的实际实现变得更加简单;在映射中检查要创建的对象类型,如果存在,则使用映射中的相应值作为实际创建对象的函数,或者如果对象类型不在映射中则返回 nullptr。 93 | 94 | 这种重构对于用户代码来说是透明的,因为使用工厂的方式没有变化。另一方面,这种方法需要更多的内存来处理静态映射,这在某些应用类别中,如物联网(IoT),可能很重要。这里展示的例子相对简单,目的是为了说明概念。实际中,可能需要以不同的方式创建对象,比如使用不同数量和类型的参数。然而,这不是重构实现所特有的,使用 if...else/switch 语句的解决方案也需要考虑到这一点。实践中,使用 if...else 语句解决问题的方案也应适用于映射。 95 | 96 | \mySubsubsection{}{There's more...} 97 | 98 | 前面的实现中,映射是虚拟函数的局部静态变量,但也可以是类的成员甚至全局变量。下面的实现将映射定义为类的静态成员。对象不是根据格式名创建的,而是根据 typeid 操作符返回类型的信息创建: 99 | 100 | \begin{cpp} 101 | struct IImageFactoryByType 102 | { 103 | virtual std::unique_ptr Create( 104 | std::type_info const & type) = 0; 105 | }; 106 | struct ImageFactoryByType : public IImageFactoryByType 107 | { 108 | std::unique_ptr Create(std::type_info const & type) 109 | override 110 | { 111 | auto it = mapping.find(&type); 112 | if (it != mapping.end()) 113 | return it->second(); 114 | return nullptr; 115 | } 116 | private: 117 | static std::map< 118 | std::type_info const *, 119 | std::function()>> mapping; 120 | }; 121 | std::map< 122 | std::type_info const *, 123 | std::function()>> ImageFactoryByType::mapping 124 | { 125 | {&typeid(BitmapImage),[](){ 126 | return std::make_unique();}}, 127 | {&typeid(PngImage), [](){ 128 | return std::make_unique();}}, 129 | {&typeid(JpgImage), [](){ 130 | return std::make_unique();}} 131 | }; 132 | \end{cpp} 133 | 134 | 用户代码略有不同,因为传递的是由 typeid 操作符返回的值,而不是表示要创建的类型的名称,例如 PNG,传递的是 typeid(PngImage): 135 | 136 | \begin{cpp} 137 | auto factory = ImageFactoryByType{}; 138 | auto movie = factory.Create(typeid(PngImage)); 139 | \end{cpp} 140 | 141 | 这种替代方法更加健壮,因为映射的键不是字符串,后者更容易出错。本示例提出了一种模式作为解决常见问题的方案,而不是具体的实现。就像大多数模式一样,可以有多种实现方式,可选择最适合特定上下文的方法。 142 | 143 | 144 | -------------------------------------------------------------------------------- /book/content/chapter10/3.tex: -------------------------------------------------------------------------------- 1 | 2 | C++ 只支持位置参数,可根据参数的位置传递给函数。其他语言还支持命名参数——调用时指定参数名称和调用参数。对于具有默认值的参数,这很有用。一个函数可能有带有默认值的参数,尽管其总是出现在所有非默认参数之后。 3 | 4 | 一种称为命名参数惯用法的技术,提供了一种方法来模拟命名参数并帮助解决“为默认参数之前的参数传参”的问题。 5 | 6 | \mySubsubsection{}{Getting ready} 7 | 8 | 为了举例说明命名参数惯用法,将使用如下代码中的 control 类型: 9 | 10 | \begin{cpp} 11 | class control 12 | { 13 | int id_; 14 | std::string text_; 15 | int width_; 16 | int height_; 17 | bool visible_; 18 | public: 19 | control( 20 | int const id, 21 | std::string_view text = "", 22 | int const width = 0, 23 | int const height = 0, 24 | bool const visible = false): 25 | id_(id), text_(text), 26 | width_(width), height_(height), 27 | visible_(visible) 28 | {} 29 | }; 30 | \end{cpp} 31 | 32 | control 类型代表一个可视控件,如按钮或输入框,并具有数值标识符、文本、尺寸和可见性等属性。这些属性提供给构造函数,除了 ID 之外,所有其他的都有默认值。实际上,这样的类型会有许多其他属性,比如:文本画笔、背景画笔、边框样式、字体大小、字体系列等。 33 | 34 | \mySubsubsection{}{How to do it...} 35 | 36 | 为了实现一个函数(通常有很多默认参数)的命名参数惯用法,按照以下步骤操作: 37 | 38 | \begin{enumerate} 39 | \item 40 | 创建一个类型来包装函数的参数: 41 | 42 | \begin{cpp} 43 | class control_properties 44 | { 45 | int id_; 46 | std::string text_; 47 | int width_ = 0; 48 | int height_ = 0; 49 | bool visible_ = false; 50 | }; 51 | \end{cpp} 52 | 53 | \item 54 | 需要访问这些属性的类或函数,可以声明为友元(以避免编写多余的 getter): 55 | 56 | \begin{cpp} 57 | friend class control; 58 | \end{cpp} 59 | 60 | \item 61 | 原始函数中,每个没有默认值的位置参数,应该成为类构造函数的一个位置参数,且没有默认值: 62 | 63 | \begin{cpp} 64 | public: 65 | control_properties(int const id) :id_(id) 66 | {} 67 | \end{cpp} 68 | 69 | \item 70 | 对于原始函数中每个有默认值的位置参数,应该有一个同名的函数,其内部设置值并返回一个对该类的引用: 71 | 72 | \begin{cpp} 73 | public: 74 | control_properties& text(std::string_view t) 75 | { text_ = t.data(); return *this; } 76 | control_properties& width(int const w) 77 | { width_ = w; return *this; } 78 | control_properties& height(int const h) 79 | { height_ = h; return *this; } 80 | control_properties& visible(bool const v) 81 | { visible_ = v; return *this; } 82 | \end{cpp} 83 | 84 | \item 85 | 应该修改原始函数,或者提供一个重载版本,以接受一个新类的参数,从中读取属性值: 86 | 87 | \begin{cpp} 88 | control(control_properties const & cp): 89 | id_(cp.id_), 90 | text_(cp.text_), 91 | width_(cp.width_), 92 | height_(cp.height_), 93 | visible_(cp.visible_) 94 | {} 95 | \end{cpp} 96 | \end{enumerate} 97 | 98 | 综上所述,结果如下: 99 | 100 | \begin{cpp} 101 | class control; 102 | class control_properties 103 | { 104 | int id_; 105 | std::string text_; 106 | int width_ = 0; 107 | int height_ = 0; 108 | bool visible_ = false; 109 | friend class control; 110 | public: 111 | control_properties(int const id) :id_(id) 112 | {} 113 | control_properties& text(std::string_view t) 114 | { text_ = t.data(); return *this; } 115 | control_properties& width(int const w) 116 | { width_ = w; return *this; } 117 | control_properties& height(int const h) 118 | { height_ = h; return *this; } 119 | control_properties& visible(bool const v) 120 | { visible_ = v; return *this; } 121 | }; 122 | class control 123 | { 124 | int id_; 125 | std::string text_; 126 | int width_; 127 | int height_; 128 | bool visible_; 129 | public: 130 | control(control_properties const & cp): 131 | id_(cp.id_), 132 | text_(cp.text_), 133 | width_(cp.width_), 134 | height_(cp.height_), 135 | visible_(cp.visible_) 136 | {} 137 | }; 138 | \end{cpp} 139 | 140 | \mySubsubsection{}{How it works...} 141 | 142 | 最初的 control 类有一个带有多个参数的构造函数。实际中,可以找到参数数量很多的例子。实践中常遇到的一种解决方案是,将常见的布尔类型属性组合成位标志,这些位标志可以作为一个单一的整数参数一起传递(例如,控件的边框样式,定义边框应在哪些位置可见:顶部、底部、左侧、右侧,或者是这四个位置的任意组合)。使用初始实现创建一个 control 实例如下所示: 143 | 144 | \begin{cpp} 145 | control c(1044, "sample", 100, 20, true); 146 | \end{cpp} 147 | 148 | 命名参数惯用法的优点在于,允许仅为想要的参数指定值,顺序任意,使用名称指定,比固定的位置顺序直观得多。 149 | 150 | 虽然实现这一惯用法并没有单一的策略,但本示例相当典型。control 类型的构造函数中提供的属性已放入一个单独的类型中,称为 \verb|control_properties|,将 control 类型声明为友元类,允许访问其私有数据成员而无需提供 getter 方法。这有一个副作用,即限制了 \verb|control_properties| 在 control 类型外部的使用。control 类型构造函数的非可选参数也是 \verb|control_properties| 构造函数的非可选参数。对于所有其他带有默认值的参数,\verb|control_properties| 类型定义了一个具有相关名称的函数,该函数只是将数据成员设置为提供的参数,然后返回一个 \verb|control_properties| 的引用。这使得用户能够以任意顺序链式调用这些函数。 151 | 152 | control 类型的构造函数已经替换为一个新的构造函数,该构造函数只有一个参数,即对 \verb|control_properties| 实例的常量引用,其数据成员会复制到 control 实例的数据成员中。 153 | 154 | 使用这种方式实现的命名参数惯用法,创建一个 control 实例: 155 | 156 | \begin{cpp} 157 | control c(control_properties(1044) 158 | .visible(true) 159 | .height(20) 160 | .width(100)); 161 | \end{cpp} 162 | 163 | 164 | -------------------------------------------------------------------------------- /book/content/chapter10/4.tex: -------------------------------------------------------------------------------- 1 | 2 | 虚函数允许派生类修改基类的实现来为类提供专业化点。当通过指向基类的指针或引用处理派生类对象时,对重写虚函数的调用最终会调用派生类中的重写实现。另一方面,定制是一个实现细节,良好的设计应将接口与实现分离。 3 | 4 | 非虚接口惯用法(Non-Virtual Interface Idiom),由 Herb Sutter 在《C/C++ Users Journal》的一篇文章中提出,通过使(公共)接口非虚且虚函数私有化,促进了接口与实现关注点的分离。 5 | 6 | 公共虚接口阻止了类型对其接口施加前置条件和后置条件,因为这些方法可以在派生类中重写,所以期望基类实例不能保证公共虚函数的预期行为。此惯用法有助于强制执行接口的契约。 7 | 8 | \mySubsubsection{}{Getting ready} 9 | 10 | 读者应对与虚函数相关的方面有所了解,包括定义和覆盖虚函数、抽象类和纯虚函数说明符。 11 | 12 | \mySubsubsection{}{How to do it...} 13 | 14 | 实现此惯用法需要遵循几个简单的设计指南,这些指南由 Herb Sutter 在《C/C++ Users Journal》19(9),2001 年 9 月期中提出: 15 | 16 | \begin{itemize} 17 | \item 18 | 将(公共)接口设为非虚。 19 | 20 | \item 21 | 将虚函数设为私有。 22 | 23 | \item 24 | 如果基类实现必须从派生类调用,则将虚函数设为受保护。 25 | 26 | \item 27 | 将基类的析构函数设为公共且虚,或设为受保护且非虚。 28 | \end{itemize} 29 | 30 | 以下是一个简单的控件层次结构的例子,遵循了上述所有四条指导原则: 31 | 32 | \begin{cpp} 33 | class control 34 | { 35 | private: 36 | virtual void paint() = 0; 37 | protected: 38 | virtual void erase_background() 39 | { 40 | std::cout << "erasing control background..." << '\n'; 41 | } 42 | public: 43 | void draw() 44 | { 45 | erase_background(); 46 | paint(); 47 | } 48 | virtual ~control() {} 49 | }; 50 | class button : public control 51 | { 52 | private: 53 | virtual void paint() override 54 | { 55 | std::cout << "painting button..." << '\n'; 56 | } 57 | protected: 58 | virtual void erase_background() override 59 | { 60 | control::erase_background(); 61 | std::cout << "erasing button background..." << '\n'; 62 | } 63 | }; 64 | class checkbox : public button 65 | { 66 | private: 67 | virtual void paint() override 68 | { 69 | std::cout << "painting checkbox..." << '\n'; 70 | } 71 | protected: 72 | virtual void erase_background() override 73 | { 74 | button::erase_background(); 75 | std::cout << "erasing checkbox background..." << '\n'; 76 | } 77 | }; 78 | \end{cpp} 79 | 80 | \mySubsubsection{}{How it works...} 81 | 82 | NVI 惯用法使用了模板方法设计模式,允许派生类定制基类功能(即算法)的部分(即步骤)。通过将算法分解成更小的部分来完成,每部分都由一个虚函数实现。基类可以提供或不提供默认实现,而派生类可以对其进行覆盖,同时保持算法的整体结构和意义。 83 | 84 | NVI 惯用法的核心原则是虚函数不应公开;应该是私有的,或者在基类中为受保护时,可以使用派生类调用。类型的接口,即用户可访问的公共部分,应完全由非虚函数组成。这有几个优点: 85 | 86 | \begin{itemize} 87 | \item 88 | 接口与不再暴露给客户的实现细节分开。 89 | 90 | \item 91 | 在不改变公共接口的情况下更改实现细节成为可能,而无需更改用户代码,从而使基类更加健壮。 92 | 93 | \item 94 | 允许一个类型对其接口拥有唯一的控制权。如果公共接口包含虚方法,派生类可以改变承诺的功能,所以无法确保其前置条件和后置条件。当除析构函数外的虚方法都不为用户所访问时,类型可以强制执行其接口的前置条件和后置条件。 95 | \end{itemize} 96 | 97 | \begin{myNotic} 98 | 对于这个惯用法,需要特别提到类的析构函数。通常强调基类的析构函数应该是虚的,以便实例可以多态地删除(通过指向基类的指针或引用)。当析构函数非虚时,多态地销毁实例会导致未定义的行为。然而,并非所有的基类都是打算多态地删除。对于这些情况,基类的析构函数不应该声明为虚。但也不应该是公共的,而应该是受保护的。 99 | \end{myNotic} 100 | 101 | 前一节中的例子定义了一个表示视觉控件的类层次结构: 102 | 103 | \begin{itemize} 104 | \item 105 | control 是基类,但有派生类,如 button 和 checkbox,是按钮的一种类型,因此是从这个类派生出来的。 106 | 107 | \item 108 | control 类定义的唯一功能是绘制控件。\verb|draw()| 方法非虚,但调用了两个虚方法,\verb|erase_background()| 和 \verb|paint()|,以实现绘制控件的两个阶段。 109 | 110 | \item 111 | \verb|erase_background()| 是一个受保护的虚方法,派生类需要在自己的实现中调用。 112 | 113 | \item 114 | paint() 是一个私有的纯虚方法。派生类必须进行实现,但不希望调用基类的实现。 115 | 116 | \item 117 | control 类的析构函数是公共且虚的,预计删除实例会是多态地。 118 | \end{itemize} 119 | 120 | 以下展示了使用这些类的例子。这些类型实例由指向基类的智能指针管理: 121 | 122 | \begin{cpp} 123 | std::vector> controls; 124 | controls.emplace_back(std::make_unique