├── .obsidian ├── hotkeys.json ├── appearance.json ├── app.json ├── core-plugins.json ├── graph.json ├── core-plugins-migration.json ├── workspace ├── workspace.json └── cache ├── .DS_Store ├── images └── 13_01.png ├── Part II 深入模板.md ├── README.md ├── obsidian.css ├── 第17章 通往未来.md ├── 第14章 实例化.md ├── 第13章 模板中的名称.md ├── 第16章 特化与重载.md └── 第12章 深入模板基础.md /.obsidian/hotkeys.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r00tk1ts/cpp-templates-2nd/HEAD/.DS_Store -------------------------------------------------------------------------------- /.obsidian/appearance.json: -------------------------------------------------------------------------------- 1 | { 2 | "theme": "moonstone", 3 | "accentColor": "" 4 | } -------------------------------------------------------------------------------- /images/13_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r00tk1ts/cpp-templates-2nd/HEAD/images/13_01.png -------------------------------------------------------------------------------- /Part II 深入模板.md: -------------------------------------------------------------------------------- 1 | # 深入模板 2 | 本书的第一部分提供了基于C++模板的大部分语法概念的解读,这足以对日常C++编程所能遇到的问题进行解答。本书的第二部分提供了一个参考,旨在回答在推动语言发展过程中实现某些高级软件效果时遇到的更不寻常的问题。如果需要,您可以在初读时跳过此部分,并根据后面各章中的引用提示或在索引中查找某个概念时返回特定主题。 3 | 我们的目标是在清楚和完整的前提下,保持讨论的简洁性。为此,示例都很简短,通常也有些虚假。这也确保了我们不会错开手头的话题而陷入到无关的议题中。 4 | 此外,我们还前瞻了C++模板语言特性未来的变化与扩展。 5 | 这一部分的议题包含以下内容: 6 | - 基础模板声明议题 7 | - 模板中名称的意义 8 | - C++模板实例化机制 9 | - 模板参数推导规则 10 | - 特化与重载 11 | - 未来的可能性 -------------------------------------------------------------------------------- /.obsidian/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "pluginEnabledStatus": { 3 | "file-explorer": true, 4 | "global-search": true, 5 | "switcher": true, 6 | "graph": true, 7 | "backlink": true, 8 | "command-palette": true, 9 | "markdown-importer": true, 10 | "word-count": true, 11 | "custom-css": true 12 | }, 13 | "useMarkdownLinks": true, 14 | "attachmentFolderPath": "./images" 15 | } -------------------------------------------------------------------------------- /.obsidian/core-plugins.json: -------------------------------------------------------------------------------- 1 | [ 2 | "file-explorer", 3 | "global-search", 4 | "switcher", 5 | "graph", 6 | "backlink", 7 | "canvas", 8 | "outgoing-link", 9 | "tag-pane", 10 | "page-preview", 11 | "daily-notes", 12 | "templates", 13 | "note-composer", 14 | "command-palette", 15 | "editor-status", 16 | "bookmarks", 17 | "outline", 18 | "word-count", 19 | "file-recovery" 20 | ] -------------------------------------------------------------------------------- /.obsidian/graph.json: -------------------------------------------------------------------------------- 1 | { 2 | "collapse-filter": true, 3 | "search": "", 4 | "showTags": false, 5 | "showAttachments": false, 6 | "hideUnresolved": false, 7 | "showOrphans": true, 8 | "collapse-color-groups": true, 9 | "colorGroups": [], 10 | "collapse-display": true, 11 | "showArrow": false, 12 | "textFadeMultiplier": 0, 13 | "nodeSizeMultiplier": 1, 14 | "lineSizeMultiplier": 1, 15 | "collapse-forces": true, 16 | "centerStrength": 0.518713248970312, 17 | "repelStrength": 10, 18 | "linkStrength": 1, 19 | "linkDistance": 250, 20 | "scale": 1, 21 | "close": false 22 | } -------------------------------------------------------------------------------- /.obsidian/core-plugins-migration.json: -------------------------------------------------------------------------------- 1 | { 2 | "file-explorer": true, 3 | "global-search": true, 4 | "switcher": true, 5 | "graph": true, 6 | "backlink": true, 7 | "canvas": true, 8 | "outgoing-link": true, 9 | "tag-pane": true, 10 | "page-preview": true, 11 | "daily-notes": true, 12 | "templates": true, 13 | "note-composer": true, 14 | "command-palette": true, 15 | "slash-command": false, 16 | "editor-status": true, 17 | "bookmarks": true, 18 | "markdown-importer": false, 19 | "zk-prefixer": false, 20 | "random-note": false, 21 | "outline": true, 22 | "word-count": true, 23 | "slides": false, 24 | "audio-recorder": false, 25 | "workspaces": false, 26 | "file-recovery": true, 27 | "publish": false, 28 | "sync": false 29 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cpp-templates-2nd 2 | Translation of C++ Templates 2nd | Powered by Obsidian 3 | 4 | 一直以来都想把这本书完整的学习一遍,但是以前功力不够好多东西理解不上去,再看过了一些其他的C++模板著作以后(C++模板元编程、C++设计新思维泛型编程与设计模式之应用),略有所悟。 5 | 这本书2018年出版以来市面上没有译本,我在github上找到了Walton1128的部分翻译,其中Part I的1-11章已阅毕,而Part III部分还只翻译了部分章节,Part II被暂时跳过。我在学习的过程中也找到了一些翻译的错误或是争议,均已在repo的issue中提出(open以及closed状态),相应repo见https://github.com/Walton1128/CPP-Templates-2nd-- 6 | 7 | 鉴于这本书篇幅巨鸿,越是后面越是艰深难懂,所以翻译的过程势必呕心沥血,非常感谢Walton1128的无私奉献,我在享受成果的同时也希望自己能尽一份力,遂决定翻译该著作的Part II。我使用Obsidian管理译本,方便整理,也推荐给大家使用这一软件。 8 | 9 | 此外,本repo的所有内容均对外免费授权,希望更多的技术人能够薪火相传,共建理想乡。 10 | 11 | --- 12 | 13 | update in 2023/08/07: 14 | 15 | 时隔几年对C++模板的理解有所精进,重新又看了一遍《C++ Templates 2nd》,顺便把12~16章中此前翻译不够准确的地方做了修正,同时也补充了第17章的翻译。到此,整个Part 16 | 2部分译毕。只可惜某才疏学浅,毕竟是个工地英语六级的门外汉,许多地方的翻译依然狗屁不通,好在我借助了GPT的力量帮忙捋顺了不少语句(厉不厉害你G哥)。重新审校的同时也不禁感慨,从机器学习到深度学习、从Bert到GPT,算来不过几年光景,何移之速也。这本书里的内容还都建立在C++17之前,虽然也涉及了部分C++20的知识,但在了解过C++20、C++23甚至未来的C++26之后再回过头来看看,不免潸然泪下,历史的沧桑感刻骨铭心。本书的第一部分和第三部分Walton1128大佬早已译毕,尽管C++还在不断地发展,但我相信《C++ Templates 2nd》以其干货满满的质量必定名垂青史,引无数后来人考古专研。 17 | -------------------------------------------------------------------------------- /.obsidian/workspace: -------------------------------------------------------------------------------- 1 | { 2 | "main": { 3 | "id": "c4c73ec3293a0571", 4 | "type": "split", 5 | "children": [ 6 | { 7 | "id": "98c33ac956e86b71", 8 | "type": "leaf", 9 | "active": true, 10 | "state": { 11 | "type": "markdown", 12 | "state": { 13 | "file": "第15章 模板实参推导.md", 14 | "mode": "source" 15 | } 16 | } 17 | } 18 | ], 19 | "direction": "vertical" 20 | }, 21 | "left": { 22 | "id": "bcd75cf8d6535be2", 23 | "type": "split", 24 | "children": [ 25 | { 26 | "id": "0bbdc1b893319c13", 27 | "type": "tabs", 28 | "children": [ 29 | { 30 | "id": "613cb7057fb9e798", 31 | "type": "leaf", 32 | "state": { 33 | "type": "file-explorer", 34 | "state": {} 35 | } 36 | }, 37 | { 38 | "id": "3f00c9ac0156819b", 39 | "type": "leaf", 40 | "state": { 41 | "type": "search", 42 | "state": { 43 | "query": "" 44 | } 45 | } 46 | } 47 | ] 48 | } 49 | ], 50 | "direction": "horizontal", 51 | "width": 255 52 | }, 53 | "right": { 54 | "id": "8fbb07cd5832465c", 55 | "type": "split", 56 | "children": [ 57 | { 58 | "id": "c58398bef387985c", 59 | "type": "tabs", 60 | "children": [ 61 | { 62 | "id": "03029763cead9cba", 63 | "type": "leaf", 64 | "state": { 65 | "type": "backlink", 66 | "state": { 67 | "file": "第15章 模板实参推导.md" 68 | } 69 | } 70 | } 71 | ] 72 | } 73 | ], 74 | "direction": "horizontal", 75 | "width": 300, 76 | "collapsed": true 77 | }, 78 | "lastOpenFiles": [ 79 | "第15章 模板实参推导.md", 80 | "第14章 实例化.md", 81 | "Untitled.md", 82 | "第13章 模板中的名称.md", 83 | "第12章 深入模板基础.md" 84 | ] 85 | } -------------------------------------------------------------------------------- /.obsidian/workspace.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": { 3 | "id": "c4c73ec3293a0571", 4 | "type": "split", 5 | "children": [ 6 | { 7 | "id": "ab4b8ebd2a707f72", 8 | "type": "tabs", 9 | "children": [ 10 | { 11 | "id": "0949957aae65044d", 12 | "type": "leaf", 13 | "state": { 14 | "type": "markdown", 15 | "state": { 16 | "file": "第16章 特化与重载.md", 17 | "mode": "source", 18 | "source": false 19 | } 20 | } 21 | }, 22 | { 23 | "id": "ac37fcbe9ee71346", 24 | "type": "leaf", 25 | "state": { 26 | "type": "markdown", 27 | "state": { 28 | "file": "第17章 通往未来.md", 29 | "mode": "source", 30 | "source": false 31 | } 32 | } 33 | } 34 | ], 35 | "currentTab": 1 36 | } 37 | ], 38 | "direction": "vertical" 39 | }, 40 | "left": { 41 | "id": "bcd75cf8d6535be2", 42 | "type": "split", 43 | "children": [ 44 | { 45 | "id": "0bbdc1b893319c13", 46 | "type": "tabs", 47 | "children": [ 48 | { 49 | "id": "613cb7057fb9e798", 50 | "type": "leaf", 51 | "state": { 52 | "type": "file-explorer", 53 | "state": { 54 | "sortOrder": "alphabetical" 55 | } 56 | } 57 | }, 58 | { 59 | "id": "3f00c9ac0156819b", 60 | "type": "leaf", 61 | "state": { 62 | "type": "search", 63 | "state": { 64 | "query": "", 65 | "matchingCase": false, 66 | "explainSearch": false, 67 | "collapseAll": false, 68 | "extraContext": false, 69 | "sortOrder": "alphabetical" 70 | } 71 | } 72 | }, 73 | { 74 | "id": "bfc35067f7886c8e", 75 | "type": "leaf", 76 | "state": { 77 | "type": "bookmarks", 78 | "state": {} 79 | } 80 | } 81 | ] 82 | } 83 | ], 84 | "direction": "horizontal", 85 | "width": 255 86 | }, 87 | "right": { 88 | "id": "8fbb07cd5832465c", 89 | "type": "split", 90 | "children": [ 91 | { 92 | "id": "c58398bef387985c", 93 | "type": "tabs", 94 | "children": [ 95 | { 96 | "id": "03029763cead9cba", 97 | "type": "leaf", 98 | "state": { 99 | "type": "backlink", 100 | "state": { 101 | "file": "第17章 通往未来.md", 102 | "collapseAll": true, 103 | "extraContext": false, 104 | "sortOrder": "alphabetical", 105 | "showSearch": false, 106 | "searchQuery": "", 107 | "backlinkCollapsed": false, 108 | "unlinkedCollapsed": true 109 | } 110 | } 111 | }, 112 | { 113 | "id": "92e2c535a9c698db", 114 | "type": "leaf", 115 | "state": { 116 | "type": "outgoing-link", 117 | "state": { 118 | "file": "第17章 通往未来.md", 119 | "linksCollapsed": false, 120 | "unlinkedCollapsed": true 121 | } 122 | } 123 | }, 124 | { 125 | "id": "9803edabc32a111a", 126 | "type": "leaf", 127 | "state": { 128 | "type": "tag", 129 | "state": { 130 | "sortOrder": "frequency", 131 | "useHierarchy": true 132 | } 133 | } 134 | }, 135 | { 136 | "id": "1ee608aea183966e", 137 | "type": "leaf", 138 | "state": { 139 | "type": "outline", 140 | "state": { 141 | "file": "第17章 通往未来.md" 142 | } 143 | } 144 | } 145 | ], 146 | "currentTab": 3 147 | } 148 | ], 149 | "direction": "horizontal", 150 | "width": 300 151 | }, 152 | "left-ribbon": { 153 | "hiddenItems": { 154 | "switcher:Open quick switcher": false, 155 | "graph:Open graph view": false, 156 | "canvas:Create new canvas": false, 157 | "daily-notes:Open today's daily note": false, 158 | "templates:Insert template": false, 159 | "command-palette:Open command palette": false 160 | } 161 | }, 162 | "active": "ac37fcbe9ee71346", 163 | "lastOpenFiles": [ 164 | "第16章 特化与重载.md", 165 | "第17章 通往未来.md", 166 | "第12章 深入模板基础.md", 167 | "第13章 模板中的名称.md", 168 | "第14章 实例化.md", 169 | "第15章 模板实参推导.md", 170 | "images/13_01.png 1.png", 171 | "images/13_01.png.md", 172 | "images/13_01.png", 173 | "images/Pasted.md", 174 | "images", 175 | "README.md", 176 | "Untitled.canvas", 177 | "Untitled 1.canvas", 178 | "Part II 深入模板.md" 179 | ] 180 | } -------------------------------------------------------------------------------- /obsidian.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | .workspace-split.mod-root .view-content .markdown-preview-view { 4 | background-color: #fff; 5 | } 6 | 7 | code[class*="language-"], 8 | pre[class*="language-"], 9 | .token.punctuation, 10 | .token.operator { 11 | color: #24292e !important; 12 | } 13 | 14 | 15 | .markdown-preview-view { 16 | -ms-text-size-adjust: 100%; 17 | -webkit-text-size-adjust: 100%; 18 | line-height: 1.5; 19 | color: #24292e; 20 | font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji; 21 | font-size: 16px; 22 | line-height: 1.5; 23 | word-wrap: break-word; 24 | } 25 | 26 | .markdown-preview-view a { 27 | background-color: initial; 28 | text-decoration: none !important; 29 | } 30 | 31 | .markdown-preview-view a:active, 32 | .markdown-preview-view a:hover { 33 | outline-width: 0; 34 | } 35 | 36 | .markdown-preview-view strong { 37 | font-weight: inherit; 38 | font-weight: bolder; 39 | } 40 | 41 | .markdown-preview-view h1 { 42 | font-size: 2em; 43 | margin: .67em 0; 44 | } 45 | 46 | .markdown-preview-view img { 47 | border-style: none; 48 | } 49 | 50 | .markdown-preview-view code, 51 | .markdown-preview-view pre { 52 | font-family: monospace,monospace; 53 | font-size: 1em; 54 | color: #24292e; 55 | } 56 | 57 | .markdown-preview-view p code { 58 | display: inline-block; 59 | padding: 3px 5px; 60 | font: 11px SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; 61 | line-height: 10px; 62 | color: #444d56; 63 | vertical-align: middle; 64 | background-color: #fafbfc; 65 | border: 1px solid #d1d5da; 66 | border-radius: 3px; 67 | box-shadow: inset 0 -1px 0 #d1d5da; 68 | } 69 | 70 | .markdown-preview-view hr { 71 | box-sizing: initial; 72 | height: 0; 73 | overflow: visible; 74 | } 75 | /* 76 | .markdown-preview-view input { 77 | font: inherit; 78 | margin: 0; 79 | } 80 | 81 | .markdown-preview-view input { 82 | overflow: visible; 83 | } 84 | 85 | .markdown-preview-view [type=checkbox] { 86 | box-sizing: border-box; 87 | padding: 0; 88 | } */ 89 | 90 | .markdown-preview-view * { 91 | box-sizing: border-box; 92 | } 93 | 94 | .markdown-preview-view input { 95 | font-family: inherit; 96 | font-size: inherit; 97 | line-height: inherit; 98 | } 99 | 100 | .markdown-preview-view a { 101 | color: #0366d6; 102 | text-decoration: none; 103 | } 104 | 105 | .markdown-preview-view a:hover { 106 | text-decoration: underline; 107 | } 108 | 109 | .markdown-preview-view strong { 110 | font-weight: 600; 111 | } 112 | 113 | .markdown-preview-view hr { 114 | height: 0; 115 | margin: 15px 0; 116 | overflow: hidden; 117 | background: transparent; 118 | border: 0; 119 | border-bottom: 1px solid #dfe2e5; 120 | } 121 | 122 | .markdown-preview-view hr:after, 123 | .markdown-preview-view hr:before { 124 | display: table; 125 | content: ""; 126 | } 127 | 128 | .markdown-preview-view hr:after { 129 | clear: both; 130 | } 131 | 132 | .markdown-preview-view table { 133 | border-spacing: 0; 134 | border-collapse: collapse; 135 | } 136 | 137 | .markdown-preview-view td, 138 | .markdown-preview-view th { 139 | padding: 0; 140 | } 141 | 142 | .markdown-preview-view details summary { 143 | cursor: pointer; 144 | } 145 | 146 | 147 | .markdown-preview-view h1, 148 | .markdown-preview-view h2, 149 | .markdown-preview-view h3, 150 | .markdown-preview-view h4, 151 | .markdown-preview-view h5, 152 | .markdown-preview-view h6 { 153 | margin-top: 0; 154 | margin-bottom: 0; 155 | color: #24292e; 156 | } 157 | 158 | .markdown-preview-view h1 { 159 | font-size: 32px; 160 | } 161 | 162 | .markdown-preview-view h1, 163 | .markdown-preview-view h2 { 164 | font-weight: 600; 165 | } 166 | 167 | .markdown-preview-view h2 { 168 | font-size: 24px; 169 | } 170 | 171 | .markdown-preview-view h3 { 172 | font-size: 20px; 173 | } 174 | 175 | .markdown-preview-view h3, 176 | .markdown-preview-view h4 { 177 | font-weight: 600; 178 | } 179 | 180 | .markdown-preview-view h4 { 181 | font-size: 16px; 182 | } 183 | 184 | .markdown-preview-view h5 { 185 | font-size: 14px; 186 | } 187 | 188 | .markdown-preview-view h5, 189 | .markdown-preview-view h6 { 190 | font-weight: 600; 191 | } 192 | 193 | .markdown-preview-view h6 { 194 | font-size: 12px; 195 | } 196 | 197 | .markdown-preview-view p { 198 | margin-top: 0; 199 | margin-bottom: 10px; 200 | } 201 | 202 | .markdown-preview-view blockquote { 203 | margin-left: 0; 204 | margin-right: 0; 205 | } 206 | 207 | .markdown-preview-view ol, 208 | .markdown-preview-view ul { 209 | padding-left: 0; 210 | margin-top: 0; 211 | margin-bottom: 0; 212 | } 213 | 214 | .markdown-preview-view ol ol, 215 | .markdown-preview-view ul ol { 216 | list-style-type: lower-roman; 217 | } 218 | 219 | .markdown-preview-view ol ol ol, 220 | .markdown-preview-view ol ul ol, 221 | .markdown-preview-view ul ol ol, 222 | .markdown-preview-view ul ul ol { 223 | list-style-type: lower-alpha; 224 | } 225 | 226 | .markdown-preview-view dd { 227 | margin-left: 0; 228 | } 229 | 230 | .markdown-preview-view code, 231 | .markdown-preview-view pre { 232 | font-family: SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; 233 | font-size: 12px; 234 | } 235 | 236 | .markdown-preview-view pre { 237 | margin-top: 0; 238 | margin-bottom: 0; 239 | } 240 | 241 | .markdown-preview-view input::-webkit-inner-spin-button, 242 | .markdown-preview-view input::-webkit-outer-spin-button { 243 | margin: 0; 244 | -webkit-appearance: none; 245 | appearance: none; 246 | } 247 | 248 | .markdown-preview-view :checked+.radio-label { 249 | position: relative; 250 | z-index: 1; 251 | border-color: #0366d6; 252 | } 253 | 254 | .markdown-preview-view .border { 255 | border: 1px solid #e1e4e8!important; 256 | } 257 | 258 | .markdown-preview-view .border-0 { 259 | border: 0!important; 260 | } 261 | 262 | .markdown-preview-view .border-bottom { 263 | border-bottom: 1px solid #e1e4e8!important; 264 | } 265 | 266 | .markdown-preview-view .rounded-1 { 267 | border-radius: 3px!important; 268 | } 269 | 270 | .markdown-preview-view .bg-white { 271 | background-color: #fff!important; 272 | } 273 | 274 | .markdown-preview-view .bg-gray-light { 275 | background-color: #fafbfc!important; 276 | } 277 | 278 | .markdown-preview-view .text-gray-light { 279 | color: #6a737d!important; 280 | } 281 | 282 | .markdown-preview-view hr { 283 | border-bottom-color: #eee; 284 | } 285 | 286 | .markdown-preview-view kbd { 287 | display: inline-block; 288 | padding: 3px 5px; 289 | font: 11px SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; 290 | line-height: 10px; 291 | color: #444d56; 292 | vertical-align: middle; 293 | background-color: #fafbfc; 294 | border: 1px solid #d1d5da; 295 | border-radius: 3px; 296 | box-shadow: inset 0 -1px 0 #d1d5da; 297 | } 298 | 299 | .markdown-preview-view:after, 300 | .markdown-preview-view:before { 301 | display: table; 302 | content: ""; 303 | } 304 | 305 | .markdown-preview-view:after { 306 | clear: both; 307 | } 308 | 309 | .markdown-preview-view>:first-child { 310 | margin-top: 0!important; 311 | } 312 | 313 | .markdown-preview-view>:last-child { 314 | margin-bottom: 0!important; 315 | } 316 | 317 | .markdown-preview-view a:not([href]) { 318 | color: inherit; 319 | text-decoration: none; 320 | } 321 | 322 | .markdown-preview-view blockquote, 323 | .markdown-preview-view dl, 324 | .markdown-preview-view ol, 325 | .markdown-preview-view p, 326 | .markdown-preview-view pre, 327 | .markdown-preview-view table, 328 | .markdown-preview-view ul { 329 | margin-top: 0; 330 | margin-bottom: 16px; 331 | } 332 | 333 | .markdown-preview-view hr { 334 | height: .25em; 335 | padding: 0; 336 | margin: 24px 0; 337 | background-color: #e1e4e8; 338 | border: 0; 339 | } 340 | 341 | .markdown-preview-view blockquote { 342 | padding: 0 1em; 343 | color: #6a737d; 344 | border: none; 345 | border-radius: 0px; 346 | border-left: .25em solid #dfe2e5 !important; 347 | } 348 | 349 | .markdown-preview-view blockquote p, 350 | .markdown-preview-view blockquote ul { 351 | margin-bottom: 16px !important; 352 | } 353 | 354 | .markdown-preview-view blockquote>:first-child { 355 | margin-top: 0; 356 | } 357 | 358 | .markdown-preview-view blockquote>:last-child { 359 | margin-bottom: 0; 360 | } 361 | 362 | .markdown-preview-view h1, 363 | .markdown-preview-view h2, 364 | .markdown-preview-view h3, 365 | .markdown-preview-view h4, 366 | .markdown-preview-view h5, 367 | .markdown-preview-view h6 { 368 | margin-top: 24px; 369 | margin-bottom: 16px; 370 | font-weight: 600; 371 | line-height: 1.25; 372 | } 373 | 374 | .markdown-preview-view h1 { 375 | font-size: 2em; 376 | } 377 | 378 | .markdown-preview-view h1, 379 | .markdown-preview-view h2 { 380 | padding-bottom: .3em; 381 | border-bottom: 1px solid #eaecef; 382 | } 383 | 384 | .markdown-preview-view h2 { 385 | font-size: 1.5em; 386 | } 387 | 388 | .markdown-preview-view h3 { 389 | font-size: 1.25em; 390 | } 391 | 392 | .markdown-preview-view h4 { 393 | font-size: 1em; 394 | } 395 | 396 | .markdown-preview-view h5 { 397 | font-size: .875em; 398 | } 399 | 400 | .markdown-preview-view h6 { 401 | font-size: .85em; 402 | color: #6a737d; 403 | } 404 | 405 | .markdown-preview-view ol, 406 | .markdown-preview-view ul { 407 | padding-left: 2em; 408 | } 409 | 410 | .markdown-preview-view ol ol, 411 | .markdown-preview-view ol ul, 412 | .markdown-preview-view ul ol, 413 | .markdown-preview-view ul ul { 414 | margin-top: 0; 415 | margin-bottom: 0; 416 | } 417 | 418 | .markdown-preview-view li { 419 | word-wrap: break-all; 420 | } 421 | 422 | .markdown-preview-view li>p { 423 | margin-top: 16px; 424 | } 425 | 426 | .markdown-preview-view li+li { 427 | margin-top: .25em; 428 | } 429 | 430 | .markdown-preview-view dl { 431 | padding: 0; 432 | } 433 | 434 | .markdown-preview-view dl dt { 435 | padding: 0; 436 | margin-top: 16px; 437 | font-size: 1em; 438 | font-style: italic; 439 | font-weight: 600; 440 | } 441 | 442 | .markdown-preview-view dl dd { 443 | padding: 0 16px; 444 | margin-bottom: 16px; 445 | } 446 | 447 | .markdown-preview-view table { 448 | display: block; 449 | width: 100%; 450 | overflow: auto; 451 | } 452 | 453 | .markdown-preview-view table th { 454 | font-weight: 600; 455 | } 456 | 457 | .markdown-preview-view table td, 458 | .markdown-preview-view table th { 459 | padding: 6px 13px; 460 | border: 1px solid #dfe2e5; 461 | } 462 | 463 | .markdown-preview-view table tr { 464 | background-color: #fff; 465 | border-top: 1px solid #c6cbd1; 466 | } 467 | 468 | .markdown-preview-view table tr:nth-child(2n) { 469 | background-color: #f6f8fa; 470 | } 471 | 472 | .markdown-preview-view img { 473 | max-width: 100%; 474 | box-sizing: initial; 475 | background-color: #fff; 476 | } 477 | 478 | .markdown-preview-view code { 479 | padding: .2em .4em; 480 | margin: 0; 481 | font-size: 85%; 482 | background-color: rgba(27,31,35,.05); 483 | border-radius: 3px; 484 | } 485 | 486 | .markdown-preview-view pre { 487 | word-wrap: normal; 488 | } 489 | 490 | .markdown-preview-view pre>code { 491 | padding: 0; 492 | margin: 0; 493 | font-size: 100%; 494 | word-break: normal; 495 | white-space: pre; 496 | background: transparent; 497 | border: 0; 498 | } 499 | 500 | .markdown-preview-view mark { 501 | color: #24292e !important; 502 | background-color: yellow !important; 503 | } 504 | 505 | .markdown-preview-view pre, 506 | .markdown-previev-view :not(pre) > code[class*="language-"], 507 | .markdown-preview-view pre[class*="language-"] { 508 | color: #24292e; 509 | padding: 16px; 510 | overflow: auto; 511 | font-size: 85%; 512 | line-height: 1.45; 513 | background-color: #f6f8fa; 514 | border-radius: 3px; 515 | } 516 | 517 | .markdown-preview-view pre code { 518 | display: inline; 519 | max-width: auto; 520 | padding: 0; 521 | margin: 0; 522 | overflow: visible; 523 | line-height: inherit; 524 | word-wrap: normal; 525 | background-color: initial; 526 | border: 0; 527 | } 528 | 529 | 530 | 531 | .markdown-preview-view .task-list-item { 532 | list-style-type: none; 533 | } 534 | 535 | .markdown-preview-view .task-list-item+.task-list-item { 536 | margin-top: 3px; 537 | } 538 | 539 | .markdown-preview-view .task-list-item input { 540 | margin: .08em .43em .25em .15em; 541 | vertical-align: top; 542 | } 543 | 544 | .markdown-preview-view ul > li.task-list-item { 545 | text-indent: -1.55em; 546 | } 547 | 548 | 549 | .markdown-preview-view a.footnote-link { 550 | font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif,Segoe UI Emoji; 551 | } 552 | 553 | .markdown-preview-view a.tag { 554 | display: inline-block; 555 | font-size: 12px; 556 | font-weight: 500; 557 | line-height: 18px; 558 | border: 1px solid transparent; 559 | border-radius: 2em; 560 | padding: 0 10px; 561 | line-height: 22px; 562 | color: #0366d6; 563 | background-color: #f1f8ff; 564 | } -------------------------------------------------------------------------------- /第17章 通往未来.md: -------------------------------------------------------------------------------- 1 | # 第17章 通往未来 2 | 3 | C++模板几乎一直在不断发展,从1988年的初始设计,到1998年、2011年、2014年和2017年的各种标准化里程碑。可以说,在原初的98标准之后,模板至少与大部分语言新增的主要功能有关联。 4 | 5 | 本书的第一版罗列了一些我们在首个标准之后可能会看到的扩展能力,这其中的一部分已经得以实现: 6 | - 尖括号hack:C++11移除了需要在两个连续的尖括号之间插入一个空格的必要性 7 | - 默认函数模板实参:C++11开始,函数模板参数可以具有默认实参 8 | - Typedef模板:C++11引入了别名模板,具有类似的功能 9 | - `typeof`操作符:C++11引入了`decltype`操作符,扮演了相同的角色(但是使用了一个不同的token来避免与已存在的扩展冲突,尽管该扩展并不满足C++开发者社区的需求)。 10 | - 静态属性:第一版预测了编译器将直接支持某些type traits。事实上当前确实如此,尽管接口是使用标准库(然后使用若干traits的编译器扩展实现)来表达的。 11 | - 个性实例化诊断:新的关键字`static_assert`实现了本书第一版所描述的`std::instantiation_error`的想法。 12 | - 参数列表:在C++11中变成了参数包。 13 | - 布局控制:C++11的`alignof`和`alignas`满足了本书第一版的需求。此外,C++17还新增了一个`std::variant`模板来支持union。 14 | - 初始化器推导:C++17支持了类模板实参推导,算是同样的议题。 15 | - 函数表达式:C++11的lambda表达式完整提供了这一功能(相比第一版的讨论使用了不一样的语法)。 16 | 17 | 第一版中其他方向的假设暂未收录到当前的语言规范,但其中的大部分目前仍在火热的讨论中,这里我们也对它们予以保留。与此同时,一些其他想法也在萌生,在此我们也会对其中的一部分想法进行表述。 18 | 19 | ## 17.1 宽容的`typename`法则 20 | 21 | 在本书的第一版中,在这一章节曾说过在未来可能会带来两种宽容的`typename`使用法则(228页节13.3.2):允许在以前不允许的地方使用`typename`;当编译器可以相对轻松得推理出具有依赖型限定的限定名称指代的必定是某种类型时,可以省略`typename`。前者已经实现(C++11中的类型名可以在许多地方冗余使用),但后者还没有。 22 | 23 | 然而最近,有人再次呼吁在一些常见的上下文中将`typename`做成可选的,因为这些上下文对类型说明符的期望很明确: 24 | - 在命名空间和类作用域中的函数和成员函数的返回类型与参数类型。在任何作用域中出现的函数、成员函数模板以及lambda表达式亦是如此。 25 | - 声明的变量、变量模板以及静态数据成员的类型。对变量模板来说也一样。 26 | - 在别名或别名模板的token `=` 之后的类型。 27 | - 模板类型参数的默认实参。 28 | - 跟随在`static_cast`, `const_cast`, `dynamic_cast`或是`reinterpret_cast`之后的尖括号内的类型。 29 | - 在`new`表达式中命名的类型。 30 | 31 | 虽然这个列表相对来说是比较临时的,但事实证明,这种语言的改变将允许大多数使用`typename`的实例被省略,这将使代码更加紧凑和易读。 32 | 33 | ## 17.2 泛化的非类型模板参数 34 | 35 | 在非类型模板实参的限制中,最可能令模板初学者和老司机惊讶的是:没办法提供一个字符串字面值来作为模板实参。下面的例子看上去足够符合直觉: 36 | ```cpp 37 | template 38 | class Diagnoser { 39 | public: 40 | void print(); 41 | }; 42 | 43 | int main() { 44 | Diagnoser<"Suprise!">().print(); 45 | } 46 | ``` 47 | 48 | 然而,这里有些潜在的隐患。在标准C++中,当且仅当`Diagnoser`的两个实例拥有相同的实参时,它们俩的类型才是一致的。在该示例中,实参是一个指针值(换句话说,是个地址)。然而,在不同位置的两个字面上相同的字符串字面值却并不一定有相同的地址。这个时候我们就会发现`Diagnoser<"X">`和`Diagnoser<"X">`实际上是两种截然不同的类型且彼此并不兼容!(注意,`"X"`的类型是`char const[2]`,但是当它作为模板实参传递时,退化成了`char const *`。) 49 | 50 | 基于这些考虑,C++标准禁止将字符串字面值作为模板的实参。然而,一些(厂商编译器)实现提供这一功能作为扩展。它们通过在模板实例的内部表示中使用实际的字符串字面值内容来实现这一点。尽管这显然是可行的,但一些C++语言评论员认为,一个可以由字符串字面值替换的非类型模板参数应该与可以由地址替换的参数声明方式不同。一个可能的方法是将字符串字面值捕捉在一个字符参数包中。举个例子: 51 | ```cpp 52 | template 53 | class Diagnoser { 54 | public: 55 | void print(); 56 | }; 57 | 58 | int main() { 59 | // instantiates Diagnoser<’S’,’u’,’r’,’p’,’r’,’i’,’s’,’e’,’!’> 60 | Diagnoser<"Surprise!">().print(); 61 | } 62 | ``` 63 | 64 | 我们还应该注意到这个问题的一个额外的技术细节。考虑以下模板声明,并假设语言已经扩展以接受字符串字面值作为模板参数的情况: 65 | ```cpp 66 | template 67 | class Bracket { 68 | public: 69 | static char const* address(); 70 | static char const* bytes(); 71 | }; 72 | 73 | template 74 | char const* Bracket::address() 75 | { 76 | return str; 77 | } 78 | 79 | template 80 | char const* Bracket::bytes() 81 | { 82 | return str; 83 | } 84 | ``` 85 | 86 | 在上述代码中,两个成员函数除了名字以外,其他都完全相同(这种情况不太寻常)。假设有一种实现采用了类似宏展开的方式对`Bracket<"X">`进行实例化:此时,如果两个成员函数被实例化到不同的编译单元,它们就会返回不同的值。有意思的是,在一些支持该扩展功能的C++编译器上进行测试后,发现它们有着这样的问题。 87 | 88 | 还有一个相关的议题,就是模板实参对浮点数字面值的支持(以及简单的常量浮点数表达式)。举个例子: 89 | ```cpp 90 | template 91 | class Converter { 92 | public: 93 | static double convert (double val) { 94 | return val*Ratio; 95 | } 96 | }; 97 | 98 | using InchToMeter = Converter<0.0254>; 99 | ``` 100 | 101 | 这个特性在某些C++实现中也予以了支持,同时也没什么技术上的挑战(与字符串字面值不同)。 102 | 103 | C++11引入了字面值类类型的概念:一种可以在编译时接受常量值的类类型(包括通过`constexpr`函数进行的非平凡计算)。一旦这种类类型可用,马上就可以期待将它们用作非类型模板参数。然而,与上述描述的字符串字面值参数类似的问题出现了。特别地,两个类类型值的“相等性”并不是一个简单的问题,因为它通常是由操作符`==`的定义来确定的。这种相等性决定了两个实例是否相等,但实际上,链接器必须通过比较修饰后的名称来检查这种相等性。一个解决办法可能是在特定的字面值类中添加一个选项,标记它们具有平凡的相等性条件,即对类的标量成员进行两两比较。只有具有这种平凡相等性条件的类类型才被允许作为非类型模板参数类型。 104 | 105 | ## 17.3 函数模板的偏特化 106 | 107 | 在第16章中,我们讨论了类模板是如何做偏特化的,而函数模板仅支持简单的重载能力。这两种机制有些差异。 108 | 109 | 偏特化并没有引入一种新的模板:它是在既有模板(主模板)的基础上进行扩展。在查找类模板时,一开始只会考虑主模板。而在选择了主模板之后,如果发现有能够匹配模板实例的偏特化时,它的定义(也就是身体)就会被实例化出来以替代主模板的定义。(对完整特化来说也一样。) 110 | 111 | 相比之下,重载的函数模板是彼此完全独立的独立模板。在选择要实例化哪一个模板时,所有的重载模板都会被同时考虑,然后重载决议会尝试选出最适合的那一个。乍一看可能会觉着这种机制完全可以作为替代品,但在实践中还是有着诸多限制: 112 | - 特化类的成员模板而不去修改类的定义是可行的。然而,增加重载的成员需要对类的定义进行修改。在多数情况下,我们由于没有这一权限而无法这样操作。此外,C++标准当前也不允许我们向`std`命名空间新增模板,但是它允许我们做模板的特化。 113 | - 对重载函数模板来说,它们的参数必须有所区别。考虑这样一个函数模板`R convert(T const &)`,其中`R`和`T`是模板参数。我们非常想用`R = void`来特化这一模板,但使用重载是办不到的。 114 | - 对于有效的未重载的函数,当函数一旦被重载后,可能会变得永久失效。特别是,对给定的两个函数模板`f(T)`和`g(T)`(其中T是模板参数),表达式`g(&f)`当且仅当`f`没有被重载时才有效(否则就无法决定`f`指代哪一个函数)。 115 | - 友元声明指代一个特定的函数模板或是特定的函数模板的实例化。函数模板的重载版本可能不会自动地授权原始模板的使用权限。 116 | 117 | 上述总总共同组织成了一个对支持函数模板偏特化这一能力的有力论点。 118 | 119 | 函数模板偏特化的自然语法可以从类模板中提炼: 120 | ```cpp 121 | template 122 | T const& max(T const&, T const&); // primary template 123 | 124 | template 125 | T* const& max (T* const&, T *const&); // partial specialization 126 | ``` 127 | 128 | 一些语言设计者担心函数模板重载与这种偏特化实现之间的互动,举个例子: 129 | ```cpp 130 | template 131 | void add(T& x, int i); // a primary template 132 | 133 | template 134 | void add(T1 a, T2 b); // another(overloaded) primary template 135 | 136 | template 137 | void add (T*&, int); // Which primary template does this specialize? 138 | ``` 139 | 140 | 然而,我们预计此类情况将被视为错误,不会对该功能的实用性产生重大影响。 141 | 142 | 在C++11标准化期间曾简要讨论了这一扩展,但相对而言大家意兴阑珊。尽管如此,这个话题偶尔还会出现,因为它巧妙地解决了一些常见的编程问题。也许它将在未来的C++标准中再次被采用。 143 | 144 | ## 17.4 命名的模板实参 145 | 146 | 512页章节21.4描述了一种技术,它可以让我们为特定的参数提供一个非默认模板实参,而无需指定其他的具有默认值的模板实参。尽管这是一种有趣的技术,但很明显,为了达成这样一个简单的效果它做了太多的工作。因此,提供一种语言机制来命名模板实参是一个自然而然的想法。 147 | 148 | 我们应该注意到,在C++标准化过程中,Roland Hartinger曾提议了(详见【StroustrupDnE】之节6.5.1)一个相似的扩展(有时也被称作关键字实参(keyword arguments))。虽然技术上是合理的,但还是由于种种原因,该提议最终没有被纳入语言标准。在这一点上,没什么理由去相信命名的模板实参会被纳入语言标准,但这个话题在委员会讨论中的确经常出现。 149 | 150 | 然而,为了完整起见,这里我们提及一个已经讨论过的句法想法: 151 | ```cpp 152 | template, 154 | typename Copy = defaultCopy, 155 | typename Swap = defaultSwap, 156 | typename Init = defaultInit, 157 | typename Kill = defaultKill> 158 | class Mutator { 159 | ... 160 | }; 161 | 162 | void test(MatrixList ml) 163 | { 164 | mySort(ml, Mutator); 165 | } 166 | ``` 167 | 168 | 在这里,实参名称的`.`用来表示我们是按名称来引用模板实参。该语法与C99标准所引入的“指定的初始化器”语法相似: 169 | ```c 170 | struct Rectangle { int top, left, width, height; }; 171 | struct Rectangle r = { .width = 10, .height = 10, .top = 0, .left = 0 }; 172 | ``` 173 | 174 | 当然,引入命名模板实参意味着模板的模板参数的名称现在是该模板公共接口的一部分,不能自由更改。可以通过一个更显式的选择语法来解决这一问题,如下所示: 175 | ```cpp 176 | template, 178 | Copy: typename C = defaultCopy, 179 | Swap: typename S = defaultSwap, 180 | Init: typename I = defaultInit, 181 | Kill: typename K = defaultKill> 182 | class Mutator { 183 | ... 184 | }; 185 | 186 | void test(MatrixList ml) 187 | { 188 | mySort(ml, Mutator); 189 | } 190 | 191 | ``` 192 | 193 | ## 17.5 重载的类模板 194 | 完全可以想象:类模板基于模板参数也可以进行重载。比如,我们可以创建一个`Array`模板家族,它们同时包括动态和静态尺寸的数组: 195 | ```cpp 196 | template 197 | class Array { 198 | // dynamically sized array 199 | ... 200 | }; 201 | 202 | template 203 | class Array { 204 | // fixed size array 205 | ... 206 | }; 207 | ``` 208 | 209 | 重载无需受限于模板参数的数量变化,参数类型有所变化也行得通: 210 | ```cpp 211 | template 212 | class Pair { 213 | // pair of fields 214 | ... 215 | }; 216 | 217 | template 218 | class Pair { 219 | // pair of constant integer values 220 | ... 221 | }; 222 | ``` 223 | 224 | 尽管这一想法曾被一些语言设计者在非官方场合讨论过,但截止到目前,它还没有被正式地呈现给C++标准委员会。 225 | 226 | ## 17.6 非最终包展开的推导 227 | 228 | 包展开的模板实参推导当且仅当包展开位于实参列表的最后才可行。这就意味着,从一个列表中榨取出首个元素可以相当简单: 229 | ```cpp 230 | template 231 | struct Front; 232 | 233 | template 234 | struct Front { 235 | using Type = FrontT; 236 | }; 237 | ``` 238 | 239 | 正如在347页节16.4中所描述的偏特化中的位置限制,我们没办法简单地榨取出列表的最后一个元素: 240 | ```cpp 241 | template 242 | struct Back; 243 | 244 | template 245 | struct Back { // ERROR: pack expansion not at the end of 246 | using Type = BackT; // template argument list 247 | }; 248 | ``` 249 | 250 | 可变函数模板的模板实参推导也有类似的限制。放宽模板实参推导和偏特化的规则,让包展开可以在模板实参列表中的任意位置出现,从而使得这种操作变得更简单,这一方法貌似可行。此外,虽然可能性较小,但推导也可以允许在同一参数列表中出现多个包展开: 251 | ```cpp 252 | template class Tuple { 253 | }; 254 | 255 | template 256 | struct Split; 257 | 258 | template 259 | struct Split { 260 | using before = Tuple; 261 | using after = Tuple; 262 | }; 263 | ``` 264 | 265 | 对多个包展开的支持引入了额外的复杂度。比方说,`Split`是在见到`T`出现的第一次、最后一次还是其中的某一次时进行分割呢?推导过程达到怎样的复杂度时才允许编译器放弃呢? 266 | 267 | ## 17.7 `void`的正则化 268 | 269 | 在编写模板时,规则性是一种美德:如果单一的结构能够覆盖所有情况,那么我们的模板就会变得更简单。我们的程序中有一个不太规则的方面:类型。例如,请看下例: 270 | ```cpp 271 | auto&& r = f(); // error if f() returns void 272 | ``` 273 | 274 | 这行代码仅在`f()`返回一个`void`类型以外的类型时才能正常工作。当我们使用`decltype(auto)`时也会遇到同样的问题: 275 | ```cpp 276 | decltype(auto) r = f(); // error if f() returns void 277 | ``` 278 | 279 | `void`并非唯一的不规则类型:函数类型和引用类型也经常在一些情景中表现得有所例外。然而,然而,事实证明,`void`往往使我们的模板复杂化,它也没有深刻的理由变得不同寻常。比如,在162页节11.1.3中就有一个例子,它展示了`void`类型如何让完美的`std::invoke()` wrapper的实现变得复杂化。 280 | 281 | 我们可以宣布`void`是一种具有唯一值的正常值类型(如`std::nullptr_t`之于`nullptr`)。出于向后兼容性的目的,我们仍然必须为函数声明保留以下特殊情况: 282 | ```cpp 283 | void g(void); // same as void g(); 284 | ``` 285 | 286 | 然而,在大多数其他方法中,`void`会成为一种完全的值类型。此时我们将可以用`void`来声明变量和引用: 287 | ```cpp 288 | void v = void{}; 289 | void&& rrv = f(); 290 | ``` 291 | 292 | 最重要的是,许多模板将不再需要为`void`情景进行特化处理。 293 | 294 | ## 17.8 模板的类型检查 295 | 296 | 模板编程的大部分复杂性源于编译器无法进行局部地检查模板定义是否正确。相反地,模板的大部分检查都发生在模板实例化期间,此时模板定义上下文和模板实例化上下文交织在一起。不同上下文的混合让我们难以追责:究竟是模板定义的问题(因其错误地使用了模板实参),还是模板使用者的问题(因其提供的模板实参未满足模板的需求)?这一问题可以用一个简单的例子来解释,我们用一个常规编译器所产生的诊断信息加以注解: 297 | ```cpp 298 | template 299 | T max(T a, T b) 300 | { 301 | return b < a ? a : b; // ERROR: "no match for operator < 302 | // (operator types are 'X' and 'X')" 303 | } 304 | 305 | struct X { 306 | }; 307 | bool operator > (X, X); 308 | 309 | int main() 310 | { 311 | X a, b; 312 | X m = max(a, b); // NOTE: "in instantiation of function template specialization 313 | // 'max' requested here" 314 | } 315 | ``` 316 | 317 | 可以看到实际的错误(缺少合适的`operator <`)是在函数模板`max()`的定义中检测出来的。也有可能真正的错误在于——`max()`应该使用`operator >`取而代之?然而,编译器在引起`max`实例化的位置也给与了提示,这里或许才是真正的错误——`max()`被文档标注为需要一个`operator <`。无法回答这一问题往往会导致第143页9.4节中描述的"error novel",在这种情况下,编译器会提供完整的模板实例化历史,从实例化的初始原因一直到检测到错误的实际模板定义。然后,程序员需要确定究竟是哪个模板定义(可能就是模板的最初使用)真正存在错误。 318 | 319 | 模板类型检查背后的思想是在模板内部描述模板的要求,以便编译器在编译失败时确定是模板定义还是模板使用上出了问题。解决这一问题的一种方法是在模板自身的签名中使用`concept`来描述模板的要求: 320 | ```cpp 321 | template requires LessThanComparable 322 | T max(T a, T b) 323 | { 324 | return b < a ? a : b; 325 | } 326 | 327 | struct X { }; 328 | bool operator> (X, X); 329 | 330 | int main() 331 | { 332 | X a, b; 333 | X m = max(a, b); // ERROR: X does not meet the LessThanComparable requirement 334 | } 335 | ``` 336 | 337 | 通过对模板参数`T`的要求描述,编译器就可以确信函数模板`max()`仅对`T`使用了它所期望使用者提供的那些操作(在本例中,`LessThanComparable`是对`operator <`的需求)。此外,在使用模板时,编译器可以检查提供的模板实参是否提供了`max()`函数模板在工作时所需的所有行为。通过解耦这一类型检查问题,对编译器来说就可以提供出更精准的问题诊断信息。 338 | 339 | 在上例中,`LessThanComparable`被称作为一个`concept`:它表示编译器能够检查的某种类型上的限制(在更广泛的场合,是对一个类型集合上的限制)。Concept系统有着各种不同的方式来指定。 340 | 341 | 在C++11标准化周期中,曾为concepts设计并实现了一个复杂的系统,它足够强大可以用来检查模板POI和模板定义。前者意味着,在上例中,我们可以提前捕捉到`main()`中的错误,并诊断出`X`不满足`LessThanComparable`的限制。而后者意味着,在处理`max()`模板时,编译器会检查是否使用了`LessThanComparable`这一`concept`所不允许的操作(如果违反了此约束,则抛出诊断信息)。该C++11提议最终被移出了语言标准,主要是因为各种实践上的考虑(比如,仍有许多次要规范议题,其解决措施威胁着已经延后的标准)。 342 | 343 | 在C++11最终发布后,委员会成员提出并开发了一项新提案(最初称作"concepts lite")。该系统并非旨在基于施加的限制来检查模板的正确性。相反地,它仅仅聚焦于POI。所以对于我们的`max()`示例,如果实现中使用了`>`操作符,并不会导致错误。然而,在`main()`中的错误依然存在,这是因为`X`并不满足`LessThanComparable`的要求。这一崭新的concepts提议得以实现,并被认定为"Concepts TS(TS代表Technical Specification)",称作"C++ extensions for Concepts"。目前,该项TS的核心要素已经被整合到了下一个标准(即C++20)的草案中。附录E涵盖了本书出版时该草案中规定的语言特性。 344 | 345 | ## 17.9 反射元编程 346 | 347 | 在编程上下文中,反射是指以程序化的方式来检查程序功能的能力(例如,回答诸如某个类型是否是一个整型数?或是某个class类型包含了哪些非静态成员变量?)。元编程这门技艺是指“编写可以编程的程序”,它通常被用来量产新的代码。反射元编程是一种自动合成代码的技艺,它能根据程序的现有特性(通常是类型)自适应地进行适配。 348 | 349 | 在本书的第三部分,我们会去探索模板是如何达成一些简单的反射制式和元编程(某种意义上,模板实例化是一种元编程制式,因为它合成了新的代码)。然而,C++17模板的能力在面对反射时有着诸多限制(比如,没有办法回答“某个class类型包含了哪些非静态成员变量”这一问题),并且元编程的选项在各种方法中也常常不太方便(尤其是语法笨重且性能拉胯)。 350 | 351 | 认识到在这一领域对新机制的需求,C++标准委员会创建了一个研究小组(SG7)来探索更加强大的反射选项。该小组的章程后来也扩展到了元编程。以下是正在考虑的选项之一的示例: 352 | ```cpp 353 | template void report(T p) { 354 | constexpr { 355 | std::meta::info infoT = reflexpr(T); 356 | for (std::meta::info : std::meta::data_members(infoT)) { 357 | -> { 358 | std::cout << (: std::meta::name(info) :) 359 | << ": " << p.(.info.) << '\n'; 360 | } 361 | } 362 | } 363 | // code will be injected here 364 | } 365 | ``` 366 | 367 | 代码里展示了相当多的新事物。首先,`constexpr{...}`结构会强制这一语句在编译期进行计算,但是如果它在一个模板中出现,就仅会在模板实例化时才进行计算。其次,`relfexpr()`操作符对隐晦类型`std::meta::info`产出了一个表达式,用于找到其背后实参的反射信息(本例中就是类型`T`)。标准库的元函数允许去查询这一元信息,`std::meta::data_members`就是那些标准元函数的其中一个,它会生成一个`std::meta::info`对象序列,它们描述了该操作数背后的非静态成员变量。因此,该for循环真正的进行了对`p`的非静态数据成员的遍历。 368 | 369 | 该系统元编程能力的核心是在各种作用域内“注入”代码的能力。结构`->{...}`注入了语句和(或)声明,触发了`constexpr`的计算。在本例中,意味着是在`constexpr{...}`结构之后。注入的代码片段可以包含某些模式,通过值计算后重新替换。在本例中,`(:...:)`会产生一个字符串字面值(`std::meta::name(info)`会产生一个类字符串的对象,它表示成员变量实体的非限定名称,在本例中由`info`表示)。同样,表达式`(.info.)`生成了一个标识符,命名由`info`表示的实体。其他生成类型的模式,像模板实参列表等也都支持。 370 | 371 | 对号入座之后,对`X`类型: 372 | ```cpp 373 | struct X 374 | { 375 | int x; 376 | std::string s; 377 | }; 378 | ``` 379 | 实例化函数模板`report()`就会生成下面的代码: 380 | ```cpp 381 | template<> void report(X const& p) { 382 | std::cout << "x" << ": " << "p.x" << '\n'; 383 | std::cout << "s" << ": " << "p.s" << '\n'; 384 | } 385 | ``` 386 | 387 | 也就是说,该函数会自动生成一个输出class类型的非静态成员变量的函数。 388 | 389 | 这些类型的功能有很多应用。可能会有类似的能力最终被语言标准所采用,但只能说未来可期。在本书撰写之时,已经有一些实现在做实验了。(就在本书发表前,SG7认同了使用`constexpr`计算和诸如`std::meta::info`值类型来处理反射这一方向。但是这里的注入机制没能被认可,它们可能最终会采用另一种系统。) 390 | 391 | ## 17.10 包设施 392 | 393 | 参数包在C++11所引入,但对它们的处理往往需要使用递归的模板技术。回顾第263页14.6节中讨论的代码大纲: 394 | ```cpp 395 | template 396 | void f(Head &&h, Remainder&&... r) 397 | { 398 | doSomething(h); 399 | if constexpr (sizeof...(r) != 0) { 400 | // handle the remainder recusively (perfectly forwarding the arguments): 401 | f(r...); 402 | } 403 | } 404 | ``` 405 | 406 | 在使用了C++17的编译期`if`语句之后(第134页节8.5),这一示例变得非常简单,但是它依然保留了在编译时可能会进行的昂贵的递归实例化技术。 407 | 408 | 几个委员会的提案尝试在某种程度上简化这种情况。一个例子是引入一种表示从包中选择特定元素的符号。具体而言,对于一个包`P`,已经有人建议使用符号`P.[N]`来表示该包中的第`N+1`个元素。同样,也有提案用于表示包的“切片”(例如,使用符号`P.[b, e]`)。 409 | 410 | 在审查这些提案时,已经清楚地看到它们与上面讨论的反射元编程的概念有些交互。目前尚不清楚是否会向语言中添加特定的包选择机制,还是将提供满足此需求的元编程工具。 411 | 412 | ## 17.11 模块 413 | 414 | 另一个即将到来的重大扩展模块,虽然与模板的关系只是间接的,但在这里提及它仍然是值得的,因为模板库是其中最大的受益者之一。 415 | 416 | 当前,库接口是通过指定头文件、用`#include`宏来引入到编译单元。这种方法有几个缺点,但最令人反感的两个缺点是(a)界面文本的含义可能会被之前包含的代码(例如,通过宏)意外修改,以及(b)每次重新处理该文本都会迅速主导构建时间。 417 | 418 | 模块是一种特性,它允许将编译为特定于编译器的格式,然后这些接口可以“导入”到翻译单元中,而不会受到宏展开或通过意外的额外声明修改代码含义的影响。而且,编译器可以只读取与客户端代码相关的编译模块文件的部分内容,从而大大加快编译过程。 419 | 420 | 这里给出模块定义的表现形式: 421 | ```cpp 422 | module MyLib; 423 | 424 | void helper() { 425 | ... 426 | } 427 | 428 | export inline void libFunc() { 429 | ... 430 | helper() 431 | ... 432 | } 433 | ``` 434 | 435 | 该模块导出了函数`libFunc()`,他可以被client代码这样使用: 436 | ```cpp 437 | import MyLib; 438 | 439 | int main() { 440 | libFunc(); 441 | } 442 | ``` 443 | 444 | `libFunc()`对client代码可见,但是`helper()`却是不可见的,尽管编译模块的文件很可能包含了有关于`helper()`的信息来支持内联。 445 | 446 | C++模块的提案正在路上,标准委员会将在C++17之后进行集成。制定此类提案的担忧之一是如何从头文件世界过渡到模块世界。已经有一些设施可以在一定程度上实现这一点(例如,在不将其内容作为模块的一部分的情况下包含头文件的能力),以及仍在讨论的其他设施(例如,从模块导出宏的能力)。 447 | 448 | 模块对模板库来说非常有用,这是因为模板大部分都完全定义在头文件中。即使包含一个像是``这样的基础头文件,也要处理上万行C++代码(即使该头文件中只有少量的声明会被引用)。其他的流行库还要再高一个数量级。避免对所有代码都进行编译从而降低成本,将是处理大型复杂代码库的C++程序员的一大兴趣。 -------------------------------------------------------------------------------- /第14章 实例化.md: -------------------------------------------------------------------------------- 1 | # 第14章 实例化 2 | 3 | 模板实例化就是从泛型模板定义中生成类型、函数和变量的过程。C++模板实例化的概念非常基础,但有时又错综复杂。这一复杂性的其中一个底层原因在于:模板生成的实体定义不再局限于源代码单一的位置。模板本身的位置、模板使用的位置以及模板实参定义的位置均在实体的含义中扮演着重要角色。 4 | 5 | 本章我们会讲解如何组织源代码来正确使用模板。此外,我们调查了最流行的C++编译器处理模板实例所使用的各种各样的方法。尽管所有的方法都应该语义等价,但理解编译器实例化策略的基本原则是大有裨益的。在构建实际软件时,每种机制都带有一些小怪癖,相反地,每种机制都影响了标准C++的最终规范。 6 | 7 | ## 14.1 On-Demand实例化 8 | 9 | 当C++编译器遇到模板特化的使用时,它会用需要的实参来替换模板参数来生成特化体。这一过程是自动完成的,不需要客户端代码来引导(或者不需要模板定义来引导)。这一”on-demand“实例化特性使得C++与其他早期的编译型语言的类似功能大相径庭(如Ada或Eiffel,其中的一些语言需要显式地实例化引导,另外一些使用运行时分发机制来避免编译期实例化过程)。有时这也被称作”隐式(implicit)实例化“或者”自动(automatic)实例化“。 10 | 11 | On-demand实例化意味着编译器常常需要访问模板完整的定义(换句话说,不只是声明)以及某些成员。考虑下面这一段精简的源码文件: 12 | 13 | ```cpp 14 | template class C; // #1 declaration only 15 | C* p = 0; // #2 fine: definition of C not needed 16 | 17 | template 18 | class C{ 19 | public: 20 | void f(); // #3 member declaration 21 | }; // #4 class template definition completed 22 | 23 | void g(C& c) // #5 use class template declaration only 24 | { 25 | c.f(); // #6 use class template definition; 26 | } // will need definition of C::f() 27 | // in this translation unit 28 | 29 | template 30 | void C::f() // required definition due to #6 31 | { 32 | } 33 | ``` 34 | 35 | 在源码的`#1`处,仅仅只有模板的声明,并没有定义(这种声明有时也被称作前置声明)。与普通类的情况一样,如果你声明的是一个指向某种类型的指针或引用(`#2`处的声明),那么在声明的作用域中,你并不需要看到该类模板的定义。例如,声明函数`g`的参数类型并不需要模板`C`的完整定义。然而,一旦某个组件需要知道模板特化体的大小或是访问了该特化体的成员,那么就需要看到完整的类模板定义。这就解释了为什么`#6`处必须看到类模板的定义。若非如此,编译器无法确认该成员是否存在、是否可访问(非private或protected)。更进一步,成员函数定义也是需要的,因为`#6`处的调用需要确认`C::f()`是否存在。 36 | 37 | 另一个需要类模板实例化的表达式如下所示,这里需要`C`实例化是因为它需要该类型的尺寸: 38 | ```cpp 39 | C* p = new C; 40 | ``` 41 | 42 | 本例中,需要实例化来保证编译器可以确定`C`的尺寸,该new表达式需要去确认要分配多少存储空间。你可能会发现,对这一模板来说,替换模板参数`T`的实参`X`的类型无论是什么,都不会影响模板的尺寸,毕竟`C`是一个空类(没有成员变量或虚函数)。然而,编译器并不会通过分析模板定义来避免实例化(所有编译器实际上都会进行实例化)。此外,对于上例来说,为了确定`C`是否有可访问的默认构造器并确保`C`没有成员`operator new`或`operator delete`操作符函数,实例化也同样是必要的。 43 | 44 | 在源代码中是否需要访问类模板的成员并不总是那么直观。例如,C++重载决议规则要求:如果候选函数的参数是类类型,那么该类类型就必须是可见的: 45 | 46 | ```cpp 47 | template 48 | class C { 49 | public: 50 | C(int); // a constructor that can be called with a single parameter 51 | }; // may be used for implicit conversions 52 | 53 | void candidate(C); // #1 54 | void candidate(int) { } // #2 55 | 56 | int main() 57 | { 58 | candidate(42); // both previous function declarations can be called 59 | } 60 | ``` 61 | 62 | 调用`candidate(42)`会采用`#2`处的声明。然而,在`#1`处的声明也会被实例化来检查对于这个调用来说它是否是可用的候选者(这个例子中,由于模板的单实参构造器可以把42隐式转换成一个类型为`C`的右值)。请注意,如果模板不经实例化也可以找到调用函数(合适的候选),编译器还是被允许(但不强制)执行该实例化(上例的情景中,由于有精准匹配的候选者,隐式转换的那个不会被选择)。另外,令我们的惊讶的是:`C`的实例化可能还会触发一个错误。 63 | 64 | ## 14.2 延迟实例化 65 | 66 | 到目前为止所展示的这些例子,和使用非模板类相比并没有本质上的区别。譬如,非模板类的许多用法会要求类类型的完整性(参考P154节10.3.1)。而对模板来说,编译器会用类模板定义来生成完整的定义。 67 | 68 | 现在有一个相关问题:模板实例化的程度如何?可以给出这样的模糊答案:会实例化到它实际需要的程度。换句话说,编译器在实例化模板时应该是“懒惰”的。让我们来细究“懒惰”在这里的真正意义。 69 | 70 | ### 14.2.1 部分实例化和完整实例化 71 | 72 | 如我们之前所见,编译器有时不需要替换类或函数模板的完整定义。例如: 73 | 74 | ```cpp 75 | template T f(T p) { return 2*p; } 76 | decltype(f(2)) x = 2; 77 | ``` 78 | 79 | 本例中,`decltype(f(2))`所指示的类型并不需要函数模板`f()`的完整实例化。编译器因此只被允许替换`f()`的声明,而不是替换整个“身体”。这有时被称为部分实例化(partial instantiation)。 80 | 81 | 同样,如果引用类模板的实例而不需要将该实例作为完整类型,则编译器不应对该类模板实例执行完整的实例化。考虑下面的例子: 82 | 83 | ```cpp 84 | template class Q { 85 | using Type = typename T::Type; 86 | }; 87 | 88 | Q* p = 0; // OK: the body of Q is not substituted 89 | ``` 90 | 91 | 在这里,`Q`完整的实例化会触发一个错误,因为在`T`是`int`类型时,`T::Type`并没有意义。但是因为本例并不需要完整的`Q`,所以不会执行完整实例化,代码也是OK的(尽管可疑)。 92 | 93 | 变量模板也有“完整”和“部分”实例化的区别。下面的例子用以阐释: 94 | ```cpp 95 | template T v = T::default_value(); 96 | decltype(v) s; // OK: initializer of v not instantiated 97 | ``` 98 | 99 | `v`的完整实例化会引起错误,但是如果只是需要变量模板实例的类型的话,是不需要进行完整实例化的。 100 | 101 | 有意思的是,别名模板没有这一区别:不存在两种方法来替换它们。 102 | 103 | 在C++中,当谈到“模板实例化”而没有说特定的完整或部分实例化时,往往意味着前者。也就是说,默认情况我们指的都是完整实例化。 104 | 105 | ### 14.2.2 实例化组件 106 | 107 | 当类模板隐式(完整)实例化时,其所有成员的声明也都会进行实例化,但是对应的定义却并不会实例化(即,成员是部分实例化的)。对此有一些特殊情况:首先,如果类模板包含一个匿名的联合体(union),该联合体的成员的定义也会实例化;另一个特殊的情况出现在虚成员函数场景中,它们的定义作为模板实例化的结果,可能会也可能不会进行实例化。实际上,许多实现都会实例化该定义,因为“实现虚函数调用机制的内部结构”需要虚函数有一个链接实体存在。 108 | 109 | 实例化模板时,默认函数调用实参被单独考虑。具体来说,除非调用该函数(或成员函数)时确实使用了默认实参,否则它们不会被实例化。反之,如果调用该函数时显式地指定了实参去覆盖这一默认实参,那么默认实参就不会被实例化。 110 | 111 | 类似的,除非有必要,异常规范和默认成员初始化器也不会被实例化。 112 | 113 | 让我们用一些例子来阐释这些原则: 114 | 115 | *details/lazy1.hpp* 116 | ```cpp 117 | template 118 | class Safe { 119 | }; 120 | 121 | template 122 | class Danger { 123 | int arr[N]; // OK here, although would fail for N<=0 124 | }; 125 | 126 | template 127 | class Tricky { 128 | public: 129 | void noBodyHere(Safe = 3); // OK until usage of default value results in an error 130 | void inclass() { 131 | Danger noBoomYet; // OK until inclass() is used with N<=0 132 | } 133 | struct Nested { 134 | Danger pfew; // OK until Nested is used with N<=0 135 | }; 136 | union { // due anonymous union: 137 | Danger anonymous; // OK until Tricky is instantiated with N<=0 138 | int aligh; 139 | }; 140 | void unsafe(T (*p)[N]); // OK until Tricky is instantiated with N<=0 141 | void error(){ 142 | Danger<-1> boom; // always ERROR (which not all compilers detect) 143 | } 144 | }; 145 | ``` 146 | 147 | 标准C ++编译器将审查这些模板定义以检查语法和常规语义约束。这样做时,当检查涉及模板参数的约束时,它将“假设最佳”。举个例子,`Danger::arr`的成员参数`N`可能是零或负数(非法的),但是编译器会假定不会出现这种情况。`inclass()`,`struct Nested`,匿名联合体的定义因而都没有问题。 148 | 149 | 出于同样的原因,只要`N`还是一个未被替换的模板参数时,成员`unsafe(T (*p)[N])`的声明也不是问题。 150 | 151 | `noBodyHere()`的默认实参规格声明(`=3`)看起来很诡异,因为模板`Safe<>`并不能以一个整型数来初始化,但是编译器会假定:对于`Safe`泛型定义来说,它实际上并不需要默认实参;或者是`Safe`的特化体会引入使用一个整型数来初始化的能力(见第16章)。然而,成员函数`error()`的定义必定会引起一个错误,即使模板尚未实例化,这是因为`Danger<-1>`的使用需要一个完整的类`Danger<-1>`的定义,也就会生成该类并尝试去定义一个负数尺寸的数组。有趣的是,虽然标准明确指出此段代码无效,但它还是允许编译器在未实际使用模板实例时不去诊断这个错误。也就是说,只要`Tricky::error()`对任何具体的`T`和`N`类型都未被使用,那么编译器就不用抛出这个错误。例如,GCC和Visual C++在撰写此书时都不会抛出这一错误。 152 | 153 | 让我们来分析一下,在增加下面的一行定义语句时,会发生什么: 154 | ```cpp 155 | Tricky inst; 156 | ``` 157 | 158 | 这将引起编译器(完整)实例化`Tricky`,在模板`Tricky<>`定义中替换`T`为`int`,`N`为`-1`。并非所有的成员定义都是必要的,但是默认构造器和析构器(本例中都是隐式声明的)一定会被调用到,因此它们的定义必须是可用的(在我们的例子中,它们都会隐式生成)。如上所述,`Tricky`的成员会部分实例化(即,它们的声明会被替换):这一过程可能会引起错误。例如,`unsafe(T (*p)[N])`的声明创建了一个负数尺寸的数组类型,这就是一个错误。类似的,`anonymous`成员现在也会抛出一个错误,因为并不能生成`Danger<-1>`类型。另一方面,成员`inclass()`和`struct Nested`的定义现在还不会被实例化,因此对完整类型`Danger<-1>`的需求并不会产生错误(它们都包含了一个无效的数组定义)。 159 | 160 | 如上所述,当实例化一个模板时,对于虚函数实际上是需要提供定义的。否则,就会遇到链接错误。例如: 161 | *details/lazy2.cpp* 162 | ```cpp 163 | template 164 | class VirtualClass { 165 | public: 166 | virtual ~VirtualClass() {} 167 | virtual T vmem(); // Likely ERROR if instantiated without definition 168 | }; 169 | 170 | int main() 171 | { 172 | VirtualClass inst; 173 | } 174 | ``` 175 | 176 | 最后,`operator->`值得留意。考虑: 177 | ```cpp 178 | template 179 | class C{ 180 | public: 181 | T operator-> (); 182 | }; 183 | ``` 184 | 185 | 通常来说,`operator->`必须返回一个指针类型或是另一个应用了`operator->`的类类型。`C`的完全体会触发一个错误,因为它声明了一个`int`返回类型的`operator->`。然而,因为某些常见的类模板定义实现了这种(返回类型为`T`或者`T*`)定义,所以语言规则更加灵活。于是,只有在重载决议规则确实选择了用户自定义的`operator->`时,才要求该自定义`operator->`只能返回一个应用了其他(例如,内建的)`operator->`的类型。这甚至对模板之外的代码也同样生效(尽管这种无约束行为(relaxed behavior)在那些上下文中用处不大)。因此,这里的声明不会触发错误,尽管`int`会替代该返回类型。 186 | 187 | ## 14.3 C++实例化模型 188 | 189 | 模板实例化就是从对应的模板实体通过合适地模板参数替换来得到一个常规的类型、函数或是变量的过程。这可能听起来直截了当,但实际上需要遵循非常多的细节。 190 | 191 | ### 14.3.1 两阶段查找 192 | 193 | 在第13章中,我们曾看到依赖型名称无法在解析模板时被找到。取而代之的是,它们会在实例化的时刻再次进行查找。非依赖型名称则会在更早的阶段被查找,因此当模板第一次看到它的时候,就可以诊断出许多错误。这就引出了“两阶段查找”的概念。第一阶段查找发生在解析模板的时候,而第二阶段查找发生在模板实例化的时候: 194 | 1. 在第一阶段,当解析模板时,非依赖型名称会并用一般查找规则和ADL规则(如果可行的话)。非限定依赖型名称(诸如函数调用中的函数名称,它们之所以是依赖型名称,是因为它们具有依赖型实参)会使用普通查找规则,但是这一查找结果并不会作为最终结果,而是要等到第二阶段的另一个查找过程完成(也就是模板实例化的时候)。 195 | 2. 在第二阶段,此时的模板实例化被称作POI(point of instantiation),依赖型限定名称会在此时被查找(对选定的实例用模板实参替换模板参数),而且还会对非限定依赖型名称进行额外的ADL查找(它们曾在第一阶段进行过普通查找)。 196 | 197 | 对非限定依赖型名称,首次的普通查找(并不是终态)被用来判断该名称是否是一个模板。考虑下面的例子: 198 | ```cpp 199 | namespace N { 200 | template void g() {} 201 | enum E { e }; 202 | } 203 | 204 | template void f() {} 205 | 206 | template void h(T p) { 207 | f(p); // #1 208 | g(p); // #2 ERROR 209 | } 210 | 211 | int main() { 212 | h(N::e); // calls template h with T = N::E 213 | } 214 | ``` 215 | 216 | 在`#1`行,当看到跟着一个`<`的名称`f`时,编译器就需要判断`<`到底是一个尖括号还是一个小于号。这取决于`f`是否是一个已知的模板名称。在本例中,普通查找会找到`f`的声明,它确实是一个模板,因此这里会以尖括号来成功解析。 217 | 218 | 而在`#2`行,这里会产生一个错误,这是因为普通查找并不能找到模板`g`,因此,`<`就被认为是一个小于号操作符,对于我们的例子来说这就是个语法错误。如果想让该解析通过,那么在用`T` = `N::E`实例化`h`的时候最终得用ADL找到一个模板`N::g`(尽管`N`是与`E`关联的命名空间),但是只有先成功解析`h`的泛型定义,这才能行得通。 219 | 220 | > 译者注:示例中的g和f都漏写了一个模板参数作为函数参数。不过无所谓,C++20已经允许这么写了。 221 | 222 | ### 14.3.2 POI 223 | 224 | 如上所述,C++编译器会在模板客户端代码的某些位置访问模板实体的声明或者定义。当某些代码结构引用了模板特化,而且为了生成该特化需要实例化相应的模板定义时,就会在源代码中产生一个POI。POI是源代码中的一个点,在这里会插入已被替换的模板。例如: 225 | 226 | ```cpp 227 | class MyInt { 228 | public: 229 | MyInt(int i); 230 | }; 231 | 232 | MyInt operator - (MyInt const&); 233 | 234 | bool operator > (MyInt const&, MyInt const&); 235 | 236 | using Int = MyInt; 237 | 238 | template 239 | void f(T i) 240 | { 241 | if(i > 0) { 242 | g(-i); 243 | } 244 | } 245 | 246 | // #1 247 | void g(Int) 248 | { 249 | // #2 250 | f(42); // point of call 251 | // #3 252 | } 253 | // #4 254 | ``` 255 | 256 | C++编译器看到`f(42)`时,它知道模板`f`需要用`MyInt`替换`T`来实例化:这就产生了一个POI。`#2`和`#3`与该调用点紧邻,但是它们都不适合做POI,因为C++不允许我们在这里插入`::f(Int)`的定义。此外,`#1`和`#4`两处的本质区别在于,在`#4`处,函数`g(Int)`是可见的,因此模板依赖的调用`g(-i)`可以在`#4`处被解析。然而,如果我们假定`#1`是POI的话,那么调用`g(-i)`将不能被解析,因为`g(Int)`在`#1`处是不可见的。幸运的是,对于函数模板特化的引用,C++把它的POI定义,置于紧跟在“包含这个引用的定义或声明所在的最近的命名空间作用域”之后。在我们的例子中,这个位置就是`#4`。 257 | 258 | 你可能好奇为什么这个例子引入了类型`MyInt`而不是用`int`基础类型。这是因为,在POI执行的第二次查找(指`g(-i)`)仅仅使用了ADL,而基础类型`int`并没有关联的命名空间,因此,如果使用`int`类型,就不会发生ADL查找,也就不能找到函数`g`。所以,如果你用下面的类型别名声明语句: 259 | ```cpp 260 | using Int = int; 261 | ``` 262 | 代码将无法通过编译。下面的例子有着类似的问题: 263 | ```cpp 264 | template 265 | void f1(T x) 266 | { 267 | g1(x); // #1 268 | } 269 | 270 | void g1(int) 271 | { 272 | } 273 | 274 | int main() 275 | { 276 | f1(7); // ERROR: g1 not found! 277 | } 278 | // #2 POI for f1(int) 279 | ``` 280 | 281 | `f1(7)`调用对`f1(int)`产生了一个POI紧随其后(在位置`#2`)。在这一实例中,关键点在于函数`g1`的查找。当首次遇到模板定义`f1`时,它会注意到非限定名称`g1`是一个依赖型名称,因为它作为一个函数名称,有着依赖型实参(实参`x`的类型取决于模板参数`T`)。因此,`g1`会在`#1`处使用一般查找规则,然而,在`#1`处找不到任何的`g1`。在`#2`处,即POI处,函数名称被再一次查找(在关联的命名空间和类中),但是唯一的实参类型是一个`int`型,它根本没有关联的命名空间和类。因此,`g1`永远都无法被找到,尽管在这里(POI处)其实用一般查找就可以找到`g1`。 282 | 283 | 变量模板POI的处理与函数模板相似。而对于类模板特化来说,情况则不太一样,如下例所示: 284 | ```cpp 285 | template 286 | class S { 287 | public: 288 | T m; 289 | }; 290 | 291 | // #1 292 | unsigned long h() 293 | { 294 | // #2 295 | return (unsigned long)sizeof(S); 296 | // #3 297 | } 298 | // #4 299 | ``` 300 | 301 | 老规矩,`#2`和`#3`都不能作为POI,这两个位置不能进行命名空间作用域类`S`的定义(模板是不能出现在函数作用域内部的)。假如我们可以遵循函数模板实例的规则,POI将会出现在位置`#4`处,然而,这样一来,表达式`sizeof(S)`是无效的,这是因为`S`的尺寸直到`#4`之后才能被确定。因此,生成的类模板实例的引用被紧邻地定义在包含该引用的声明或定义的命名空间作用域之前。在我们的例子中,这个位置就是`#1`。 302 | 303 | 模板在实例化时,可能还需要进行额外的实例化。请看下方这一简例: 304 | ```cpp 305 | template 306 | class S { 307 | public: 308 | using I = int; 309 | }; 310 | 311 | // #1 312 | template 313 | void f() 314 | { 315 | S::I var1 = 41; 316 | typename S::I var2 = 42; 317 | } 318 | 319 | int main() 320 | { 321 | f(); 322 | } 323 | // #2: #2a, #2b 324 | ``` 325 | 326 | 根据我们之前的讨论,`f()`的POI位于`#2`处。函数模板`f()`还引用了类模板特化`S`,它的POI位于`#1`处。与此同时它还引用了`S`,但是因为这仍然是一个依赖型名称,我们此时此刻无法真正完成实例化。然而,如果我们在`#2`处实例化`f()`,我们会注意到同时也需要实例化`S`的定义。这种副(secondary)POI(或这叫过渡的POI)的定义位置会有些差异。对于函数模板,副POI与主(primary)POI严格一致;而对于类模板,副POI会(在最近的命名空间作用域中)先于主POI。在我们的例子中,这意味着`f()`会被放在`#2b`处,而前面紧邻的`#2a`处会是`S`的副POI。请注意`S`与`S`POI的差别。 327 | 328 | 编译单元通常会包含相同实例的多个POI。对类模板实例,在每个编译单元中,只有首个POI会被保留,后续的那些都会被忽略(它们不会被真正视为POI)。对函数模板实例和变量模板实例,所有的POI都会被保留。无论是哪一种情形,ODR原则都会要求:对保留的任何一个POI处所出现的同种实例化体,都必须是等价的;但是C++编译器既不需要保证这一原则,也不需要诊断是否违反这一原则。这就允许C++编译器随便选择一个POI来完成真正的实例化,而不必担心其他POI会产生不同的实例化结果。 329 | 330 | 事实上,大多数编译器会对大部分函数模板的实例化,直到编译单元末尾处再延迟进行。某些实例化不能被拖延,这其中包括:判定某个推导的返回类型所需的实例化时(参考P296节15.10.1和P303节15.10.4)、函数是`constexpr`且必须产生一个常量结果时。有些编译器在首次使用内联函数时会进行实例化,以便立即内联调用。这实际上将对应模板特化的POI转移到了翻译单元的末尾,这是C++标准所允许的替代POI的方式。 331 | 332 | ### 14.3.3 包含式模型 333 | 334 | 当遇到POI时,对应模板的定义必须是可访问的。对类特化来说,这意味着类模板定义必须在编译单元中被更早地看见。而对函数模板和变量模板(以及类模板的成员函数和静态数据成员)的POI来说,也同样需要。典型的模板定义被简单的通过`#include`语句引入到编译单元,尽管是非类型模板也一样。这种模板定义的源码模型被称为包含式模型,它目前是当下C++标准所支持的模板的唯一自动源码模型。 335 | 336 | 尽管包含式模型鼓励程序员将所有模板定义都放在头文件中,以便它们可以满足可能出现的任何POI,但显式地使用“显式实例化声明(explicit instantiation declarations)”和“显式实例化定义(explicit instantiation definitions)”(P260节14.5)来管理实例化也是可行的。从逻辑上讲,这样做并不是一件容易的事,大多数时候程序员会更喜欢依靠自动的实例化机制。用自动方案实现的一个挑战是要解决跨不同编译单元为函数模板或变量模板(或类模板实例的相同成员函数或静态数据成员)的特化体实现完全相同的POI。我们随后会讨论这个问题的解法。 337 | 338 | ## 14.4 几种实现方案 339 | 340 | 本节我们来回顾一下支持包含式模型的几种C++实现。所有的这些实现都依赖于两个基础组件:编译器和链接器。编译器将源代码编译成目标文件,它们包含机器码和符号注释(跨引用其他目标文件和库)。链接器通过组合这些目标文件解决它们包含的跨引用符号来创建可执行程序或库文件。在下面的内容中,即使完全有可能(但不流行)以其他方式实现C ++(例如,你可以假想出一个C++解释器),我们也将采用这种模型。 341 | 342 | 当类模板特化在多个编译单元中被使用时,编译器会为每个编译单元重复实例化过程。这几乎不会造成什么问题,因为类定义并没有直接产出低层级代码。它们仅仅由C++实现体在内部使用,用来审查并解释各种其他表达式和声明。在这方面,类定义的多个实例化体与类定义的多个包含(在不同编译单元中通常通过头文件包含)没有实质性区别。 343 | 344 | 然而,如果你实例化一个(非内联)函数模板,情况就有些不同了。如果你想提供某个普通的非内联函数的多个定义,那么就会违反ODR原则。例如,假设你编译和链接下面这两个文件: 345 | ```cpp 346 | // ==== a.cpp: 347 | int main() 348 | { 349 | } 350 | 351 | // ==== b.cpp: 352 | int main() 353 | { 354 | } 355 | ``` 356 | 357 | C++编译器会对每个模块进行单独编译,此时没有什么问题,因为在每个编译单元内它们都合法。然而,如果你想把它们链接在一起,你的链接器很可能会抗议:不允许出现重复的定义。 358 | 359 | 反之,我们考虑模板的场合: 360 | ```cpp 361 | // ==== t.hpp: 362 | // common header (inclusion model) 363 | template 364 | class S { 365 | public: 366 | void f(); 367 | }; 368 | 369 | template 370 | void S::f() // member definition 371 | { 372 | } 373 | 374 | void helper(S*); 375 | // ==== a.cpp: 376 | #include "t.hpp" 377 | void helper(S* s) 378 | { 379 | s->f(); // #1 first point of instantiation of S::f 380 | } 381 | 382 | // ==== b.cpp: 383 | #include "t.hpp" 384 | int main() 385 | { 386 | S s; 387 | helper(&s); 388 | s.f(); // #2 second point of instantiation of S::f 389 | } 390 | ``` 391 | 392 | 如果链接器处理类模板实例化的成员函数与处理普通函数或成员函数的方式一致,那么编译器就需要保证它只会生成一份代码,要么在`#1`处生成,要么在`#2`处生成(两处POI的位置)。为了达成这一目标,编译器需要在每个编译单元中都携带其他的编译单元的信息,而这对于C++编译器来说在引入模板之前是从未有过的要求。接下来,我们讨论C++实现中已投入使用的三种类型解决方案。 393 | 394 | 请注意,模板实例化产生的所有的链接实体都有同样的问题:实例化的函数模板和成员函数模板,以及实例化的静态数据成员和实例化的变量模板。 395 | 396 | ### 14.4.1 贪婪实例化 397 | 398 | 首个实现贪婪实例化的C++编译器是由Borland公司开发的。现如今,这一技术已经在各种C++系统上被广泛使用了。 399 | 400 | 贪婪实例化假定链接器会意识到特定的实体(尤其是可链接的模板实例化体),它们大多在多个目标文件和库中重复出现。编译器会以一种特殊的方式标记这些实体。当链接器发现了多个实例时,它会保留单个并丢弃掉所有其他的。这就是贪婪实例化的处理方法。 401 | 402 | 理论上,贪婪实例化有一些严重的缺陷: 403 | - 编译器会在生成和优化N个实例化体时浪费时间,它只需要保持一个即可。 404 | - 链接器一般不会检查两个实例化体是否相同,因为一个模板特化的多个实例生成的代码可能有些合法的无关紧要的差别。这些微小的差异不应该导致链接器失败(编译器在实例化的时刻可能因状态不同而产生细微的差异)。然而,这常常会导致链接器无法注意到更多的充足的差异,比如某一个实例化是使用严格的浮点数运算法则,而另一个确是松弛的、高性能的浮点数运算法则。 405 | - 所有的目标文件加起来可能大小远远超过理应生成的替换体总和,这是因为相同的代码会被复制多次。 406 | 407 | 实践当中,这些缺陷看起来并没有引起重大问题。也许这是因为贪婪实例化在一个重要方面与竞品相比非常有优势:源对象之间的原始依赖被保留了下来。尤其是,每个编译单元只产生一个目标文件,并且在相应的源文件(它包含了实例化后的定义)中,每个目标文件都包含针对所有可链接定义的代码,而且这些代码是已经经过编译的代码。另一个重要的收益在于所有的函数模板实例都是内联的候选对象而无需求助于昂贵的“链接时”优化机制(实际上,函数模板实例常常是短小的函数而从内联中得益)。其他的实例化机制则需要专门对函数模板进行内联(判定)处理,以确保它们是否可以内联展开。然而,贪婪实例化甚至允许非内联函数模板也进行内联展开。 408 | 409 | 最后值得一提的是,允许可链接实体重复定义的链接器机制,通常还被用于处理重复的“内联函数溢出”(spilled inlined functions)和“虚函数调度表“(virtual function dispatch tables)。如果这一机制不可用,那么替代方法通常是以内部链接来发出这些项,但这会增大代码的体积。内联函数必须具有单一地址的要求使得以符合标准的方式去实现这一替代方法变得相当困难。 410 | 411 | ### 14.4.2 查询实例化 412 | 413 | 上世纪90年代中期,一家名为Sun Microsystems的公司发行了它们的C++编译器的新版实现(版本4.0),这一版本以一种新的有趣的方式解决了实例化问题,我们称之为查询实例化(queried instantiation)。查询实例化在概念上明显更简单、优雅,而且按照时间顺序,它也是我们在此回顾的实例化方案中最新的一种。在这一方案中,程序中参与的所有编译单元会汇集一个共享的数据库。该数据库可以追溯哪些特化体被实例化了,并且可以找到其所依赖的源代码。生成的特化体本身会把信息存储在数据库中。当可链接实体遇到一个POI时,会进入下面的处理流程: 414 | 1. 尚无可用的特化体:这种情况会进行实例化,特化的结果会保存到数据库中。 415 | 2. 特化体虽可用但超期了,因为自它生成以来源代码发生了变化。这种情况同样会进行实例化,新的特化结果会覆盖数据库中旧的那一个。 416 | 3. 数据库中有最新可用的特化体。这种情况什么都不用做。 417 | 418 | 尽管从概念上来讲非常简单,但这一设计还是要面临一些实现上的挑战: 419 | - 正确的维护数据库内容相对于源代码的依赖性并不是一件简单的事情。尽管将第三种情况误认为是第二种也不会导致错误,但是这样做会增加编译器完成的工作量(并因此增加了总体构建时间)。 420 | - 并行编译多个源文件非常常见,因此,工业级实现需要支持适当数量的并发控制。 421 | 422 | 尽管存在这些挑战,这一方案还是可以非常有效地实施。此外,没有明显的病态场景会导致该方案的伸缩性变差。例如,与贪婪实例化相比,贪婪实例化可能会导致许多浪费的工作。 423 | 424 | 不幸的是,数据库的使用可能对程序员来说也存在一些问题。这些问题中的大部分的源头都在于传统的继承自C编译器的编译模型将不再可用:单一的编译单元不再会产生单独的目标文件。例如,假设你希望链接最终的程序,链接操作不仅需要各个编译单元所关联的目标文件的内容,还需要数据库中存储的目标文件。类似地,如果你创建了一个二进制库文件,你需要确保创建该库的工具(一般是一个链接器或是一个打包器)也能意识到数据库中的内容。这些问题大都可以通过不将实例化体存储在数据库,而是在目标文件中第一个引起实例化的地方放置目标代码的方式来缓解。 425 | 426 | 库文件还面临另一个挑战。许多生成的特化体可以打包在同一个库中。当库被另一个项目所添加时,该项目的数据库也需要意识到该库的数据库中已经可用的那些实例化体。否则,一旦项目创建了存在于库中的某个实例化的POI,就会遇到重复的实例化。一种可以解决该问题的策略是效仿贪婪实例化的链接器技术;让链接器意识到生成的特化体,并把它们淘汰掉(尽管如此,它的发生频率要比贪婪实例化要少得多)。源文件、目标文件以及库文件的各种复杂组织形式通常也会带来一些很难解决的问题,诸如找不到实例化体,因为包含该实例化体的目标代码可能并没有被链接入最终的可执行程序中。 427 | 428 | 总而言之,查询实例化最终没能在市场中存活,甚至Sun的编译器目前也在使用贪婪实例化。 429 | 430 | ### 14.4.3 迭代实例化 431 | 432 | 第一个支持C++模板的编译器是Cfront 3.0,它是语言之父Bjarne Stroustrup开发C++语言时所写的编译器的后浪。Cfront有一个不予变通的限制:它必须有良好的跨平台移植性。这就意味着:(1)在多个目标平台中,它都是使用C语言作为共同的目标表示;(2)它使用了局部的目标链接器,即链接器无法察觉到模板的存在。实际上,Cfront以普通C函数的形式来分发模板实例化体,因此它也必须避免重复的实例化体。虽然Cfront的源模型与标准的包含式模型有所差异,但它的实例化策略可以通过一些修改而适应包含式模型。于是,它也值得被公认为是迭代实例化的第一个实现。 433 | 434 | Cfront的迭代过程如下所述: 435 | 1. 编译源代码,此时不要实例化任何需要链接的特化体 436 | 2. 使用预链接器(prelinker)链接目标文件 437 | 3. 预链接器调用链接器,解析错误信息,判断是否缺少某个实例化体。如果缺少的话,预链接器会调用编译器,来编译包含所需模板定义的源代码,然后(可选地)生成该缺少的实例化体。 438 | 4. 重复第3步,直到不再生成新的定义。 439 | 440 | 第3步中,这种迭代的要求基于这样的事实:在实例化一个可链接实体过程中,可能会要求”另一个仍未实例化“的实体进行实例化;最后,所有的迭代都已经完成,链接器才会成功创建一个完整的程序。 441 | 442 | 原始Cfront方案的缺陷相当严重: 443 | - 要完成一次完整的链接,所需要的时间不仅包含预链接器的时间开销,还包括每次询问重新编译和重新链接的时间。某些使用Cfront系统的用户会抱怨说:”链接时间往往需要几天,而同样的工作,如果采用前面介绍的其他候选解决方案,则一个小时就足够了。” 444 | - 诊断信息(错误和警告)延迟到了链接期,当链接大型程序时,这个缺点才是最严重的。譬如,对于模板定义中的某个书写错误,开发者可能需要等待漫长的几个小时才能检查出来。 445 | - 需要进行特别地处理,来记住包含特殊定义的源代码的位置,Cfront(在一些情况下)会使用一个中心库,他不得不克服查询实例化方案中所面临的中心数据库的一些挑战。另外,原始Cfront实现并不支持并行编译。 446 | 447 | 迭代原则后来被Edison Design Group(EDG)和惠普的C++编译器实现精炼了一番,消除了原始Cfront实现的一些缺陷。实际上,这些实现体表现相当好,尽管从头开始构建比其他的替代方案更耗时,但后续的构建时间却相当有可比性。不过,相对而言,很少有C ++编译器使用迭代实例化。 448 | 449 | ## 14.5 显式实例化 450 | 451 | 为模板特化显式地生成POI是可行的,我们把获得这种特化的结构称为显式实例化引导(explicit instantiation directive)。从语法上来说,它由关键字`template`和紧随其后的待实例化的特化声明组成。例如: 452 | ```cpp 453 | template 454 | void f(T) 455 | { 456 | } 457 | 458 | // four valid explicit instantiations: 459 | template void f(int); 460 | template void f<>(float); 461 | template void f(long); 462 | template void f(char); 463 | ``` 464 | 465 | 注意上面的每一个实例化引导都是有效的。模板实参可以被推导(见第15章)。 466 | 467 | 类模板的成员也可以通过这种方式显式实例化: 468 | ```cpp 469 | template 470 | class S { 471 | public: 472 | void f() { 473 | } 474 | }; 475 | 476 | template void S::f(); 477 | 478 | template class S; 479 | ``` 480 | 481 | 此外,通过显式实例化该类模板特化本身,其所有的成员也都可以被显式实例化。因为这些显式实例化引导确保了具有名称的模板特化的定义被创造了出来,上面的显式实例化引导更准确地来说,指的是显式实例化定义(explicit instantiation definitions)。显式实例化的模板特化不应该被显示地特化,反之亦然,因为这样会产生两个不同的定义(也就违反了ODR原则)。 482 | 483 | ### 14.5.1 手动实例化 484 | 485 | 许多C++程序员都观察到了自动模板实例化在编译期有一个值得一提的负面影响。这对于实现了贪婪实例化的编译器来说确实如此(P256节14.4.1),因为相同的模板特化可以在许多不同的编译单元中实例化。 486 | 487 | 有一种缩短构建时间的技术:在单一位置手动实例化程序所需的那些模板特化,并禁止其在所有其他编译单元中实例化。一种确保这种禁止行为的可行方法是:除非在编译单元中,有显示地实例化,否则不提供其模板定义。例如: 488 | ```cpp 489 | // ===== translation unit 1: 490 | template void f(); // no definition: prevents instantiation 491 | // in this translation unit 492 | void g() 493 | { 494 | f(); 495 | } 496 | 497 | // ===== translation unit 2: 498 | template void f() 499 | { 500 | // implementation 501 | } 502 | 503 | template void f(); // manual instantiation 504 | 505 | void g(); 506 | 507 | int main() 508 | { 509 | g(); 510 | } 511 | ``` 512 | 513 | 在第一个编译单元中,编译器看不到函数模板`f`的定义,因此它不会实例化`f`。第二个编译单元借由显式实例化定义提供了`f`的定义,如果没有该定义的话,程序链接会失败。 514 | 515 | 手动实例化有一个明显的缺陷:我们必须小心地追溯哪些实体会被实例化。对于大型项目来说,这很快就变成一个负担,因此我们并不推荐使用。我们已经在好几个项目中使用了这种做法,这些项目最初低估了这种负担,然而随着代码的成熟,我们对一开始的决定感到遗憾。 516 | 517 | 然而,手动实例化也有一些优势,因为实例化转变成了程序的需求。显然,它避免了大型头文件的开销,也避免了在多个编译单元中重复实例化具有相同参数的相同模板的开销。此外,模板定义的源代码可以隐藏起来,只不过客户端程序此后就再也无法创建额外的实例化体了。 518 | 519 | 手动实例化的一些负担可以通过将模板定义摆放至第三方源文件中来减轻,按照惯例,以`.tpp`作为扩展。对我们的函数`f`来说,就会变成: 520 | ```cpp 521 | // ===== f.hpp 522 | template void f(); // no definition: prevents instantiation 523 | 524 | // ===== t.hpp 525 | #include "f.hpp" 526 | template void f() // definition 527 | { 528 | // implementation 529 | } 530 | 531 | // ===== f.cpp 532 | #include "f.tpp" 533 | 534 | template void f(); // manual instantiation 535 | ``` 536 | 537 | 这种结构提供了某种灵活性。你可以仅仅引用`f.hpp`来获取`f`的声明,此时不会有自动实例化。显式实例化体可以被手动地添加到`f.cpp`中(如果需要的话)。或者,如果手动实例化太费劲,你也可以包含`f.tpp`来启用自动实例化。 538 | 539 | ### 14.5.2 显式实例化声明 540 | 541 | 消除冗余自动实例化的一种更有针对性的方法是使用显式实例化声明,该声明是一个以关键字`extern`为前缀的显式实例化引导。显式实例化声明通常会抑制命名模板特化的自动实例化,因为它声明命名模板特化将在程序中的某个位置定义(通过显式实例化定义)。之所以说是通常来说,是因为有一些特例存在: 542 | - 内联函数仍可以实例化,以展开成内联样式(但不会生成单独的目标代码)。 543 | - 具有`auto`或`decltype(auto)`推导的类型和具有返回类型推导的函数仍然可以被实例化,以判断它们的类型。 544 | - 其值可用作常量表达式的变量仍可以被实例化,以便对其值进行求值。 545 | - 引用类型的变量仍然可以被实例化,因此可以解析它们引用的实体。 546 | - 类模板和别名模板仍然可以被实例化,以检查其返回类型。 547 | 548 | 通过使用显式实例化声明,我们可以在头文件(t.hpp)中为`f`提供模板定义,然后通过使用特化来抑制自动实例化,如下: 549 | ```cpp 550 | // ===== t.hpp 551 | template void f() 552 | { 553 | } 554 | 555 | extern template void f(); // declared but not defined 556 | extern template void f(); // declared but not defined 557 | 558 | // ===== t.cpp 559 | template void f(); // definition 560 | template void f bool f(T p) { 574 | if constexpr (sizeof(T) <= sizeof(long long)) { 575 | return p > 0; 576 | } else { 577 | return p.compare(0) > 0; 578 | } 579 | } 580 | 581 | bool g(int n) { 582 | return f(n); // OK 583 | } 584 | ``` 585 | 586 | 编译器if是一个if语句,其中关键字`if`后面紧跟着一个`constexpr`关键字(如本例所示)。跟随在后面的是一个小括号条件语句,该语句必须是一个常量布尔值(也可以是隐式转换为`bool`值的情形)。编译器因而就会知道该选择哪一个分支,而另一个未被选中的分支则被称作“丢弃的分支”。特别有趣的是,在模板(包括泛型lambda)的实例化过程中,被丢弃的分支不会进行实例化。对于这一示例代码的合法性来说,该机制很有必要:我们用`T=int`来实例化`f(T)`,会使得else分支被丢弃。如果该分支未被丢弃的话,它就会进行实例化,此时表达式`p.compare(0)`会引起一个错误(当`p`是简单的`int`型时,这段代码是不合法的)。 587 | 588 | 在C++17的constexpr if语句出现之前,规避这类错误需要进行显式模板特化或重载(见第16章)才能起到相似的效果。 589 | 590 | 上面的例子,在C++14中,可能会按如下方法来实现: 591 | 592 | ```cpp 593 | template struct Dispatch { // only to be instantiated when b is false 594 | static bool f(T p) { // (due to next specialization for true) 595 | return p.compare(0) > 0; 596 | } 597 | }; 598 | 599 | template<> struct Dispatch { 600 | static bool f(T p) { 601 | return p > 0; 602 | } 603 | }; 604 | 605 | template bool f(T p) { 606 | return Dispatch::f(p); 607 | } 608 | 609 | bool g(int n) { 610 | return f(n); // OK 611 | } 612 | ``` 613 | 614 | 显然,constexpr if这一替代方案的引入使得我们的意图简明扼要、一目了然。然而,它需要(编译器)的实现去提炼实例化单元:此前的函数定义始终都是作为整体来实例化,现在它必须禁用其中的一部分。 615 | 616 | 另一个非常好用的constexpr if的场景是处理函数模板包的递归表达式。为了泛化这一例子,我们引用P134节8.5中出现的例子: 617 | ```cpp 618 | template 619 | void f(Head&& h, Remainder&&... r) { 620 | doSomething(std::forward(h)); 621 | if constexpr (sizeof...(r) != 0) { 622 | // handle the remainder recursively (perfectly forwarding the arguments): 623 | f(std::forward(r)...); 624 | } 625 | } 626 | ``` 627 | 628 | 如果没有constexpr if语句,我们需要对`f()`模板实现一个额外的重载来保证递归的终结。 629 | 630 | 甚至在非模板上下文中,constexpr if语句有时也能起到独特的效果: 631 | ```cpp 632 | void h(); 633 | void g() { 634 | if constexpr (sizeof(int) == 1) { 635 | h(); 636 | } 637 | } 638 | ``` 639 | 640 | 大部分平台,`g()`中的条件都是`false`,对`h()`的调用也就会被丢弃掉。因此,`h()`甚至完全不需要被定义(当然,除非它在别的地方被使用到了)。如果在此示例中省略了关键字`constexpr`,则在链接期会触发“缺少`h()`的定义”的错误。 641 | 642 | ## 14.7 标准库中的显式实例化 643 | 644 | C++标准库包含了若干数量的模板,这些模板通常仅仅与一些基础类型一起使用。例如,和`std::basic_string`类模板一起最常用的类型就是`char`或`wchar_t`,尽管使用其他的类字符类型也可以完成实例化。因此,对标准库的实现来说,通常会为这些常见的情景引入显式实例化声明。例如: 645 | 646 | ```cpp 647 | namespace std { 648 | template, 649 | typename Allocator = allocator> 650 | class basic_string { 651 | ... 652 | }; 653 | 654 | extern template class basic_string; 655 | extern template class basic_string; 656 | } 657 | ``` 658 | 659 | 实现了标准库的源文件会包含相应的显式实例化定义,因此这些常见的实现体可以在所有使用标准库的编译单元中共享。类似的显示实例化还出现在各种“流(stream)”类类型中,诸如`basic_iostream`, `basic_istream`等等。 660 | 661 | ## 14.8 后记 662 | 663 | 本章处理了两个有一定联系但并不相同的议题:C++模板编译模型和各种C++模板实例化机制。 664 | 665 | 编译模型在程序编译的各个阶段确定模板的含义。特别地,它确定了实例化模板中各种结构的含义。名称查找是编译模型的重要组成部分。 666 | 667 | 标准C++仅仅支持单个编译模型,即包含式模型。然而,在1998和2003标准中还支持一个叫分离式模型的模板编译模型。分离式模型允许模板定义可以在其实例化体所在的不同的编译单元中书写。这种导出式模板(exported templates)仅曾经由Edison Design Group(EDG)实现过一次。EDG在实现中付出的努力确定了以下两点:(1)实现C++模板的分离式模型相当的困难,而且完成这一任务的耗时远超预期;(2)分离式模型的假定好处(例如优化编译时间)由于模型的复杂性而无法实现。随着2011标准的制定工作逐渐结束,很明显其他实现者将不会支持这一功能,于是,C++标准委员会根据投票结果最终从语言中删除了导出式模板。如果你对分离式模型的细节感兴趣,可以看看本书的第一版,里面描述了导出式模板的行为。 668 | 669 | 实例化机制是一种外部机制,用以允许C++实现者去正确地创建实例化体。这些机制可能会受限于链接器和其他软件构建工具的需求。尽管每一种实例化机制都各不相同且各有利弊,但它们对日常C++编程来说并没有显著的影响。 670 | 671 | 就在C++11标准完成之后,Walter Bright, Herb Sutter和Andrei Alexandrescu提议了一种“static if”特性,它与“constexpr if”不同(文献N3329)。这是一种更为宽泛的特性,它甚至可以出现在函数定义外部(Walter Bright是D语言的设计者和实现者,它有一个相似的特性)。例如: 672 | ```cpp 673 | template 674 | struct Fact { 675 | static if (N <= 1) { 676 | constexpr unsigned long value = 1; 677 | } else { 678 | constexpr unsigned long value = N*Fact::value; 679 | } 680 | }; 681 | ``` 682 | 683 | 请注意看在上例中,类作用域声明是如何条件化的。然而,这种强大的能力是有争议的,有些委员会成员担心它可能会被滥用,而另一些委员会成员则不喜欢该提案的某些技术方面(诸如花括号未引入作用域,以及完全不分析丢弃的分支)。 684 | 685 | 几年之后,Ville Voutilainen又提出了一个提案(P0128),该提案的大部分内容在日后摇身一变促成了constexpr if语句的诞生。它经历了几轮小版本的设计迭代(涉及临时关键字`static_if`和`constexpr_if`),并且在Jens Maurer的帮助下,Ville最终将该提议编入了该语言中(由文献P0292r2)。 -------------------------------------------------------------------------------- /第13章 模板中的名称.md: -------------------------------------------------------------------------------- 1 | # 第13章 模板中的名称 2 | 3 | 在大多数编程语言中,名称是一个基本的概念。借助名称,程序员可以引用前面已经构造完毕的实体。当C++编译器遇到一个名称时,它会查找该名称,来确认它所引用的是哪个实体。从实现者角度来看,就名称而言,C++在这方面相当棘手。譬如C++语句`x * y;`,如果`x`和`y`都是变量的名称,那么这一语句就是一个乘法表达式,但是如果`x`是类型的名称,该语句就是在声明一个`y`变量实体,其类型是`x`类型实体的指针。 4 | 5 | 这一小小的例子阐释了C++(类C)是一门上下文敏感型语言(context-sensitive language):对于C++的一个结构,我们无法脱离上下文来理解它。而这又与模板有什么关联呢?事实上,模板也是一种结构,它也必须处理多种上下文相关信息:(1)模板出现的上下文;(2)模板实例化的上下文;(3)用于模板实例化的模板实参的上下文。因此,在C++中,“名称”需要被小心的处理这一事实就不足为奇了。 6 | 7 | ## 13.1 名称的分类 8 | 9 | C++对名称的分类有多种多样的方式。为了理解名称的众多术语,我们提供了表13.1和表13.2,对这些分类进行了描述。幸运的是,熟悉下面两种主要的命名概念,就可以深入理解大多数的C++模板话题: 10 | 1. 如果名称的作用域由域操作符(`::`)或是成员访问操作符(`.`或`->`)显式指定,我们就称该名称为限定名称(qualified name)。例如,`this->count`是一个限定名称,但是`count`本身则不是(尽管字面上`count`实际上指代的也是一个类成员)。 11 | 2. 如果一个名称以某种方式依赖于模板参数,那么该名称就是一个依赖型名称(dependent name)。例如,当`T`是一个模板参数时,`std::vector::iterator`是一个依赖型名称;但如果`T`是一个已知的类型别名时(比如`using T = int` ),那么`std::vector::iterator`就不是一个依赖型名称。 12 | 13 | | 分类 | 解释和说明 | 14 | | - | - | 15 | | 标识符(Identifier) | 仅由不间断的字母、下划线和数字组成的名称。不能以数字开头,并且某些标识符是被保留的:你不能在应用程序中引入它们(有个潜规则:请避免使用下划线和双下划线开头)。字母这一概念较为宽泛,它还包含了通用字符名称(Universal Character Name, UCN),UCN通过非字符的编码格式存储信息。 | 16 | | 操作符函数id(Operator-function-id) | 关键字`operator`后紧跟的操作符符号。例如,`operator new`和`operator []`。| 17 | | 类型转换函数id(Conversion-function-id) | 用于表示用户自定义的隐式转换运算符,例如`operator int &`(也可以以`operator int bitand`的方式进行混淆)。 | 18 | | 字面操作符id(Literal-operator-id) | 用于表示一个用户定义的字面操作符——例如,`operator ""_km`,可以用来书写字面值`100_km`(C++11中引入) | 19 | | 模板id(Template-id) |由闭合的尖括号子句内的模板实参构成的模板名称。例如,`List`。模板实参所在的闭合尖括号前面的操作符函数id或一个字面操作符id也可以是一个模板id。例如,`operator+>。`| 20 | | 非限定id(Unqualified-id) | 广义的标识符。可以是上述的任何一种(标识符、操作符函数id、类型转换函数id、字面操作符id或是模板id),也可以是一个“析构器名称”(例如,记如`~Data`或是`~List`)。 | 21 | | 限定id(Qualified-id) | 对非限定id使用类、枚举、命名空间的名称做限定或是仅仅使用全局作用域操作符做限定,得到的就是一个限定id。显然这种名称本身也可以是多次限定的。例如`::X`,`S::x`,`Array::y`和`::N::A::z`。 | 22 | | 限定名称(Qualified-name) | 标准中并没有定义这一概念,但是我们一般用它来表示经过限定查找的名称。具体来说,它是一个限定id或是一个在前面显式使用了成员访问操作符(`.`或`->`)的非限定id。例如`S::x`,`this->f`和`p->A::m`。然而,上下文中隐式等价于`this->class_mem`的`class_mem`并不是一个限定名称:成员访问必须是显式的。| 23 | | 非限定名称(Unqualified-name) | 除限定名称以外的非限定id。这并非标准中的概念,我们只是用它来表示调用非限定查找时引用的名称。 | 24 | | 名称(Name) | 一个限定或非限定名称 | 25 |
表13.1 名称分类(第一部分)
26 | 27 | | 分类 | 解释和说明 | 28 | | - | - | 29 | | 依赖型名称(Dependent name) | 通过某种方式依赖于模板参数的名称。一般来说,显式包含模板参数的限定名称或非限定名称都是依赖型名称。此外,如果成员访问运算符(`.`或`->`)左侧的表达式与类型相关,则通常由其限定的限定名称也是一个依赖型名称,这一概念在P223节13.3.6中进行了讨论。特别地,`this->b`中的`b`当其出现在模板中时,通常是依赖型名称。最后,取决于参数依赖查找的名称,诸如函数调用`ident(x, y)`中的`ident`或是表达式`x+y`中的`+`,当且仅当参数表达式中任意一个是类型依赖的,那么其就是一个依赖型名称。 | 30 | | 非依赖型名称(Nondependent name) | 不满足上述描述中“依赖型名称”的名称即是一个非依赖型名称 | 31 |
表13.2 名称分类(第二部分)
32 | 33 | 通读该表会更加熟悉C++模板话题中的这些概念,但是也没有必要去记住每个定义的精准含义。什么时候需要,就什么时候通过索引来查阅。 34 | 35 | ## 13.2 名称查找 36 | 37 | 在C++中,名称查找有非常多的小细节,但是我们这里只关注一些主要概念。只有在下面两种情景中我们才有必要确认名称查找的细节:(1)按直觉处理会犯错的一般案例(2)C++标准给出的错误案例。 38 | 39 | 限定名称在限定结构所隐含的作用域中进行查找。如果该作用域是一个类,则还可以向上搜索基类。然而,在查找限定名称时不会考虑封闭作用域(enclosing scopes)。下面的例子阐释了这一基本原则: 40 | 41 | ```cpp 42 | int x; 43 | 44 | class B { 45 | public: 46 | int i; 47 | }; 48 | 49 | class D: public B { 50 | }; 51 | 52 | void f(D* pd) 53 | { 54 | pd->i = 3; // finds B::i 55 | D::x = 2; // ERROR: does not find ::x in the enclosing scope 56 | } 57 | ``` 58 | 59 | 非限定名称的查找则恰恰相反,它可以(由内到外)在所有外围类中逐层地进行查找(但在某个类内部定义的成员函数定义中,它会优先查找该类和基类的作用域,然后才查找外围类的作用域),这种查找方式被称为一般性查找(ordinary lookup)。下面是一个用于理解一般性查找的基本示例: 60 | 61 | ```cpp 62 | extern int count; // #1 63 | int lookup_example(int count) // #2 64 | { 65 | if (count < 0) { 66 | int count = 1; // #3 67 | lookup_example(count); // unqualified count refers to #3 68 | } 69 | return count + ::count; // the first (unqualified) count refers to #2; 70 | // the second (qualified) count refers to #1 71 | } 72 | ``` 73 | 74 | 对于非限定名称的查找,最近的一种变化是除了普通的查找之外,它们可能还会经历参数依赖查找(argument-dependent lookup, ADL)。在展开叙述ADL之前,我们先用前面的`max()`模板来说明这一机制的动机: 75 | 76 | ```cpp 77 | template 78 | T max(T a, T b) 79 | { 80 | return b < a ? a : b; 81 | } 82 | ``` 83 | 84 | 假设我们现在需要让”在另一个命名空间所定义的某个类型“来使用这一模板: 85 | 86 | ```cpp 87 | namespace BigMath { 88 | class BigNumber { 89 | ... 90 | }; 91 | 92 | bool operator < (BigNumber const &, BigNumber const &); 93 | ... 94 | } 95 | 96 | using BigMath::BigNumber; 97 | 98 | void g(BigNumber const& a, BigNumber const& b) 99 | { 100 | ... 101 | BigNumber x = ::max(a,b); 102 | ... 103 | } 104 | ``` 105 | 106 | 这里的问题在于`max()`模板不认识`BigMath`命名空间,一般性查找无法找到类型`BigNumber`适用的`operator <`。如果没有特殊规则的话,这种限制大大降低了C++命名空间中模板的应用性。而ADL正是这个“特殊规则”,也正是解决这种限制的关键之处。 107 | 108 | ### 13.2.1 ADL 109 | 110 | ADL主要适用于在函数调用或运算符调用中看起来像非成员函数名称的非限定名称。如果一般性查找找到了以下信息,ADL就不会发生: 111 | - 成员函数名称 112 | - 变量名称 113 | - 类型名称 114 | - 块作用域函数声明名称 115 | 116 | 如果把被调用函数的名称用圆括号括起来,ADL也会被禁用。 117 | 118 | 否则,如果名称后的括号里面有实参表达式列表,则ADL将会查找这些实参“关联”的命名空间和类。对这些关联的命名空间(associated namespace)和关联类(associated class)的精准定义会在后文给出,但在直觉上它们可以被认为是与给定类型相关联的所有命名空间和类。例如,如果某一类型是一个`class X`的指针,那么关联的类和命名空间就包括`X`和`X`所属的任何命名空间或类。 119 | 120 | 对给定类型,关联命名空间和关联类所组成的集合的精准定义,我们可以通过下列规则来确定: 121 | - 对内置类型,该集合为空集。 122 | - 对指针和数组类型,该集合就是其底层所引用类型的关联类和关联命名空间。 123 | - 对枚举类型,关联命名空间就是枚举声明所在的命名空间。 124 | - 对类成员,关联类就是其所在的类。 125 | - 对类类型(包括联合体类型),关联类集合包括其类型本身、它的外围类型、所有的直接或间接基类。关联命名空间集合是每个关联类所在的命名空间。如果类是一个类模板实例,那么类模板实参的类型以及声明模板的模板实参所在的类和命名空间也将包含在内。 126 | - 对函数类型,关联命名空间和类的集合包含每一个参数类型和返回值所关联的命名空间和类。 127 | - 对指向类`X`的成员指针类型,关联的命名空间和类包括`X`以及成员类型本身的关联。(如果是指向成员函数的类型,那么参数和返回类型也算数。) 128 | 至此,ADL会在所有的关联命名空间和关联类中依次地查找,就好像依次地直接使用这些命名空间进行限定一样。唯一的例外情况是:它会忽略`using`指示符(using-directives)。下面的例子说明了这一点: 129 | 130 | *details/adl.cpp* 131 | ```cpp 132 | #include 133 | 134 | namespace X { 135 | template void f(T); 136 | } 137 | 138 | namespace N { 139 | using namespace X; 140 | enum E { e1 }; 141 | void f(E) { 142 | std::cout << "N::f(N::E) called\n"; 143 | } 144 | } 145 | 146 | void f(int) 147 | { 148 | std::cout << "::f(int) called\n"; 149 | } 150 | 151 | int main() 152 | { 153 | ::f(N::e1); // qualified function name: no ADL 154 | f(N::e1); // ordinary lookup finds ::f() and ADL finds N::f(), 155 | // the latter is preferred 156 | } 157 | ``` 158 | 159 | 我们可以看出:在这个例子中,当执行ADL时,命名空间`N`中的`using-directive`被忽略了。因此,在这个`main()`函数内部的调用中,`X::f()`甚至永远都无法作为一个候选者。 160 | 161 | ### 13.2.2 友元声明的ADL 162 | 163 | 在类中友元函数的声明可以是该友元函数的首次声明。在此场景中,对于包含这个友元函数的类,假设它所属的最近的命名空间作用域(可能是全局作用域)为作用域A,我们就可以认为该友元函数是在作用域A中声明的。然而,这样的友元声明在该作用域中并不是直接可见的。考虑下面的例子: 164 | 165 | ```cpp 166 | template 167 | class C { 168 | ... 169 | friend void f(); 170 | friend void f(C const&); 171 | ... 172 | }; 173 | 174 | void g(C* p) { 175 | f(); // is f() visible here? 176 | f(*p); // is f(C const&) visible here? 177 | } 178 | ``` 179 | 180 | 如果友元声明在封闭命名空间中可见,那么实例化一个类模板可能会使一些普通函数的声明也变为可见的(比如f())。这可能会产生一些令人惊讶的行为:函数调用`f()`会导致编译错误,除非类C的实例化在程序更早的地方进行过! 181 | 182 | 另一方面,仅仅通过友元函数声明(并定义)一个函数非常有用(参考P497节21.2.1依赖于这种行为的某个技巧)。当友元函数所在的类属于ADL查找过程的关联类时,该友元函数就是可见的。 183 | 184 | 再次考虑上面的例子,`f()`没有关联类或关联命名空间,因为它并没有任何参数:在这个例子中该调用是无效的。然而,`f(*p)`调用有着关联类`C`(因为它是`*p`的类型),并且全局命名空间也是关联的(因为这是`*p`的类型声明所在的命名空间)。因此,只要我们在调用之前完全实例化`class C`,就可以找到这一第二个友元函数。为了确保这一点,我们可以假设:对于涉及在关联类中友元查找的调用,实际上会导致该(关联)类被实例化(如果还没有实例化的话)。 185 | 186 | ADL查找友元声明和定义的能力有时候也被称为友元名称注入(friend name injection)。然而,这一术语有些误导性,因为它是一个前标准C++特性的名称,该特性会确实地把友元声明的名称“注入”到封闭作用域中,使得它们在一般性名称查找中可见。对上例来说,这就意味着两个调用都有效。本章的后续内容会详述友元名称注入的历史。 187 | 188 | ### 13.2.3 注入的类名称 189 | 190 | 类的名称会被注入到类本身的作用域中,因此在该作用域中作为非限定名称可访问。(然而,它作为限定名称不可访问,因为这种符号表示用于表示构造函数。)例如下面的例子: 191 | 192 | *details/inject.cpp* 193 | ```cpp 194 | #include 195 | 196 | int C; 197 | class C { 198 | private: 199 | int i[2]; 200 | public: 201 | static int f() { 202 | return sizeof(C); 203 | } 204 | }; 205 | 206 | int f() 207 | { 208 | return sizeof(C); 209 | } 210 | 211 | int main() 212 | { 213 | std::cout << "C::f() = " << C::f() << ',' 214 | << " ::f() = " << ::f() << '\n'; 215 | } 216 | ``` 217 | 218 | 成员函数`C::f()`返回了`class`类型`C`的尺寸,而`::f()`则返回了`int`变量`C`的尺寸。 219 | 220 | 类模板也可以有注入的类名称。然而,相比较一般的注入的类名称来说,二者有些区别:它的后面可以紧跟模板实参(在此场景,它们也被称为注入的类模板名称)。但是,如果后面没有紧跟模板实参,那么它们代表的就是用参数来代表实参的类(例如,对于偏特化,还可以用特化实参代表对应的模板实参)。下述代码解释了这一情景: 221 | 222 | ```cpp 223 | template class TT> class X { 224 | }; 225 | 226 | template class C { 227 | C* a; // OK: same as "C* a;" 228 | C& b; // OK 229 | X c; // OK: C without a template argument list denotes the template C 230 | X<::C> d; // OK: ::C is not the injected class name and therefore always 231 | // denotes the template 232 | }; 233 | ``` 234 | 235 | 注意看非限定名称是如何引用注入的名称的,并且,如果名称后没有跟随模板实参列表的话,它们不会被认作模板名称。为了补偿,我们可以在模板名称前强制使用`::`限定符。 236 | 237 | 可变模板的注入的类名称还有一个额外的特点:如果注入的类名称是通过使用可变模板的模板参数直接组成的,那么注入的类名称也将包含尚未展开的模板参数包(参考P201节12.4.1了解包展开的细节)。因此,在为可变参数模板形成注入的类名时,与模板参数包对应的模板参数是一个模板参数包的展开,其模式就是那个模板参数包: 238 | 239 | ```cpp 240 | template class V { 241 | V* a; // OK: same as "V* a;" 242 | V<0, void> b; // OK 243 | }; 244 | ``` 245 | 246 | ### 13.2.4 当前实例 247 | 248 | 类或类模板的注入的类名称实际上是类型定义的一个别名。对非模板类来说,这一特性是显然的,因为类本身就是其作用域内其名称的唯一类型。然而,在类模板或是类模板嵌套的类中,每个模板实例都会产生一个不同的类型。在这一上下文中,该特性就非常有趣了,因为这意味着注入的类名称指向类模板的相同实例而非类模板的其他实例(对类模板的嵌套类来说也一样)。 249 | 250 | 在类模板中,类或类模板范围内的注入的类名称或是其他等价于注入的类名称的类型(包括类型别名的声明)都被称为一个当前实例(current instantiation)。依赖于模板参数但并不指代一个当前实例的类型被称为一个未知的特化(unknown specialization),它可以从相同的类模板或某些全然不同的类模板实例化。下面的例子阐释了这一区别: 251 | 252 | ```cpp 253 | template class Node { 254 | using Type = T; 255 | Node* next; // Node refers to a current instantiation 256 | Node* previous; // Node refers to a current instantiation 257 | Node* parent; // Node refers to an unknown specialization 258 | }; 259 | ``` 260 | 261 | 在嵌套类和类模板中辨别某个类型是否指代一个当前实例往往扑朔迷离。类和类模板范围内的注入的类名称(或者等价于它们的类型)是一个当前实例,而其他嵌套的类或类模板中的名称则不是一个当前实例: 262 | 263 | ```cpp 264 | template class C { 265 | using Type = T; 266 | 267 | struct I { 268 | C* c; // C refers to a current instantiation 269 | C* c2; // C refers to a current instantiation 270 | I* i; // I refers to a current instantiation 271 | }; 272 | 273 | struct J { 274 | C* c; // C refers to a current Instantiation 275 | C* c2; // C refers to a current instantiation 276 | I* i; // I refers to an unknown specialization, 277 | // because I does not enclose 278 | J* j; // J refers to a current instantiation 279 | }; 280 | }; 281 | ``` 282 | 283 | 当类型指代的是一个当前实例时,实例化的类的内容可以保证是由当前定义的类模板或嵌套类所实例化的。当解析模板(下一节的主题)时这对名称查找有着意义,但与此同时它也引导了另一种方案,一种更像游戏的方式来决定类模板中的类型`X`的定义指代的是一个当前实例还是一个未知的特化:如果另一个程序员可以写出一个显式特化(在第16章描述细节)使得`X`指向该特化体,那么`X`就指代一个未知的特化。例如,考虑上例上下文中类型`C::J`的实例:我们知道`C::J`的定义用于实例化特定的具体类型(也就是我们所实例化的类型)。此外,由于显式特化无法在不同时特化范围内所有模板或成员的情况下,特化某一个模板或模板成员,`C`会在类定义范围内被实例化。因此,`J`和`C`的引用在J所在范围内均指代一个当前实例。而另一方面,我们可以写出一个`C::I`的显式特化,如下文: 284 | 285 | ```cpp 286 | template<> struct C::I { 287 | // definition of the specialization 288 | }; 289 | ``` 290 | 291 | 这里,`C::I`的特化提供了一个与`C::J`所可见的定义完全不同的定义,因此定义`C::J`中定义的`I`指代的是一个未知的特化。 292 | 293 | ## 13.3 模板解析 294 | 295 | 大多数程序设计语言的编译都包含两个最基本的步骤——token化(也称作扫描或词法解析)和(语法)解析。Token化过程会按字符顺序读取源代码,然后生成一个token序列。例如,当看到字符序列`int* p = 0;`时,扫描器会为关键字`int`、符号/操作符`*`、标识符`p`、符号/操作符`=`、整型字面量`0`和符号/操作符`;`生成token。 296 | 297 | 解析器会通过将token或先前发现的模式(pattern)递归地归约为更高级别的结构,从而在token序列中找到已知的模式。例如,token `0`是一个合法的表达式,`*`后跟随的标识符`p`是一个合法的声明器(declarator),该声明器后接`=`再接表达式`0`是一个合法的初始化声明器(init-declarator)。最后,关键字`int`是一个已知的类型名称,并且当后面跟着初始化声明器`*p = 0`时,就归约为`p`的初始化声明。 298 | 299 | ### 13.3.1 非模板中的上下文相关性 300 | 301 | 如你所闻与所愿,token化过程比解析要简单得多。幸运的是,解析已经是一门理论发展得相当成熟的学科,使用这一理论对于理解大多数语言的解析都不算困难。然而,这一理论在上下文无关语言中表现最佳,而我们已经知道了C++是一门上下文敏感语言。为此,C++编译器会使用一张符号表来把标记器(tokenizer)和解析器(parser)结合起来:当解析到声明时,会把它灌入到符号表中。当标记器找到一个标识符时,它会进行查找,如果找到的是一个类型的话,就对生成的token进行注解。 302 | 303 | 例如,如果C++编译器看到`x*`,标记器会查找`x`。如果找到了一个类型,解析器就会看到: 304 | ``` 305 | identifier, type, x 306 | symbol, * 307 | ``` 308 | 并得出一个结论:这是要开始声明了。然而,如果没有找到类型`x`,那么解析器会从标记器处接收这样的信息: 309 | ``` 310 | identifier, nontype, x 311 | symbol, * 312 | ``` 313 | 此时该结构按合法性只能被解析成一个乘法表达式。这些原则的细节要依赖于编译器的具体实现策略,但大同小异。 314 | 315 | 另一个上下文敏感的案例在下面的表达式中阐释: 316 | ``` 317 | X<1>(0) 318 | ``` 319 | 如果`X`是类模板的名称,那么前面的表达式就是将整型`0`强制类型转换到类型`X<1>`(由该模板产生的)。如果`X`不是一个模板,那么上面的表达式等价于 320 | ``` 321 | (X<1)>0 322 | ``` 323 | 换句话说,`X`会和1比较,然后根据结果——`true`或`false`,隐式转换成`1`或`0`——再与0进行比较。尽管这样的代码非常罕见,但它也是一个合法的C++代码(也是合法的C代码)。C++解析器会查找`<`前出现的名称,只有在该名称是一个模板名称时,才会把`<`看成是左尖括号;否则,`<`就被视为普通的小于操作符。 324 | 325 | 令人遗憾的是,这类上下文敏感性都是由于选择尖括号来界定模板参数列表所造成的。下面是另一个案例: 326 | 327 | ```cpp 328 | template 329 | class Invert { 330 | public: 331 | static bool const result = !B; 332 | }; 333 | 334 | void g() 335 | { 336 | bool test = Invert<(1>0)>::result; // parentheses required! 337 | } 338 | ``` 339 | 340 | 如果`Invert<(1>0)>`的小括号被省略,大于等于符号就会被误认为是模板参数列表的闭合尖括号。这会使得代码无效,因为编译器会把它读作`((Invert<1>))0>::result`。 341 | 342 | 尖括号带给标记器的问题还不止这些。例如,在语句: 343 | ```cpp 344 | List> a; 345 | //^-- no space between right angle brackets 346 | ``` 347 | 两个`>`字符组合成了一个右移操作符`>>`,因此它们不再被视为两个独立的符号。这要归因于所谓的maximum munch tokenization原则:C++实现体必须让一个token能捕获尽可能多的连续字符。 348 | 349 | 如P28节2.2所提及,在C++11之后,C++标准特别指出了这一情景——嵌套的模板id紧跟着右移符号`>>`——解析器会将模板id紧邻的右移符号视为两个独立的右尖括号`>`。有趣的是,此变更项会默默地更改某些程序(公认的程序)的含义。考虑下面的例子: 350 | 351 | *names/anglebrackethack.cpp* 352 | ```cpp 353 | #include 354 | 355 | template struct X { 356 | static int const c = 2; 357 | }; 358 | 359 | template<> struct X<0> { 360 | typedef int c; 361 | }; 362 | 363 | template struct Y { 364 | static int const c = 3; 365 | }; 366 | 367 | static int const c = 4; 368 | 369 | int main() 370 | { 371 | std::cout << (Y >::c >::c>::c) << ' '; 372 | std::cout << (Y>::c >::c>::c) << '\n'; 373 | } 374 | ``` 375 | 376 | 这是一个合法的C++98程序,输出`0 3`。它也是合法的C++11程序,但是尖括号变革使得括号内的两个语句是等价的,最终输出`0 0`。 377 | 378 | 由于`<:`是字符`[`的两字符替代(某些传统键盘是不支持的),还存在一个类似的问题,考虑下面的案例: 379 | 380 | ```cpp 381 | template struct G {}; 382 | struct S; 383 | G<::S> gs; // valid since C++11, but an error before that 384 | ``` 385 | 386 | C++11之前,最后一行代码等价于`G[:S>gs;`,这显然是不合法的。另一个词法hack技术被引入来解决该问题:当编译器看到字符序列`<::`没有紧跟着`:`或`>`时,前导`<:`字符对不再被视为`[`等价的两字符符号。这一两字符hack技术使得以前合法的程序变得不再合法: 387 | 388 | ```cpp 389 | #define F(X) X ## : 390 | 391 | int a[] = {1, 2, 3}, i = 1; 392 | int n = a F(<::)i]; // valid in C++98/C++03, but not in C++11 393 | ``` 394 | 395 | 想要理解它,就要注意到两字符hack应用于预处理符号,对预处理器来说变成了截然不同的符号,它们在宏展开完成前被确定。因此,C++98/C++03会无条件转换`<:`到`[`,因而定义展开成`int n = a[ :: i];`,显然这是没问题的。而C++11则不会进行字符转换,因为在宏展开前,序列`<::`没有跟随`:`或`>`而是`)`时,两字符转译不会进行,因此连接操作符`##`会试图连接`::`和`:`成为一个新的预处理符号`:::`,但显然这是一个不合法的符号。这一标准会导致UB行为(undefined behavior),也就意味着放任编译器自由处理。某些编译器会诊断出这一问题,但也有些不会:它们会保持两个预处理符号分离,然后导致语法错误,因为对`n`的定义最终展开成如下语句: 396 | ```cpp 397 | int n = a < :: : i]; 398 | ``` 399 | 400 | ### 13.3.2 类型的依赖型名称 401 | 402 | 模板中名称的问题在于它们无法始终被充分地分类。具体来讲,一个模板无法引用另一个模板的名称,因为其他模板的内容可能因显式特化而使原本的名称失效。下面的例子阐释了这一概念: 403 | 404 | ```cpp 405 | template 406 | class Trap { 407 | public: 408 | enum { x }; // #1 x is not a type here 409 | }; 410 | 411 | template 412 | class Victim { 413 | public: 414 | int y; 415 | void poof() { 416 | Trap::x * y; // #2 declaration or multiplication? 417 | } 418 | }; 419 | 420 | template<> 421 | class Trap { // evil specialization! 422 | public: 423 | using x = int; // #3 x is a type here 424 | }; 425 | 426 | boid boom(Victim& bomb) 427 | { 428 | bomb.poof(); 429 | } 430 | ``` 431 | 432 | 编译器解析行#2时,它必须确定这是一个声明语句还是一个乘法表达式。这一决定取决于依赖型限定名称`Trap::x`是否是一个类型名称。编译器此时会尝试在模板`Trap`中查找,并且发现根据行#1,`Trap::x`并不是一个类型,从而让我们相信行#2是一个乘法表达式。然而,在后面`T`取`void`的特化中,我们改写了(泛型的)`Trap::X`,让它变成了一个类型,这完全违背了前面的源码。在特化场景中,`Trap::x`实际上是一个`int`类型。 433 | 434 | 本例中,类型`Trap`是一个依赖型类型,因为类型取决于模板参数`T`。此外,`Trap`指代的是一个未知的特化(在P223节13.2.4中描述),这意味着编译器无法安全的在模板中查找以判定名称`Trap::x`是否是一个类型。当`::`前的类型指代的是一个当前实例时——例如,`Victim::y`——编译器才可以在模板定义中查找,这是因为它已经确定不会有其他的特化来干预。因此,如果`::`前的类型指代的是一个当前实例,那么模板中限定名称的查找与非依赖类型的限定名称查找表现得非常相似。 435 | 436 | 然而,如上例所阐释,未知特化中的名称查找始终是一个问题。C++语言通过下面的规定来解决这个问题:通常来说,一个依赖型限定名称并不代表一个类型,除非在名字的前面加上了一个关键字`typename`前缀。对于类型而言,如果不加上`typename`前缀,那么在替换模板实参后,就不会被看成是一个类型名称,从而导致程序是无效的,你的C++编译器还会抱怨在实例化过程中出现了错误。另一方面,我们应该知道`typename`的这种用法和前面用于表示模板类型参数的用法是不同的:在这里你不能使用关键字`class`来等价替换`typename`。 437 | 438 | 总之,当类型名称具有以下性质时,就应该在名称前面添加`typename`前缀: 439 | 1. 名称是限定的,且本身没有后跟`::`组成一个更为限定的名称。 440 | 2. 名称不是详细类型说明符(elaborated-type-specifier)的一部分(例如,以`class`,`struct`,`union`,或`enum`起始的关键字)。 441 | 3. 名称不在指定基类继承的列表中,也不在引入构造函数的成员初始化列表中。 442 | 4. 名称依赖于模板参数。 443 | 5. 名称是某个未知特化的成员,这意味着由限定器命名的类型指代一个未知的特化。 444 | 445 | 此外,除非至少满足前两个条件,才能使用`typename`前缀。下面的错误案例为此予以解释: 446 | 447 | ```cpp 448 | template // 1 449 | struct S : typename X::Base { // 2 450 | S() : typename X::Base(typename X::Base(0)) { // 3 4 451 | } 452 | 453 | typename X f() { // 5 454 | typename X::C * p; // declaration of pointer p // 6 455 | X::D *q; 456 | } 457 | 458 | typename X::C *s; // 7 459 | 460 | using Type = T; 461 | using OtherType = typename S::Type; // 8 462 | } 463 | ``` 464 | 465 | 每个出现的`typename`,不管正确与否,都被标了号。第一个`typename`表示一个模板参数。前面的规则没有应用于此。第二个和第三个`typename`由于上述规则的第三条而被禁止。这两个上下文中,基类的名称不能用`typename`引导。然而,第四个`typename`是必不可少的,因为这里基类的名称既不是位于初始化列表,也不是位于派生类的继承列表,而是为了基于实参`0`构造一个临时`X::Base`表达式(也可以是某种强制类型转换)。第5个`typename`同样不合法,因为它后面的名称`X`并不是一个限定名称。对于第6个`typename`,如果期望声明一个指针,那么这个`typename`是必不可少的。下一行省略了关键字`typename`,因此也就被编译器解释为一个乘法表达式。第7个`typename`是可选(可有可无)的,因为它符合前面的两条规则,但不符合后面的两条规则。第8个`typename`也是可选的,因为它指代的是一个当前实例的成员(不满足最后一条规则)。 466 | 467 | 最后一条判断`typename`前缀是否需要的规则有时候难以评估,因为它取决于判断类型所指代的是一个当前实例还是一个未知特化这一事实。在这种场景中,最简单安全的方法就是直接添加`typename`关键字来表明限定名称是一个类型。`typename`关键字,尽管它是可选的,也会提供一个意图上的说明。 468 | 469 | ### 13.3.3 模板的依赖型名称 470 | 471 | 当一个模板的名称是依赖型名称时,我们将会遇到类似上一小节的问题。通常而言,C++编译器会把模板名称后面的`<`看作模板实参列表的开始,否则的话`<`就会被视为小于操作符。与类型名称一样,除非程序员使用关键字`template`提供了额外的信息,编译器是不会把依赖性名称视作模板的: 472 | 473 | ```cpp 474 | template 475 | class Shell { 476 | public: 477 | template 478 | class In { 479 | public: 480 | template 481 | class Deep { 482 | public: 483 | virtual void f(); 484 | }; 485 | }; 486 | }; 487 | 488 | template 489 | class Weird { 490 | public: 491 | void case1 (typename Shell::template In::template Deep* p) { 492 | p->template Deep::f(); // inhibit virtual call 493 | } 494 | void case2 (typename Shell::template In::template Deep& p) { 495 | p.template Deep::f(); // inhibit virtual call 496 | } 497 | }; 498 | ``` 499 | 500 | 这个多少有些复杂的例子展示了所有可以限定名称的操作符是如何需要在操作符前添加关键字`template`的。明确来讲,如果限定符号前面的名称或表达式的类型需要依赖于某个模板参数,并且紧跟在限定符后面的是一个模板id(template-id)(换句话说,就是指一个后面带有闭合尖括号实参列表的模板名称),那么就应该使用关键字`template`。例如,在下面的表达式中: 501 | ```cpp 502 | p.template Deep::f() 503 | ``` 504 | `p`的类型依赖于模板参数`T`。因此,C++编译器并不会查找`Deep`来判断它是否是一个模板,并且我们必须显式地通过插入`template`前缀来指定`Deep`是一个模板名称。如果没有该前缀,`p.Deep::f()`就会被解析成`((p.Deep)f()`。还要注意在一个限定名称内部,可能需要多次使用关键字`template`,因为限定符本身可能还会受限于外部的依赖型名称(可以从上例的case1和case2的参数中看到)。 505 | 506 | 如果例子中的关键字`template`被省略了,那么左尖括号和右尖括号会被解析为小于和大于操作符。由于使用了`typename`关键字,我们可以安全的添加`template`前缀来指明后面的名称是一个模板id(template-id),即使`template`前缀并不是严格需要的。 507 | 508 | ### 13.3.4 Using声明中的依赖型名称 509 | 510 | Using声明会从两个地方引入名称:命名空间和类。命名空间这一部分与本文不相干,因为并没有诸如命名空间模板(namespace templates)这样的东西。而对于类来说,using声明只能把基类的名称引入到继承类。这样的using声明看起来像继承类访问基类的“符号链接”或是“快捷方式”,就好像是继承类自身声明的成员一样。千言万语不及一个小小示例,我们用一个非模板示例来阐述: 511 | 512 | ```cpp 513 | class BX { 514 | public: 515 | void f(int); 516 | void f(char const*); 517 | void g(); 518 | }; 519 | 520 | class DX : private BX { 521 | public: 522 | using BX::f; 523 | }; 524 | ``` 525 | 526 | 类`DX`使用using声明将名称`f`从基类`BX`中引入。本例中,该名称关联了两个不同的声明,但我们这里强调的是一种名称机制,而不是关注该名称是否是一个单一的声明。此外,using声明可以让以前不能访问的成员变成可访问的。从示例代码中可以看到,基类和它的成员对派生类`DX`是私有的(因为私有继承),只有函数`BX::f`是个例外,它因被using引入到了`DX`的公有接口而能够访问。 527 | 528 | 现在,你可能已经发现了当使用using声明从依赖类中引入名称的问题所在。尽管我们知道该名称,我们还是不知道这个名称到底是一个类型,还是一个模板,或是其他什么东西: 529 | 530 | ```cpp 531 | template 532 | class BXT { 533 | public: 534 | using Mystery = T; 535 | template 536 | struct Magic; 537 | }; 538 | 539 | template 540 | class DXTT : private BXT { 541 | public: 542 | using typename BXT::Mystery; 543 | Mystery* p; // would be a syntax error without the earlier typename 544 | }; 545 | ``` 546 | 547 | 如果我们想要使用using声明引入依赖型名称来指定类型时,我们必须显式地插入`typename`关键字前缀。奇怪的是,在这样的名称是一个模板时,C++标准并没有提供一个类似的机制来标记。下面的代码片段揭示了这个问题: 548 | 549 | ```cpp 550 | template 551 | class DXTM : private BXT { 552 | public: 553 | using BXT::template Magic; // ERROR: not standard 554 | Magic* plink; // SYNTAX ERROR: Magic is not a known template 555 | }; 556 | ``` 557 | 558 | 标准委员会至今没有考虑这个议题。然而,C++11别名模板提供了一个迂回解决方案: 559 | 560 | ```cpp 561 | template 562 | class DXTM : private BXT { 563 | public: 564 | template 565 | using Magic = typename BXT::template Magic; // Alias template 566 | Magic* plink; // OK 567 | }; 568 | ``` 569 | 570 | 这可能看起来有点笨,但是对类模板的场景它满足了需求。不幸的是,函数模板的情景目前还没有解决(可以说非常少见)。 571 | 572 | ### 13.3.5 ADL与显式模板实参 573 | 574 | 考虑下面的示例: 575 | ```cpp 576 | namespace N { 577 | class X { 578 | ... 579 | }; 580 | 581 | template void select(X*); 582 | } 583 | 584 | void g(N::X* xp) 585 | { 586 | select<3>(xp); // ERROR: no ADL! 587 | } 588 | ``` 589 | 590 | 我们期望在调用`select<3>(xp)`中模板`select()`可以通过ADL来找到。然而事与愿违,这是因为编译器直到确定`<3>`是一个模板实参列表之前,它都无法确定`xp`是一个函数调用参数。更进一步,编译器直到确定`select()`是一个模板之前它都无法确定`<3>`是一个模板实参列表。由于这个先有鸡还是先有蛋的问题无法被解决,表达式就会被解析成一个毫无意义的表达式:`(select<3)>(xp)`。 591 | 592 | 这个例子可能会给你一种ADL对模板id(template-id)没有发挥作用的假象,但事实并非如此。我们可以通过在调用前引入`select`的函数模板声明来解决这个问题: 593 | ```cpp 594 | template void select(); 595 | ``` 596 | 597 | 尽管对于调用`select<3>(xp)`来说这没有任何意义,但这一函数模板的存在确保了`select<3>`会被解析成一个模板id(template-id)。ADL就可以顺势找到函数模板`N::select`,然后成功调用。 598 | 599 | ### 13.3.6 依赖型表达式 600 | 601 | 与名称相似,表达式本身也可以依赖于模板参数。依赖于模板参数的表达式彼此之间有着较大差异——例如,选择一个不同的重载函数或是产生一个不同的类型或常量。不依赖于模板参数的表达式,其所有的实例提供相同的行为。 602 | 603 | 依赖于模板参数的表达式多种多样。最常见的是类型依赖表达式(type-dependent expression),表达式的类型本身可以因实例的变化而不同——例如,函数参数类型为模板参数的表达式: 604 | 605 | ```cpp 606 | template void typeDependent1(T x) 607 | { 608 | x; // the expression type-dependent, because the type of x can vary 609 | } 610 | ``` 611 | 具有类型依赖子表达式的表达式,通常来说,其本身也是类型依赖的——例如,使用实参`x`调用函数`f()`: 612 | 613 | ```cpp 614 | template void typeDependent2(T x) 615 | { 616 | f(x); // the expression is type-dependent, because x is type-dependent 617 | } 618 | ``` 619 | 620 | 这里请注意`f(x)`的类型可能因实例的变化而有所不同,因为`f`本身依赖于参数类型,而该参数类型又依赖于模板,因此,两阶段查找(在P249节14.3.1讨论)会在不同的实例中找到完全不同的函数名`f`。 621 | 622 | 并非所有涉及模板参数的表达式都是类型依赖的。例如,涉及模板参数的某个表达式可以在不同的实例中产生不同的常量`values`。这种表达式被称为值依赖表达式(value-dependent expression),最简单的一种就是指向非依赖类型的非类型模板参数。例如: 623 | 624 | ```cpp 625 | template void valueDependent1() 626 | { 627 | N; // the expression is value-dependent but not type-dependent; 628 | // because N has a fixed type but a varying constant type 629 | } 630 | ``` 631 | 632 | 正如类型依赖表达式那样,如果一个表达式是由其他值依赖表达式所组成的,那么通常来说它也是一个值依赖表达式,因此`N + N`或是`f(N)`都是值依赖表达式。 633 | 634 | 有趣的是,一些操作符,诸如`sizeof`,拥有一个已知的结果类型,因此它们可以把一个类型依赖操作数转换成一个值依赖表达式(也就不是类型依赖的)。例如: 635 | 636 | ```cpp 637 | template void valueDependent2(T x) 638 | { 639 | sizeof(x); // the expression is value-dependent but not type-dependent 640 | } 641 | ``` 642 | 643 | 不论输入什么,`sizeof`操作符总是产生一个类型为`std::size_t`的值,因此`sizeof`表达式永远不会是类型依赖的,即使——在本例中——它的子表达式是类型依赖的。然而,计算得到的结果常量值会因不同的实例而有所变化,因此`sizeof(x)`是一个值依赖表达式。 644 | 645 | 那么如果我们对一个值依赖表达式使用`sizeof`操作符会发生什么呢? 646 | 647 | ```cpp 648 | template void maybeDependent(T const& x) 649 | { 650 | sizeof(sizeof(x)) 651 | } 652 | ``` 653 | 654 | 这里,正如前文所述,内层的`sizeof`表达式是值依赖的。然而,外层的`sizeof`表达式永远会计算`std::size_t`的尺寸,因此它的类型和常量值对所有的模板实例来说都是一致的,尽管最内层的表达式(`x`)是类型依赖的。涉及模板参数的任何表达式都是一个实例依赖表达式(instantiation-dependent expression),即使它的类型和常量值对所有有效的实例来说都是不变的。然而,实例依赖表达式可能在实例化过程中变得无效。例如,使用不完整类类型去实例化`maybeDependent()`会触发一个错误,因为`sizeof()`不能应用于这种类型。 655 | 656 | 类型、值和实例依赖性可以被认为是一系列表达式更为广义的分类。任何类型依赖表达式也可以被认为是值依赖的,因为因不同实例而变化的表达式类型自然而然地会有不同的常量值。类似地,类型或值因不同实例而变化的表达式在某种意义上依赖于模板参数,因此类型依赖表达式和值依赖表达式都是实例依赖的。它们的关系如图13.1所示。 657 | 658 | ![](images/13_01.png) 659 |
图13.1 类型、值、实例依赖表达式的关系
660 | 661 | 因为上下文都是由内(类型依赖表达式)向外推进,更多模板行为会在模板解析时确定,因而无法因不同实例而变化。例如,对于调用`f(x)`:如果`x`是类型依赖的,那么`f`就是依赖型名称,它会面临两阶段查找(P249节14.3.1);而当`x`是值依赖而并非类型依赖时,`f`就不是一个依赖型名称,它的名称在模板被解析的那一刻就已经完全被确定了。 662 | 663 | ### 13.3.7 编译错误 664 | 665 | 当所有的模板实例都将产生错误时,C++编译器被允许(但没被要求)在解析模板时可以忽略该错误。让我们扩展一下前文`f(x)`这一例子: 666 | 667 | ```cpp 668 | void f() { } 669 | 670 | template void nondependentCall() 671 | { 672 | f(x); // x is value-dependent, so f() is nondependent; 673 | // this call will never succeed 674 | } 675 | ``` 676 | 677 | 函数调用`f()`在每个(模板)实例中都会产生一个错误,因为`f`是一个非依赖型名称,而唯一可见的`f`却接受零个参数,而非一个。C++编译器可以在解析该模板时或者等到模板进行第一个实例化时产生一个错误:常用的编译器对该案例的表现并不一致。你可以构造相似的例子:表达式是实例依赖的,但并不是值依赖的。 678 | 679 | ```cpp 680 | template void instantiationDependentBound() 681 | { 682 | constexpr int x = sizeof(N); 683 | constexpr int y = sizeof(N) + 1; 684 | int array[x - y]; // array will have a negative size in all instantiations 685 | } 686 | ``` 687 | 688 | ## 13.4 派生和类模板 689 | 690 | 类模板可以继承或被继承。对多数情况来说,模板和非模板的继承没有显著区别。然而,当从一个依赖型名称基类派生一个类模板时,二者有着微妙而又重要的区别。让我们先来看一个非依赖型基类的例子。 691 | 692 | ### 13.4.1 非依赖型基类 693 | 694 | 在类模板中,非依赖型基类是指拥有一个完整类型而无需模板实参即可确定的基类。换句话说,这种基类使用的是非依赖型名称。例如: 695 | ```cpp 696 | template 697 | class Base { 698 | public: 699 | int basefield; 700 | using T = int; 701 | }; 702 | 703 | class D1 : public Base> { // not a template case really 704 | public: 705 | void f() { basefield = 3; } // usual access to inherited member 706 | }; 707 | 708 | template 709 | class D2 : public Base { // nondependent base 710 | public: 711 | void f() { basefield = 7; } // usual access to inherited member 712 | T strange; // T is Base::T, not the template parameter! 713 | }; 714 | ``` 715 | 716 | 非依赖型模板基类的表现和普通的非模板基类没什么差别,但是有一个细微的区别(可能有些惊奇):当非限定名称在模板继承中被找到时,非依赖型基类中会优先考虑该名称而后才轮到模板参数列表。这意味着在上面的例子中,成员`strange`始终是对应`Base::T`(也就是`int`)类型。因此,下面的函数就是非法的C++代码: 717 | ```cpp 718 | void g(D2& d2, int* p) 719 | { 720 | d2.strange = p; // ERROR: type dismatch! 721 | } 722 | ``` 723 | 这可能有点反直觉,它需要编写者意识到继承的非依赖型模板基类名称的存在——即使这种派生是间接的或者名称是私有的情况。事实上,在参数化实体的(如上面的`D2`)作用域中,可能往往倾向于先查找模板参数,只可惜事与愿违。 724 | 725 | ### 13.4.2 依赖型基类 726 | 727 | 在前面的例子中,基类都是完全确定的,它并不依赖于模板参数。这意味着一旦模板定义是可见的,那么C++编译器就可以在那些基类中查找非依赖型名称。有一种替代品(一种不被C++标准所允许的)会延迟这类名称的查找,直到模板被实例化。这种替代品的缺陷在于:它同时也将诸如漏写了某个符号而导致的错误信息延迟到了模板实例化的时候才产生。因此,C++标准规定模板中出现的非依赖型名称,会在出现的第一时间进行查找。有了这一概念后,我们看看下面的例子: 728 | 729 | ```cpp 730 | template 731 | class DD : public Base { // dependent base 732 | public: 733 | void f() { basefield = 0; } // #1 problem 734 | }; 735 | 736 | template<> // explicit specialization 737 | class Base{ 738 | public: 739 | enum { basefield = 42 }; // #2 tricky! 740 | }; 741 | 742 | void g(DD& d) 743 | { 744 | d.f(); // #3 oops? 745 | } 746 | ``` 747 | 748 | 在`#1`处我们发现了一个非依赖型名称`basefield`:它必须即刻进行查找。假设我们在模板`Base`中找到了它,并且把它与该`int`型成员进行绑定。然而,紧随其后,我们在一个`Base`的显式特化中覆盖了这一泛型定义。于是,这一特化改变了刚刚确定好的`basefield`的意义!因此,当我们在`#3`处实例化`DD::f`的定义时,就会发现我们在`#1`处过早地绑定了非依赖型名称,然而,在`DD`中并没有可供修改的`basefield`(`#2`处特化的枚举值),因此这里本应该抛出一个错误信息才对。 749 | 750 | 为了解决这个问题,C++标准声明:非依赖型名称不会在依赖型基类中进行查找(但仍然是在出现的第一时间查找)。因此,符合C++标准的编译器会在`#1`处给出一个诊断信息。为了修正这段代码,只需要将`basefield`这个名称变为依赖型名称即可,这是因为依赖型名称只在实例化的时候才被查找,而此时此刻基类的实例就已经确定了。比如说,在`#3`处,编译器就会知道`DD`的基类是`Base`,并且这个基类是程序员自己显式特化的一个实例。本例中,我们推荐的方式就是让名称转成依赖型: 751 | 752 | ```cpp 753 | template 754 | class DD1 : public Base { 755 | public: 756 | void f() { this->basefield = 0; } // lookup delayed 757 | }; 758 | ``` 759 | 760 | 还可以使用限定名称来引入依赖性: 761 | 762 | ```cpp 763 | template 764 | class DD2 : public Base { 765 | public: 766 | void f() { Base::basefield = 0; } 767 | }; 768 | ``` 769 | 770 | 如果使用后一个解决方法,我们要格外小心,因为如果(原来的)非限定的非依赖型名称是被用于虚函数调用的话,那么这种引入依赖性的限定将会禁止虚函数调用,从而也会改变程序的含义。因此,当遇到第2种解决方案不适用的情况,我们可以使用方案1: 771 | 772 | ```cpp 773 | template 774 | class B { 775 | public: 776 | enum E { e1 = 6, e2 = 28, e3 = 496 }; 777 | virtual void zero(E e = e1); 778 | virtual void one(E&); 779 | }; 780 | 781 | template 782 | class D : public B { 783 | public: 784 | void f() { 785 | typename D::E e; // this->E would not be valid syntax 786 | this->zero(); // D::zero() would inhibit virtuality 787 | one(e); // one is dependent because its argument is dependent 788 | } 789 | }; 790 | ``` 791 | 792 | 注意看我们这里是如何用`D::E`来取代`B::E`的。对本例来说,二者皆可。然而在多重继承场景中,我们可能无法知道哪一个基类提供了这一想要的成员(在这种情况下,使用派生类进行资格审查),也有可能多个基类同时声明了相同的名称(在这种情况下,我们不得不使用特定的基类名称来消除歧义)。 793 | 794 | 还要注意,调用`one(e)`中的名称`one`是依赖于模板参数的,这仅仅是因为它的显式调用实参是依赖型名称。然而,如果我们是把这种“依赖于模板参数的类型”隐式地用作缺省实参,那么就不符合上述情况,因为编译器要到决定查找的时候,才会确认缺省实参是否是依赖型的,这同样是一个先有鸡还是先有蛋的问题。为了避免细微的差池,我们更趋向于在允许使用`this->`前缀的地方都使用`this->`前缀,这同样适用于非模板代码。 795 | 796 | 如果你觉着反复的限定会影响代码美观,你可以在派生类中只引入依赖型基类中的名称一次: 797 | 798 | ```cpp 799 | // Variation 3: 800 | template 801 | class DD3 : public Base { 802 | public: 803 | using Base::basefield; // #1 dependent name now in scope 804 | void f() { basefield = 0; } // #2 fine 805 | }; 806 | ``` 807 | 808 | 在`#2`处的查找是成功的,它会找到`#1`处的声明。然而,`using`声明直到实例化时才被确定,这也达成了我们的目的。这种机制也有些约束。例如,如果是多重继承,程序员必须严格地选择包含期望的成员的那一个基类。 809 | 810 | 在当前实例中查找限定名称时,C++标准规定了首先要在当前实例中查找,然后才是所有的非依赖型基类,这与非限定名称的查找类似。如果找到了某个名称,限定名称就会指代当前实例的某个成员,因而也就不是一个依赖型名称。如果找不到这样的名称,并且类还有其他的依赖型基类,那么限定名称就会指代一个未知的特化实例的某个成员。例如: 811 | 812 | ```cpp 813 | class NonDep { 814 | public: 815 | using Type = int; 816 | }; 817 | 818 | template 819 | class Dep { 820 | public: 821 | using OtherType = T; 822 | }; 823 | 824 | template 825 | class DepBase : public NonDep, public Dep { 826 | public: 827 | void f() { 828 | typename DepBase::Type t; // finds NonDep::Type; 829 | // typename keyword is optional 830 | typename DepBase::OtherType* ot; // finds nothing; DepBase::OtherType 831 | // is a member of an unknown specialization 832 | } 833 | }; 834 | ``` 835 | 836 | ## 13.5 后记 837 | 838 | 首个解析模板定义的编译器是由Taligent公司在20世纪90年代中期开发的。在这之前(即使在这之后的一段时间),大多数编译器都把模板看成是一系列要在(解析过程后面的)实例化时刻才被处理的标记。因此,除了处理诸如查找模板定义结束位置等少许操作以外,都不会进行其他的解析。在撰写本书的此刻,微软的Visual C++编译器仍然以这种方式工作。Edison Design Group's(EDG's)编译器前端使用了一种混合技术——在内部模板被视为一串注释的token,但是会执行“通用解析”来校验语法(EDG's的产品模仿大多数其他编译器;特别的,它相当程度地模仿了微软编译器的行为)。 839 | 840 | Bill Gibbons是Taligent公司在C++委员会的代表,他极力主张让模板可以无二义性地进行解析。然而,直到惠普公司完成第一个完整的编译器之后,Taligent公司的努力才真正产品化,也才有了一个真正编译模板的C++编译器。和其他具有竞争性优点的产品一样,这个C++编译器很快就由于高质量的诊断信息而得到业界的认可。模板的诊断信息不会总是延迟到实例化时刻的事实也要归功于这个编译器。 841 | 842 | 在模板的早期开发过程中,Tom Pennello(Metaware公司的一位著名解析专家)就意识到了尖括号所带来的一些问题。Stroustrup也对这个话题进行了讨论[StroustrupDnE],而且认为人们更喜欢阅读尖括号,而不是圆括号。然而,除了尖括号和圆括号,还存在其他的一些可能性:Pennello在1991年的C++标准大会(在达拉斯举办)上特别地提议使用大括号,例如(`List{::X}`)。然而,在那时,问题的扩展程度是非常有限的,因为嵌入在其他模板内部的模板(也称为成员模板)还是不合法的,因此也就不会涉及到P230节13.3.3的问题。最后,委员会拒绝了这个取代尖括号的提议。 843 | 844 | 在P237节13.4.2中描述的非依赖型名称和依赖型基类的名称查找规则是在1993年C++标准中引入的。早在1994年,Bjarne Stroustrup的[StroustrupDnE]首次公开描述了这一规则。然而直到1997年惠普才把这一规则引入其C++编译器,自那以后出现了大量的派生自依赖型基类的类模板代码。事实上,当惠普工程师开始测试该实现时,他们发现大部分以特殊方式使用模板的代码都无法再通过编译了。特别地,STL的所有实现都在成百上千个地方打破了这一规则。考虑到客户的转换成本,对于那些“假定非依赖型名称可以在依赖型基类中进行查找的”代码,惠普软化了相关的诊断信息。例如,对于位于类模板作用域的非依赖型名称,如果利用标准原则不能找到该名称,C++就会在依赖型基类中进行查找。如果仍然找不到,才会给出一个错误而编译失败。然而,如果在依赖型基类中找到了该名称,那么就会给出一个警告,对该名称进行标记并且看成是依赖型名称,然后在实例化的时候试图再次查找。 845 | 846 | 在查找过程中,“非依赖型基类中的名称会隐藏相同名称的模板参数(P236节13.4.1)”这一规则显然是一个疏忽,但是修改这一规则的建议还没有被C++标准委员会所认可。最好的办法就是避免使用非依赖型基类中的名称作为模板参数名称。命名转换对这一类问题都是一个好的解决方式。 847 | 848 | 友元注入一度被认为是有害的,因为它会使得程序的合法性与实例出现的顺序紧密相关。Bill Gibbons(此时他还在Taligent公司开发编译器)就是解决这一问题的最大支持者,因为消除实例顺序依赖性激活了一个新的、有趣的C++开发环境(传闻Taligent正在做)。然而,Barton-Nackman trick(P497节21.2.1)需要一种友元注入的形式,正是这种特殊的技术使它以基于ADL的当前(弱化)形式保留在语言中。 849 | 850 | Andrew Koenig首次为操作符函数提出了ADL查找(这就是为什么有时候ADL也被称为Koenig查找),动机主要是考虑美观性:“用外围命名空间显式地限定操作符名称”看起来很拖沓(例如,对于a+b,我们需要这样编写:`N::operator+(a,b)`),而为每个操作符都书写using声明又会让代码看起来非常笨重。因此,才决定操作符可以在参数关联的命名空间中查找。ADL随后被扩展到普通函数名称的查找,得以容纳有限种类的友元名称注入,并为模板及其实例支持两阶段查找模型(第14章)。泛化的ADL规则也被称作扩展的Koenig查找。 851 | 852 | 尖括号hack的规格说明由David Vandevoorde通过其文献N1757在C++11中引入。他还通过解决核心议题1104的方式增添了有向图hack,以解决美国对C++ 11标准草案的审核要求。 -------------------------------------------------------------------------------- /第16章 特化与重载.md: -------------------------------------------------------------------------------- 1 | # 第16章 特化与重载 2 | 3 | 到目前为止,我们已经学习了C++模板如何使得一个泛型定义能够扩展为一系列相关联的类、函数或变量。尽管这是一种强大的功能机制,但在许多情况下,对于特定的模板参数替换,泛型的操作远非最佳选择。 4 | 5 | C++与其他流行的编程语言相比,对于泛型编程来说有着一独到之处,这是因为它有着一个丰富的特性集,能够让某一个更加特化的设施对泛型定义进行无形替代。在本章,我们将会学习两种C++语言机制:模板特化和函数模板重载,它们与纯粹的泛型相比可以有所差别。 6 | 7 | ## 16.1 当“泛型代码”不完全契合时 8 | 9 | 考虑下例: 10 | ```cpp 11 | template 12 | class Array { 13 | private: 14 | T* data; 15 | ... 16 | public: 17 | Array(Array const&); 18 | Array& operator=(Array const&); 19 | 20 | void exchangeWith(Array* b) { 21 | T* tmp = data; 22 | data = b->data; 23 | b->data = tmp; 24 | } 25 | 26 | T& operator[](std::size_t k) { 27 | return data[k]; 28 | } 29 | ... 30 | }; 31 | 32 | template inline 33 | void exchange(T* a, T* b) 34 | { 35 | T tmp(*a); 36 | *a = *b; 37 | *b = tmp; 38 | } 39 | ``` 40 | 41 | 对简单类型来说,`exchange()`的泛型实现表现良好。然而,对于有着昂贵的拷贝操作符的类型来说,相比于为特定的给定结构体量身定制的实现来说,泛型实现体更为昂贵(从机器周期和内存使用两方面来说)。在我们的例子中,泛型实现体需要调用一次`Array`的拷贝构造器和两次`Array`的拷贝操作符(译者注:作者这里应该是想用`Array`代入`exchange`的模板参数`T`)。对于大尺寸的数据结构来说,这些拷贝动作通常会涉及复制相对大量的内存。然而,`exchange()`的功能可以通过仅仅交换内部的`data`指针来取而代之,就好像在其成员函数`exchangeWith()`中所作的那样。 42 | 43 | ### 16.1.1 透明的客制化 44 | 45 | 在前例中,成员函数`exchangeWith()`提供了一个对泛型`exchange()`函数的一个高效替换体,但是这样一来,就需要使用一个不同的函数,而这会在以下几个方面给我们带来不便: 46 | 1. `Array`类的使用者不得不记住这一额外接口,并且必须在可以使用时万分小心。 47 | 2. 泛型算法通常无法区分多种变体。例如: 48 | ```cpp 49 | template 50 | void genericAlgorithm(T* x, T* y) 51 | { 52 | ... 53 | exchange(x, y); // How do we select the right algorithm? 54 | ... 55 | } 56 | ``` 57 | 58 | 基于这些考虑,C++模板提供了透明地客制化函数模板和类模板的方法。对函数模板来说,可以通过重载机制来达成。例如,我们可以编写一个重载的`quickExchange()`函数模板集合,如下所示: 59 | ```cpp 60 | template 61 | void quickExchange(T* a, T* b) // #1 62 | { 63 | T tmp(*a); 64 | *a = *b; 65 | *b = tmp; 66 | } 67 | 68 | template 69 | void quickExchange(Array* a, Array* b) // #2 70 | { 71 | a->exchangeWith(b); 72 | } 73 | 74 | void demo(Array* p1, Array* p2) 75 | { 76 | int x = 42, y = -7; 77 | quickExchange(&x, &y); // uses #1 78 | quickExchange(p1, p2); // uses #2 79 | } 80 | ``` 81 | 82 | 第一处`quickExchange()`的调用有两个类型为`int*`的实参,因此只有第一个模板才能推导成功,`T`由`int`替换。因此对于哪个函数应该被调用,毫无疑问。第二处调用则恰恰相反,它可以同时匹配上面的两个模板:第一个模板使用`Array`替换`T`,第二个模板使用`int`替换`T`。另一方面,在两个函数替换的结果中,参数类型都是严格匹配调用实参的。通常来说,这应该得出一个调用有歧义的结论,但是相对于第一个模板来说,C++语言认为第二个模板“更加特化”。在其他方面都等同的场合,重载决议会倾向于选择更加特化的模板,因此这里会选择#2。 83 | 84 | ### 16.1.2 语义透明性 85 | 86 | 上一节中重载的使用,对达成透明客制化的实例化过程来说非常有用,但是有一点需要铭记:该“透明性”非常非常依赖于实现体的细节。为了厘清这一点,来看看我们的`quickExchange()`解决方案。尽管泛型算法和为`Array`类型客制化的算法最后都可以交换指针所指向的值,但是二者各自所带来的副作用却是截然不同的。下面的代码通过对比交换结构对象和交换`Array`对象的值这两种行为,解释得生动形象: 87 | ```cpp 88 | struct S { 89 | int x; 90 | } s1, s2; 91 | 92 | void distinguish(Array a1, Array a2) 93 | { 94 | int* p = &a1[0]; 95 | int* q = &s1.x; 96 | a1[0] = s1.x = 1; 97 | a2[0] = s2.x = 2; 98 | quickExchange(&a1, &a2); // *p == 1 after this(still) 99 | quickExchange(&s1, &s2); // *q == 2 after this 100 | } 101 | ``` 102 | 103 | 如示例所展示,在调用`quick_exchange()`后,指向第1个`Array`的指针`p`变成了指向第2个`Array`的指针(即使值没有改变);然而,指向非`Array`(即`struct S`)`s1`的指针在交换操作执行之后,仍然指向`s1`,只是指针所指向的值发生了交换。这种差别足够显著,可能会让模板实现的客户端感到困惑。前缀`quick_`将焦点聚焦到这一事实:为了实现所期待的操作,可以走捷径。然而,原始的泛型`exchange()`模板也可以对`Array`进行一个有效的优化: 104 | ```cpp 105 | template 106 | void exchange(Array* a, Array* b) 107 | { 108 | T* p = &(*a)[0]; 109 | T* q = &(*b)[0]; 110 | for (std::size_t k = a->size(); k-- != 0; ) { 111 | exchange(p++, q++); 112 | } 113 | } 114 | ``` 115 | 116 | 对泛型代码来说,这一版本的优势在于不再需要额外的大尺寸临时`Array`对象。`exchange()`模板会被递归地调用,因此对于诸如`Array>`这样的类型来说,可以获得更好的性能。同时也注意到模板的更加特化的版本并没有声明`inline`,这是因为它本身会做很多的递归操作,相对而言,原始的泛型实现体声明了`inline`,因为它仅仅执行了少数的几个操作(每一个操作可能都很昂贵)。 117 | 118 | ## 16.2 函数模板重载 119 | 120 | 在前面的章节中我们已经看到了两个同名函数模板可以共存,尽管它们可能会实例化出相同的参数类型。这里还有一个简单的例子: 121 | *details/funcoverload1.hpp* 122 | ```cpp 123 | template 124 | int f(T) 125 | { 126 | return 1; 127 | } 128 | 129 | template 130 | int f(T*) 131 | { 132 | return 2; 133 | } 134 | ``` 135 | 136 | 当第一个模板使用`int*`替换`T`、第二个模板使用`int`替换`T`时,二者就会得到一个参数类型(以及返回类型)完全相同的函数。不仅是这些模板可以共存,就连它们各自的实例化体也可以共存(即使它们有相同的参数和返回类型)。 137 | 138 | 下例展示了像这样生成的两个函数要如何使用显式模板实参语法来调用: 139 | *details/funcoverload1.cpp* 140 | ```cpp 141 | #include 142 | #include "funcoverload1.hpp" 143 | 144 | int main() 145 | { 146 | std::cout << f((int*)nullptr); // calls f(T) 147 | std::cout << f((int*)nullptr); // calls f(T*) 148 | } 149 | ``` 150 | 151 | 该程序输出如下: 152 | ``` 153 | 12 154 | ``` 155 | 156 | 为了解释这一结果,我们来详细分析一下`f((int*)nullptr)`的调用。`f()`表示我们想要用`int*`来替换`f()`模板的第一个参数,此时无需依赖模板实参推导。本例中有多个模板`f()`,因此得以创造一个包含两个函数的重载集合,这两个函数通过模板`f(int*)`(由第一个模板生成)和`f(int**)`(由第二个模板生成)生成。调用实参`(int*)nullptr`的类型为`int*`。这仅仅与第一个模板生成的函数匹配,因此最终调用的就是该函数。 157 | 158 | 相对而言,第二个调用所创造的重载集合中包含了`f(int)`(由第一个模板生成)和`f(int*)`(由第二个模板生成),其中第二个模板是匹配的。 159 | 160 | ### 16.2.1 签名 161 | 162 | 两个函数如果拥有不同的签名,那么就可以在一个程序中共存。函数签名被定义为以下信息: 163 | 1. 函数的非限定名称(或者生成该函数的函数模板名称)。 164 | 2. 函数名称所属的类或命名空间作用域,并且如果函数名称拥有内部链接,还包括该名称声明所在的编译单元。 165 | 3. 函数的`const`、`volatile`或`const volatile`限定(前提是具有这样一个限定符的成员函数) 166 | 4. 函数的`&`或`&&`限定(前提是具有这样一个限定符的成员函数) 167 | 5. 函数参数的类型(如果函数是从函数模板中生成的,那么指的是替换前的模板参数) 168 | 6. 如果函数是从函数模板中生成,则包括它的函数返回类型 169 | 7. 如果函数是从函数模板中生成,则包括模板参数和模板实参 170 | 171 | 这意味着下面的模板和它们的实例化体可以在同一个程序中共存: 172 | ```cpp 173 | template 174 | void f1(T1, T2); 175 | 176 | template 177 | void f1(T2, T1); 178 | 179 | template 180 | long f2(T); 181 | 182 | template 183 | char f2(T); 184 | ``` 185 | 186 | 然而,当它们定义在相同的作用域中时,它们并不能总被使用,这是因为实例化会产生重载歧义。例如,调用`f2(42)`对于上面声明的模板来说显然会产生歧义。另一个例子在下面演示: 187 | ```cpp 188 | #include 189 | 190 | template 191 | void f1(T1, T2) 192 | { 193 | std::cout << "f1(T1, T2)\n"; 194 | } 195 | 196 | template 197 | void f1(T2, T1) 198 | { 199 | std::cout << "f1(T2, T1)\n"; 200 | } 201 | // fine so far 202 | 203 | int main() 204 | { 205 | f1('a', 'b'); // ERROR: ambiguous 206 | } 207 | ``` 208 | 209 | 这里,函数`f1(T1, T2)`可以与函数`f1(T2, T1)`共存,但是重载决议永远无法抉择出哪一个更合适。如果模板在不同的编译单元中出现,这两个实例化体实际上可以在同一个程序中共存(并且,链接器不应该抱怨重复的定义,这是因为实例化体的签名是有所区别的): 210 | 211 | ```cpp 212 | // translation unit 1: 213 | #include 214 | 215 | template 216 | void f1(T1, T2) 217 | { 218 | std::cout << "f1(T1, T2)\n"; 219 | } 220 | 221 | void g() 222 | { 223 | f1('a', 'b'); 224 | } 225 | 226 | // translation unit 2: 227 | #include 228 | 229 | template 230 | void f1(T2, T1) 231 | { 232 | std::cout << "f1(T2, T1)\n"; 233 | } 234 | extern void g(); // defined in translation unit 1 235 | 236 | int main() 237 | { 238 | f1('a', 'b'); 239 | g(); 240 | } 241 | ``` 242 | 243 | 该程序是有效的,它的输出如下: 244 | ``` 245 | f1(T2, T1) 246 | f1(T1, T2) 247 | ``` 248 | 249 | ### 16.2.2 重载的函数模板的偏序 250 | 251 | 再次考虑一下先前的例子:我们发现在替换了给定的模板实参列表后(``和``),重载决议最终会选择最合适的函数并进行调用: 252 | ```cpp 253 | std::cout << f((int*)nullptr); // calls f(T) 254 | std::cout << f((int*)nullptr); // calls f(T*) 255 | ``` 256 | 257 | 然而,即使显式模板实参没有提供,函数依然会做出这样的选择。本例中,模板实参推导发挥了作用。让我们稍微修改一下`main()`函数来讨论这一机制: 258 | *details/funcoverload2.cpp* 259 | ```cpp 260 | #include 261 | 262 | template 263 | int f(T) 264 | { 265 | return 1; 266 | } 267 | 268 | template 269 | int f(T*) 270 | { 271 | return 2; 272 | } 273 | 274 | int main() 275 | { 276 | std::cout << f(0); // calls f(T) 277 | std::cout << f(nullptr); // calls f(T) 278 | std::cout << f((int*)nullptr); // calls f(T*) 279 | } 280 | ``` 281 | 282 | 考虑第一处调用`f(0)`:实参的类型是`int`,如果我们用`int`替换`T`,那么它与第一个模板的参数类型匹配。然而,第二个模板的参数类型始终都是一个指针,因此,在推导之后,对于该调用来说只会从第一个模板生成一个唯一的实例作为候选。对这一情景来说,重载决议是多余的。 283 | 284 | 对于第二处调用`f(nullptr)`来说也类似:实参类型是`std::nullptr_t`,同样地,它也仅与第一个模板匹配。 285 | 286 | 第三处调用`f((int*)nullptr)`比较有意思:实参推导对于两个模板来说都会成功,产生函数`f(int*)`和`f(int*)`。从传统的重载决议视角来看,这两个使用`int*`实参的函数同等优秀,如此理应指出调用存在歧义(参考附录C)。然而,在这一案例中,额外的重载决议发挥了作用:更加特化的模板所生成的函数会被选择。在这里,第二个模板被认为是更加特化的,因此该代码示例的输出就是`112`。 287 | 288 | ### 16.2.3 正规的排序规则 289 | 290 | 在上例中,我们可以很直观地看出第二个模板比第一个模板更加特化,这是因为第一个模板可以适配各种类型的实参,而第二个则只能容纳指针类型。然而,其他的例子可能没那么直观。下面我们来描述如何确定一个函数模板是否比另一个重载模板更特化的确切过程。请注意如下的偏序规则:有可能在给定两个模板时。它们俩都无法被认定比对方更特别。如果重载决议必须从这样的两个模板中选择一个,那么将无法做出决定,程序会产生有歧义错误。 291 | 292 | 假设我们正在比较两个名称相同的函数模板,这些模板对于给定的函数调用似乎可行。重载决议如下判定: 293 | - 函数调用参数中没有被使用的默认实参和省略号参数在后续将不被纳入考虑。 294 | - 然后,通过以下方式替换每一个模板实参,为两个模板合成各自的实参类型列表(对类型转换函数模板来说,还包括了返回类型): 295 | 1. 使用唯一的虚构类型替换每一个模板类型参数。 296 | 2. 使用唯一的虚构类模板替换每一个模板模板参数。 297 | 3. 使用适当类型的唯一虚构值替换每一个非类型模板参数。(虚构出的类型、模板和值在这一上下文中都与任何其他的类型、模板或值不同,这些其他的类型、模板或值要么是开发者使用的,要么是编译器在其他上下文中合成的。) 298 | - 如果第二个模板对于第一份合成出来的实参类型列表可以进行成功的实参推导(能够进行精确的匹配),而反过来却不行(即第一个模板对第二份实参类型列表无法推导成功),那么我们就认为第一个模板要比第二个模板更加特化。相反地,如果第一个模板对于第二份实参类型列表可以精确匹配而推导成功,反过来则不行,那么我们就认为第二个模板比第一个模板更加特化。否则(要么无法推导成功,要么两个都成功),两个模板之间就没有顺序可言。让我们将此应用于前例的两个模板之上,使得这一概念更加具体。我们从这两个模板构造出两个实参类型列表,按此前描述的那样替换其模板参数:(`A1`)和(`A2*`)(`A1`和`A2`是不同的构造出的类型)。显然,第一个模板对于第二个实参类型列表可以成功推导(将`A2*`替换`T`)。然而,对于第二个模板来说,没有办法让`T*`匹配第一个实参类型列表中的非指针类型`A1`。因此,我们得出第二个模板比第一个模板更加特化。 299 | 300 | 让我们来看一个更加错综复杂的例子,它涉及了多个函数参数: 301 | ```cpp 302 | template 303 | void t(T*, T const* = nullptr, ...); 304 | 305 | template 306 | void t(T const*, T*, T* = nullptr); 307 | 308 | void example(int* p) 309 | { 310 | t(p, p); 311 | } 312 | ``` 313 | 首先,由于实际调用没有使用第一个模板的省略号参数和第二个模板的最后一个参数(由默认实参填充),故这些参数会在排序时被忽略。此外,注意到第一个模板的默认实参没有被用到,因此参与到排序中的是其对应的参数(即与之匹配的调用实参)。 314 | 315 | 合成的两份实参列表分别是(`A1*, A1 const*`)和(`A2 const*, A2*`)。对于第二个模板来说,实参列表(`A1*, A1 const*`)可以成功推导(`A1 const`替换`T`),但是得到的结果并不能严格匹配,因为当用(`A1*, A1 const*`)类型的实参来调用`t(A1 const*, A1 const*, A1 const* = 0)`的时候,需要进行限定符的调整(即`const`)。类似地,第一个模板对于实参类型列表(`A2 const*, A2*`)也不能获得精确的匹配。因此,这两个模板之间并没有顺序关系,该调用存在歧义。 316 | 317 | 这种正规的排序规则通常都能产生符合直观的函数模板选择。然而,该原则偶尔也会产生不符合直觉选择的例子。因此,将来可能会修改某些规则,从而适用于所有例子。 318 | 319 | ### 16.2.4 模板和非模板 320 | 321 | 函数模板可以被非模板函数所重载。在选择实际调用的函数时,非模板函数将更为优先,除此之外没有什么其他区别。下面的例子说明了这一事实: 322 | *details/nontmpl1.cpp* 323 | ```cpp 324 | #include 325 | #include 326 | 327 | template 328 | std::string f(T) 329 | { 330 | return "Template"; 331 | } 332 | 333 | std::string f(int&) 334 | { 335 | return "Nontemplate"; 336 | } 337 | 338 | int main() 339 | { 340 | int x = 7; 341 | std::cout << f(x) << '\n'; // prints: Nontemplate 342 | } 343 | ``` 344 | 345 | 程序会输出`Nontemplate`。 346 | 347 | 然而,当`const`和引用限定符不同时,重载决议的优先级会有所变更。例如: 348 | *details/nontmpl2.cpp* 349 | ```cpp 350 | #include 351 | #include 352 | 353 | template 354 | std::string f(T&) 355 | { 356 | return "Template"; 357 | } 358 | 359 | std::string f(int const&) 360 | { 361 | return "Nontemplate"; 362 | } 363 | 364 | int main() 365 | { 366 | int x = 7; 367 | std::cout << f(x) << '\n'; // prints: Template 368 | int const c = 7; 369 | std::cout << f(c) << '\n'; // prints: Nontemplate 370 | } 371 | ``` 372 | 373 | 程序会输出: 374 | ``` 375 | Template 376 | Nontemplate 377 | ``` 378 | 379 | 现在,当我们传递非常量`int`参数时,函数模板`f<>(T&)`是一个更合适的选择。原因在于对于`int`来说,`f<>(int&)`实例化体要比`f(int const&)`更合适。因此,这一差异不仅仅在于以下事实:一个函数是模板,而另一个函数不是模板。在这种情况下,实际应用到的是通用的重载决议规则(参考P682节C.2)。只有当使用`int const`调用`f()`时,两个函数的签名才会有相同的类型——`int const&`,而此时才会优先选择非模板函数。 380 | 381 | 出于这一原因,按下面的方式声明成员函数模板是个不错的主意: 382 | ```cpp 383 | template 384 | std::string f(T const&) 385 | { 386 | return "Template"; 387 | } 388 | ``` 389 | 390 | 只不过,当定义成员函数接受与拷贝或移动构造函数相同的实参时,这种效果很容易发生意外并引起出人意料的行为。例如: 391 | *details/tmplconstr.cpp* 392 | ```cpp 393 | #include 394 | #include 395 | 396 | class C { 397 | public: 398 | C() = default; 399 | 400 | C(C const&) { 401 | std::cout << "copy constructor\n"; 402 | } 403 | 404 | C(C&&) { 405 | std::cout << "move constructor\n"; 406 | } 407 | 408 | template 409 | C(T&&) { 410 | std::cout << "template constructor\n"; 411 | } 412 | }; 413 | 414 | int main() 415 | { 416 | C x; 417 | C x2{x}; // prints: template constructor 418 | C x3{std::move(x)}; // prints: move constructor 419 | C const c; 420 | C x4{c}; // prints: copy constructor 421 | C x5{std::move(c)}; // prints: template constructor 422 | } 423 | ``` 424 | 425 | 程序输出如下: 426 | ``` 427 | template constructor 428 | move constructor 429 | copy constructor 430 | template constructor 431 | ``` 432 | 433 | 因此,成员函数模板要比`C`的拷贝构造函数更合适。而对于`std::move(c)`来说,它会产生`C const&&`类型(这是一种可行的类型,但是在语法上通常没有什么意义),成员函数模板此时也比移动构造函数更合适。 434 | 435 | 因此,通常当这些成员函数模板可能会屏蔽拷贝或移动构造函数时,必须部分地禁用它们。这在P99节6.4中解释过。 436 | 437 | ### 16.2.5 可变函数模板 438 | 439 | 可变函数模板(参考P200节12.4)在进行排序时需要被特殊对待,这是因为对参数包的推导(见P275节15.5)过程是将多个实参匹配到单一参数。这一行为对函数模板排序来说引入了各种有趣的场景,我们通过下例来展示: 440 | *details/variadicoverload.cpp* 441 | ```cpp 442 | #inclue 443 | 444 | template 445 | int f(T*) 446 | { 447 | return 1; 448 | } 449 | 450 | template 451 | int f(Ts…) 452 | { 453 | return 2; 454 | } 455 | 456 | template 457 | int f(Ts*…) 458 | { 459 | return 3; 460 | } 461 | 462 | int main() 463 | { 464 | std::cout << f(0, 0.0); // calls f<>(Ts...) 465 | std::cout << f((int*)nullptr, (double*)nullptr); // calls f<>(Ts*...) 466 | std::cout << f((int*)nullptr); // calls f<>(T*) 467 | } 468 | ``` 469 | 470 | 上例输出的结果是`231`,我们随后会进行讨论。 471 | 472 | 对第一个调用`f(0, 0.0)`来说,每个名称为`f`的函数模板都会被考虑:第一个函数模板`f(T*)`推导会失败,这一方面是因为模板参数`T`无法被成功推导,另一方面是因为实参的个数多于该非可变模板参数的个数;第二个函数模板`f(Ts...)`是可变模板,推导过程会针对两个实参的类型(分别是`int`和`double`)与函数参数包`(Ts)`的样式进行比较,将`Ts`推导为序列(`int`, `double`);对于第三个函数模板——`f(Ts*...)`,推导过程会将每个实参类型与函数参数包`Ts*`的样式进行比较,该推导会失败(`Ts`无法被推导出来)。因此,最终只有第二个函数模板是可行的,也就不需要函数模板的顺序。 473 | 474 | 第二个调用——`f((int*)nullptr, (double*)nullptr)`更加有趣:对第一个函数模板的推导会失败,因为实参个数多于模板参数个数;对第二个和第三个模板来说推导都会成功,我们显式地写出推导结果如下: 475 | ```cpp 476 | f((int*)nullptr, (double*)nullptr) // for second template 477 | 478 | f((int*)nullptr, (double*)nullptr) // for third template 479 | ``` 480 | 481 | 排序规则会考虑第二个和第三个模板,它们都是这样的可变模板:当对可变模板应用P331节16.2.3中描述的正规的排序规则时,每个模板参数包都会由一个单一构造的类型、类模板或是值来替代。举例来说,第二个和第三个函数模板所合成的实参类型分别为`A1`和`A2*`,其中`A1`和`A2`都是唯一的构造出的类型。第二个模板对于第三个模板的实参类型列表可以推导成功(通过替换参数包`Ts`为单一元素序列(`A2*`))。然而,无论如何构造`Ts*`的样式,第三个模板参数包始终无法匹配非指针类型`A1`,因此第三个函数模板(接受指针类型实参)要比第二个函数模板(接受任意实参)更加特化。 482 | 483 | 第三个调用——`f((int*)nullptr)`,又荡起了一层涟漪:三个函数模板的推导都是成功的,因此就需要给非可变参数模板和可变参数模板排排顺序。为了说明,我们比较第一个和第三个函数模板。这里,合成的实参类型分别是`A1*`和`A2*`,其中`A1*`和`A2*`都是唯一的构造出的类型。第一个模板对于第三个合成的实参列表可以推导成功(通过替换`T`为`A2`)。反过来,第三个模板对于第一个合成的实参列表也可以推导成功(通过替换参数包`Ts`为单一元素序列(`A1`))。第一个和第三个模板之间的顺序可能会产生有歧义的结果。然而,还有这样一条特殊的规则:它禁止了那些源于函数参数包(例如,第三个模板参数包`Ts*...`)的实参去匹配一个非参数包(第一个模板参数`T*`)的参数。因此,第一个模板使用第三个合成的实参列表时推导会失败,于是我们可以认为第一个模板相比第三个模板更加特化。这一特殊的规则让非可变模板(拥有固定数量的参数)比可变模板(拥有可变数量的参数)更加特化。 484 | 485 | 前面描述的规则对发生在函数签名的类型中的包展开时有着同等用法。例如,在前面的示例中,我们可以将函数模板的每一个参数和实参包裹成一个可变类模板`Tuple`,来实现一个类似的示例而不用引入函数参数包: 486 | *details/tupleoverload.cpp* 487 | ```cpp 488 | #include 489 | 490 | template class Tuple 491 | { 492 | }; 493 | 494 | template 495 | int f(Tuple) 496 | { 497 | return 1; 498 | } 499 | 500 | template 501 | int f(Tuple) 502 | { 503 | return 2; 504 | } 505 | 506 | template 507 | int f(Tuple) 508 | { 509 | return 3; 510 | } 511 | 512 | int main() 513 | { 514 | std::cout << f(Tuple()); // calls f<>(Tuple) 515 | std::cout << f(Tuple()); // calls f<>(Tuple) 516 | std::cout << f(Tuple()); // calls f<>(Tuple) 517 | } 518 | ``` 519 | 520 | 函数模板排序时,对模板实参到`Tuple`的包展开与我们前面示例中函数包展开有着相似的考量,运行结果输出:`231`。 521 | 522 | ## 16.3 显式特化 523 | 524 | 重载函数模板并根据偏序规则来选择“最”匹配的函数模板这一能力,使得我们可以透明地对泛型实现增加特化模板来调整代码以获得更高的效率。然而,类模板和变量模板无法被重载。取而代之的是,类模板的透明客制化采用了另一种机制:显式特化。标准术语”显式特化“是指一种我们称之为“完整特化”的语言特性。它使用完全替代后的模板参数来提供一个模板实现体:没有保留任何模板参数。类模板、函数模板和变量模板都可以进行完整特化。 525 | 526 | 类模板的成员可以被定义在类定义体的外部(即,成员函数、嵌套类、静态数据成员和成员枚举类型)。 527 | 528 | 在后面的一节中,我们会描述“偏特化”。它与完整特化相似,只不过并没有完全替换模板参数而是在模板的替换中保留了一部分。完整特化和偏特化在我们的代码中都是同等“显式的”,这也是为什么我们在讨论中避开用术语”显式特化“的原因。全特化和偏特化都没有引入一个全新的模板或是模板实例。相反,这些结构为泛型模板中已经隐式声明的实例提供了替代的定义。这是一个相对重要的概念,它是与模板重载的主要区别。 529 | 530 | ### 16.3.1 类模板的完整特化 531 | 532 | 完整特化由连续的`template`,`<`和`>`语法块引导,且类名称的后面跟随着特化所声明的模板实参。下面的例子对此进行了说明: 533 | ```cpp 534 | template 535 | class S { 536 | public: 537 | void info() { 538 | std::cout << "generic (S::info())\n"; 539 | } 540 | }; 541 | 542 | template<> 543 | class S { 544 | public: 545 | void msg() { 546 | std::cout << "fully specialized (S::msg())\n"; 547 | } 548 | }; 549 | ``` 550 | 551 | 请注意看完整特化的实现,是如何无需以任何方式与泛型定义相关联的:这意味着我们可以使用不同名称的成员函数(`info`对`msg`)。二者的关联仅仅由类模板的名称所决定。 552 | 553 | 特化模板实参列表必须与模板参数列表一致。举例来说,为模板类型参数指定一个非类型值是不合法的。然而,对于有着默认模板实参的模板参数来说,对应的模板实参也是可选的: 554 | ```cpp 555 | template 556 | class Types { 557 | public: 558 | using I = int; 559 | }; 560 | 561 | template::I> 562 | class S; // #1 563 | 564 | template<> 565 | class S { // #2 566 | public: 567 | void f(); 568 | }; 569 | 570 | template<> class S; // #3 571 | 572 | template<> class S; // ERROR: 0 cannot substitute U 573 | 574 | int main() 575 | { 576 | S* pi; // OK: uses #1, no definition needed 577 | S e1; // ERROR: uses #1, but no definition available 578 | S* pv; // OK: uses #2 579 | S sv; // OK: uses #2, definition available 580 | S e2; // ERROR: uses #1, but no definition available 581 | S e3; // ERROR: uses #3, but no definition available 582 | } 583 | 584 | template<> 585 | class S { // definition for #3 586 | }; 587 | ``` 588 | 589 | 如上例所展示,完整特化的声明可以无需定义体。然而,当声明了完整特化时,泛型定义就永远不会使用这一组既定的模板实参来实例化。因此,如果程序需要某个定义但是却找不到对应的实现体时就会出错。对类模板特化来说,有时“前置声明”类型会很有用,因为这样就可以构造相互依赖的类型。完整特化声明与普通的类声明在这一方面是等同的(记住它不是模板声明),唯一的区别在于语法以及特化声明必须匹配前面的模板声明。因为这不是模板声明,完整特化类模板的成员可以使用普通的类外成员定义语法来定义(换句话说,不能指定模板前缀`template<>`): 590 | 591 | ```cpp 592 | template 593 | class S; 594 | 595 | template<> class S { 596 | public: 597 | void print() const; 598 | }; 599 | 600 | // the following definition cannot be preceded by template<> 601 | void S::print() const 602 | { 603 | std::cout << "pointer to pointer to char\n"; 604 | } 605 | ``` 606 | 607 | 一个更复杂的例子来加强理解这一概念: 608 | ```cpp 609 | template 610 | class Outside { 611 | public: 612 | template 613 | class Inside{ 614 | }; 615 | }; 616 | 617 | template<> 618 | class Outside { 619 | // there is no special connection between the following nested class 620 | // and the one defined in the generic template 621 | template 622 | class Inside { 623 | private: 624 | static int count; 625 | }; 626 | }; 627 | // the following definition cannot be preceded by template<> 628 | template 629 | int Outside::Inside::count = 1; 630 | ``` 631 | 632 | 完整特化是泛型模板的特定实例化体的替代体,并且在同一个程序中无法同时存在显式完整特化体和模板生成的实例化体这两个版本。试图在同一个文件中使用两者通常会被编译器逮捕: 633 | ```cpp 634 | template 635 | class Invalid { 636 | }; 637 | 638 | Invalid x1; // causes the instantiation of Invalid 639 | 640 | template<> 641 | class Invalid; // ERROR: Invalid already instantiated 642 | ``` 643 | 644 | 不幸的是,如果在不同的编译单元中使用,问题可能不会被轻易捕获。下面的C++代码由两个文件组成,在多个平台上编译和链接这个例子都表示它是非法的,甚至是危险的: 645 | ```cpp 646 | // Translation unit 1: 647 | template 648 | class Danger { 649 | public: 650 | enum { max = 10 }; 651 | }; 652 | 653 | char buffer[Danger ::max]; // uses generic value 654 | 655 | extern void clear(char*); 656 | 657 | int main() 658 | { 659 | clear(buffer); 660 | } 661 | 662 | // Translation unit 2: 663 | template 664 | class Danger; 665 | 666 | template<> 667 | class Danger { 668 | public: 669 | enum { max = 100 }; 670 | }; 671 | 672 | void clear(char* buf) 673 | { 674 | // mismatch in array bound: 675 | for(int k = 0; k ::max; ++k) { 676 | buf[k] = '\0'; 677 | } 678 | } 679 | ``` 680 | 681 | 显然,为了保证简洁,我们对使该示例做了裁剪,但是它说明了:在使用特化时,必须非常小心地确认特化的声明对泛型模板的所有用户都是可见的。在实际应用中,这意味着:在模板声明所在的头文件中,特化的声明通常应该在模板的声明之后。然而,泛型实现也可能来自外部源码(诸如不能被修改的头文件),尽管现实中很少采用这种方式,但还是值得我们去创建一个包含泛型模板的头文件,并让特化声明位于泛型模板之后,以避免这种“难以排查”的错误。此外,通常来说,最好避免从外部源码引入特化模板,除非明确表示设计的目的就是如此。 682 | 683 | ### 16.3.2 函数模板的完整特化 684 | 685 | 函数模板完整特化背后的语法和原则与类模板完整特化大体相同,只是加入了重载和实参推导。 686 | 687 | 如果可以借助实参推导(用实参类型来推导声明中给出的参数类型)和偏序来确定模板的特化版本,那么完整特化实现就可以忽略显式的模板实参。举个例子: 688 | ```cpp 689 | template 690 | int f(T) // #1 691 | { 692 | return 1; 693 | } 694 | 695 | template 696 | int f(T*) // #2 697 | { 698 | return 2; 699 | } 700 | 701 | template<> int f(int) // OK: specialization of #1 702 | { 703 | return 3; 704 | } 705 | 706 | template<> int f(int*) // OK: specialization of #2 707 | { 708 | return 4; 709 | } 710 | ``` 711 | 712 | 函数模板完整特化不能包含默认实参值。然而,对于被特化的模板所指定的任何默认实参,显式特化版本都可以使用这些默认实参值。例如: 713 | ```cpp 714 | template 715 | int f(T, T x = 42) 716 | { 717 | return x; 718 | } 719 | template<> int f(int, int = 35) // ERROR 720 | { 721 | return 0; 722 | } 723 | ``` 724 | 725 | (这是因为完整特化提供的是一个替换的定义,而不是一个替换的声明。在调用函数模板的时点,该调用已经完全基于函数模板而完成解析了。) 726 | 727 | 完整特化的声明和普通声明(或者是一个普通的重声明)在很多方面都很类似。特别地,它不会声明一个模板,因此对于非内联完整特化函数模板特化来说,在程序中它只能有一个定义。然而,我们必须确保:函数模板的完整特化声明需跟随在模板定义之后,以避免试图使用一个由模板生成的函数。因此,模板`g()`的声明和完整特化声明应该被组织成两个文件,如下所示: 728 | - 接口文件包含了主模板的定义和偏特化的定义,但是仅包含完整特化的声明: 729 | 730 | ```cpp 731 | #ifndef TEMPLATE_G_HPP 732 | #define TEMPLATE_G_HPP 733 | 734 | // template definition should appear in header file: 735 | template 736 | int g(T, T x = 42) 737 | { 738 | return x; 739 | } 740 | 741 | // specialization declaration inhibits instantiations of the template; 742 | // definition should not appear here to avoid multiple definition errors 743 | 744 | template<> int g(int, int y); 745 | #endif // TEMPLATE_G_HPP 746 | ``` 747 | - 相应的,实现文件包含了完整特化的定义: 748 | ```cpp 749 | #include "template_g.hpp" 750 | template<> int g(int, int y) 751 | { 752 | return y/2; 753 | } 754 | 755 | ``` 756 | 757 | 或者完整特化也可以搞成内联,此时它的定义就可以放在同一个头文件中。 758 | 759 | ### 16.3.3 变量模板的完整特化 760 | 761 | 变量模板也可以被完整特化。如今,这一语法非常直观: 762 | ```cpp 763 | template constexpr std::size_t SZ = sizeof(T); 764 | template<> constexpr std::size_t SZ = 0; 765 | ``` 766 | 767 | 显然,该完整特化可以提供一个不同于模板所产生结果的初始化器。有趣的是,变量模板特化不需要与模板的类型匹配: 768 | ```cpp 769 | template typename T::iterator null_iterator; 770 | template<> BitIterator null_iterator>; 771 | // BitIterator doesn't match T::iterator, an that is fine 772 | ``` 773 | 774 | ### 16.3.4 成员的完整特化 775 | 776 | 类模板的成员模板、普通静态数据成员、普通成员函数都可以进行完整特化。每个类模板作用域都需要一个`template<>`前缀。如果要对一个成员模板进行特化,则必须加上另一个`template<>`前缀,来说明该声明表示的是一个特化。为了厘清上述含义,我们给出下列声明: 777 | ```cpp 778 | template 779 | class Outer { // #1 780 | public: 781 | template 782 | class Inner { // #2 783 | private: 784 | static int count; // #3 785 | }; 786 | static int code; // #4 787 | void print() const { // #5 788 | std::cout << "generic"; 789 | } 790 | }; 791 | template 792 | int Outer::code = 6; // #6 793 | 794 | template template 795 | int Outer::Inner::count = 7; // #7 796 | 797 | template<> 798 | class Outer { // #8 799 | public: 800 | template 801 | class Inner { // #9 802 | private: 803 | static int count; //#10 804 | }; 805 | void print() const { //#11 806 | } 807 | }; 808 | ``` 809 | 810 | 泛型模板`Outer`(#1)的普通成员`code`(#4)和`print()`(#5)具有单一的类模板作用域,因此完整特化时需要一个`template<>`前缀以及一组模板实参: 811 | ```cpp 812 | template<> 813 | int Outer::code = 12; 814 | 815 | template<> 816 | void Outer::print() const 817 | { 818 | std::cout << "Outer"; 819 | } 820 | ``` 821 | 822 | 这些定义将会用于替代类`Outer`(在#4和#5处替代泛型定义),但是`Outer`的其他成员仍然会通过#1处的模板来生成。请注意,在进行了这些声明之后,不能再次提供`Outer`的显式特化。 823 | 824 | 正如函数模板完整特化那般,我们也需要一种方式来声明类模板普通成员的特化而不用去定义它(防止出现多个定义体)。尽管对于普通类的成员函数和静态数据成员而言,非定义的类外声明在C++中不被允许,但如果是针对类模板的特化成员,该声明是合法的。也就是说,前面的定义可以具有如下声明: 825 | ```cpp 826 | template<> 827 | int Outer::code; 828 | 829 | template<> 830 | void Outer::print() const; 831 | ``` 832 | 833 | 细心的读者可能会发现`Outer::code`的完整特化非定义声明看上去就是一个使用默认构造器的初始化定义。实际上也确实如此,只不过这样的声明永远会被解析成非定义声明。因此,如果静态数据成员的类型是一个只能使用默认构造函数进行初始化的类型,我们就必须采用初始化列表语法。如下示例: 834 | ```cpp 835 | class DefaultInitOnly { 836 | public: 837 | DefaultInitOnly() = default; 838 | DefaultInitOnly(DefaultInitOnly const&) = delete; 839 | }; 840 | 841 | template 842 | class Statics { 843 | private: 844 | static T sm; 845 | }; 846 | ``` 847 | 848 | 下面的语句是一个声明: 849 | ```cpp 850 | template<> 851 | DefaultInitOnly Statics::sm; 852 | ``` 853 | 854 | 如果想要一个定义并调用默认构造器: 855 | ```cpp 856 | template<> 857 | DefaultInitOnly Statics::sm{}; 858 | ``` 859 | 860 | 在C++11之前,这无法办到。对于这种特化也无法实现默认初始化。以前的经典办法是使用拷贝初始化: 861 | ```cpp 862 | template<> 863 | DefaultInitOnly Statics::sm = 864 | DefaultInitOnly(); 865 | ``` 866 | 867 | 遗憾的是,对我们的例子来说这是行不通的,因为拷贝构造器被删除了。然而,C++17引入了强制复制省略(mandatory copy-elision)法则,这一法则使得该实现合法化,因为这里实际上不会真正调用拷贝构造器。 868 | 869 | 成员模板`Outer::Inner`也可以使用特定的模板实参进行特化,对于该特化所在的外围`Outer`而言,它不会影响`Outer`相应实例化体的其他成员。同样的,由于存在一个外围模板,所以我们需要添加一个`template<>`前缀。代码应该写成下面这样: 870 | ```cpp 871 | template<> 872 | template 873 | class Outer::Inner { 874 | public: 875 | static long count; // member type changed 876 | }; 877 | 878 | template<> 879 | template 880 | long Outer::Inner::count; 881 | ``` 882 | 883 | 模板`Outer::Inner`也可以被完整特化,但只能针对某个给定的`Outer`实例。我们现在需要两个`template<>`前缀:第一个是因为外围类的存在,第二个是因为我们完整特化了内层模板: 884 | ```cpp 885 | template<> 886 | template<> 887 | class Outer::Inner { 888 | public: 889 | enum { count = 1 }; 890 | }; 891 | 892 | // the following is not valid C++; 893 | // template<> cannot follow a template parameter list 894 | template 895 | template<> class Outer::Inner; // ERROR 896 | ``` 897 | 898 | 我们可以将此与`Outer`的成员模板特化进行比较。由于后者已经进行过完整特化了,也就没有外部模板了,此时我们只需要一个`template<>`前缀: 899 | ```cpp 900 | template<> 901 | class Outer::Inner { 902 | public: 903 | enum { count = 2 }; 904 | }; 905 | ``` 906 | 907 | ## 16.4 类模板的偏特化 908 | 909 | 模板的完整特化通常很有用,但有些时候我们更希望对类模板或变量模板的模板实参族进行特化,而不是针对某个具体实参列表进行完整特化。例如,假设下面是一个类模板实现的链表: 910 | ```cpp 911 | template 912 | class List { // #1 913 | public: 914 | ... 915 | void append(T const&); 916 | inline std::size_t length() const; 917 | ... 918 | }; 919 | ``` 920 | 921 | 使用该类模板的大型项目会为多种类型实例化出它的成员。对于非内联展开的成员函数来说(即`List::append()`),这会导致对象代码的显著膨胀。然而,如果我们从一个底层视角来看,`List::append()`和`List::append()`是等同的。换句话说,我们可以指定所有的指针型`List`共享同一个实现体。尽管这无法在C++中直接表达,但我们可以指定所有的指针型`List`都从不同的模板定义中实例化,从而达成近似的目标: 922 | ```cpp 923 | template 924 | class List { // #2 925 | private: 926 | List impl; 927 | ... 928 | public: 929 | ... 930 | inline void append(T* p) { 931 | impl.append(p); 932 | } 933 | inline std::size_t length() const { 934 | return impl.length(); 935 | } 936 | ... 937 | }; 938 | ``` 939 | 940 | 在该上下文中,#1处的原始模板被称作主模板,后面的定义被称为偏特化(因为模板定义所使用的模板实参只指定了一部分)。模板参数列表声明(`template<...>`),再加上显式指定的模板实参集合(在类模板名称后,本例中是``),两者组合在一起就是偏特化语法的表征。 941 | 942 | 我们的代码中有一个问题,`List`会递归地包含相同的`List`类型。为了打破这一循环,我们可以在该偏特化之前先提供出一个完整特化: 943 | ```cpp 944 | template<> 945 | class List { // #3 946 | ... 947 | void append(void* p); 948 | inline std::size_t length() const; 949 | ... 950 | } 951 | ``` 952 | 953 | 而这之所以行得通,是因为完整特化的优先级要高于偏特化。因此,指针型`List`的所有的成员函数都会通过内联函数转发到`List`的实现体。这是一种对抗代码膨胀(C++模板经常会遇到)的有效方法。 954 | 955 | 偏特化声明的参数和实参列表存在着一些约束。下面是这些约束的部分内容: 956 | 1. 偏特化的实参必须与主模板对应的参数相匹配。 957 | 2. 偏特化的参数列表不能具有默认实参;作为替代,主类模板的默认实参会被使用。 958 | 3. 偏特化的非类型实参要么是一个非依赖型值,要么是一个普通的非类型模板参数。它们不能是更加复杂的表达式,诸如`2*N`(`N`是一个模板参数)。 959 | 4. 偏特化的模板实参列表不应该与主模板的参数列表完全相同(忽略重命名)。 960 | 5. 如果模板实参的某一个是包展开,那么它必须位于模板实参列表的最后。 961 | 962 | 用一个例子来解释这些约束: 963 | ```cpp 964 | template 965 | class S; // primary template 966 | 967 | template 968 | class S; // ERROR: parameter kind mismatch 969 | 970 | template 971 | class S; // ERROR: no default arguments 972 | 973 | template 974 | class S; // ERROR: no nontype expressions 975 | 976 | template 977 | class S; // ERROR: no significant difference from primary template 978 | 979 | template 980 | class Tuple; 981 | 982 | template 983 | class Tuple; // ERROR: pack expansion not at the end 984 | 985 | template 986 | class Tuple, Tail>; // OK: pack expansion is at the end of a nested template argument list 987 | ``` 988 | 989 | 每个偏特化和完整特化一样,都和主模板相关联。模板被使用时,编译器总是会对主模板进行查找,但接下来还会匹配调用实参和相关联特化的实参(使用模板实参推导,如15章所描述),然后确定应该选择哪一个模板实现体。与函数模板实参推导一样,SFINAE原则会在这里应用:如果在试图匹配一个偏特化时产生了无效的结构,那么特化会被默默丢弃,然后继续对下一个候选进行试验(如果可行的话)。如果找不到匹配的特化,主模板就会被选择;如果能够找到多个匹配的特化,那么就会选择“最特殊”的特化(与重载函数模板所定义的规则一样),而这其中如果无法确定“最特殊”的那一个(即存在几个特殊程度相同的特化),那么程序就会抛出有歧义的错误。 990 | 991 | 最后,我们要指出:类模板偏特化的参数个数是可以和主模板不一样的,它既可以多于主模板,也可以少于主模板。让我们再次考虑泛型模板`List`(在#1处声明)。我们已经讨论了如何优化指针型`List`的情景,但我们希望可以针对特定的成员指针类型实现这种优化。下面的代码就是针对指向成员指针的指针,来实现这种优化: 992 | ```cpp 993 | // partial specialization for any pointer-to-void* member 994 | template 995 | class List { // #4 996 | public: 997 | using ElementType = void* C::*: 998 | ... 999 | void append(ElementType pm); 1000 | inline std::size_t length() const; 1001 | ... 1002 | }; 1003 | 1004 | // partial specialization for any pointer-to-member-pointer type except 1005 | // pointer-to-void* member, which is handled earlier 1006 | // (note that this partial specialization has two template parameters, 1007 | // whereas the primary template only has one parameter) 1008 | // this specialization makes use of the prior one to achieve the 1009 | // desired optimization 1010 | template 1011 | class List { // #5 1012 | private: 1013 | List impl; 1014 | ... 1015 | public: 1016 | using ElementType = T* C::*; 1017 | ... 1018 | inline void append(ElementType pm) { 1019 | impl.append((void* C::*)pm); 1020 | } 1021 | inline std::size_t length() const { 1022 | return impl.length(); 1023 | } 1024 | ... 1025 | }; 1026 | ``` 1027 | 1028 | 除了模板参数数量不同之外,我们看到在#4处定义的公共实现本身也是一个偏特化(对于简单的指针例子,这里应该是一个完整特化),而所有其他的偏特化(#5处的声明)都是把实现委托给这个公共实现。显然,在#4处的公共实现要比#5处的实现更加特化,因此也就不会造成歧义。 1029 | 1030 | 此外,显式书写的模板实参数量与主模板的模板参数数量甚至也可能不同。这会在拥有默认模板实参以及拥有可变模板时发生: 1031 | ```cpp 1032 | template 1033 | class Tuple; // primary template 1034 | 1035 | template 1036 | class Tuple; // one-element tuple 1037 | 1038 | template 1039 | class Tuple; // tuple with two or more elements 1040 | ``` 1041 | 1042 | ## 16.5 变量模板的偏特化 1043 | 1044 | 变量模板在C++11标准的草稿中引入时,其许多方面的规范都被忽视了,其中的一些问题依然没有给出官方定论。然而,在现实中,各种编译器在实现时通常对这些问题的处理都有一致的表现。 1045 | 1046 | 这些问题中可能最令人诧异的是:标准会更倾向于偏特化变量模板,但是却并没有描述它们要如何声明或者它们意味着什么。因此,下面的内容基于实践中的C++实现(确实允许这种偏特化),而不是基于C++标准。 1047 | 1048 | 如你所愿,语法与变量模板的完整特化是类似的,除了`template<>`要被替换成实际的模板声明头,并且变量模板名称后跟随着模板实参列表必须依赖于模板参数。例如: 1049 | ```cpp 1050 | template constexpr std::size_t SZ = sizeof(T); 1051 | 1052 | template constexpr std::size_t SZ = sizeof(void*); 1053 | ``` 1054 | 1055 | 与变量模板的完整特化一样,偏特化的类型也不需要匹配主模板的类型: 1056 | ```cpp 1057 | template typename T::iterator null_iterator; 1058 | 1059 | template T* null_iterator = null_ptr; 1060 | // T* doesn't match T::iterator, and that is fine 1061 | ``` 1062 | 1063 | 变量模板偏特化可以指定的模板参数种类这一规则与类模板偏特化是相同的。类似地,为给定的具体模板实参列表选择某一个特化的规则也是相同的。 1064 | 1065 | ## 16.6 后记 1066 | 1067 | 模板完整特化是C++模板机制中一开始就有的一部分。然而,函数模板重载和类模板偏特化则出现得晚一些。第一个实现了函数模板重载的是HP的C++编译器,而第一个实现了类模板偏特化的是EDG的C++ front end编译器。本章中描述的偏序规则最早由Steve Adamczyk和John Spicer发明(这两位都是EDG的成员)。 1068 | 1069 | 模板特化可以终止模板定义的无限递归(诸如P348节16.4中出现的`List`),这一项能力长久以来可谓广为人知。然而,Erwin Unruh可能是提出模板元编程(使用模板实例化机制在编译器执行非琐碎的计算。我们会在第23章中致力于这一话题)这一有趣概念的第一人。 1070 | 1071 | 你可能想知道为什么只有类模板和变量模板可以被偏特化。实际上大都是历史成因。为函数模板定义这一机制也本该是可行的(参考第17章)。在某些方面,函数模板的重载效果与之相似,但是也存在一些细微的差异。这些差异主要与以下事实有关:在用到的时候仅需要查找主模板,随后才考虑特化,以确定哪一个实现体会被使用。相反,在进行查找时,所有的重载函数模板都必须放入一个重载集合中,它们可能源于不同的命名空间或是类。这增加了模板名称被无意中重载的可能性。 1072 | 1073 | 相反地,我们也可以想象让类模板和变量模板以某种形式重载。举个例子: 1074 | ```cpp 1075 | // invalid overloading of class templates 1076 | template class Pair; 1077 | template class Pair; 1078 | ``` 1079 | 1080 | 然而,看起来对这一机制的需求并不迫切。 -------------------------------------------------------------------------------- /.obsidian/cache: -------------------------------------------------------------------------------- 1 | {"files":{"Part II 深入模板.md":{"mtime":1596511232654.29,"size":942,"hash":"5e4c61cb911926e5cd396625d8c1eadb254e56cc05fd100eaea7a77de3dc7c28"},"README.md":{"mtime":1597635022801.2222,"size":1207,"hash":"56e12f1ff6f8e3b7449539327cc7364175ac04e92703d00431e64192eb83a370"},"第12章 深入模板基础.md":{"mtime":1602841517684.4785,"size":67452,"hash":"1cfc8a548bb432072d2c55425995e1223ad9d604d8d6661ad1bdbdf1d0f5a793"},"第13章 模板中的名称.md":{"mtime":1599638746918.6711,"size":54426,"hash":"86414b9ae404f2f11c5916871ff528a3c9dc2bd240a16efef49c96b00a9cc001"},"第14章 实例化.md":{"mtime":1600068802006.6108,"size":47792,"hash":"ef998dc023ee854299f7ccc882269b29cedc8ea8269be17ba6b399256d99c213"},"第15章 模板实参推导.md":{"mtime":1605185392289.0374,"size":74141,"hash":"887e8e343be404ea8d58f9deedf63a265ccc52497032b85c17fec6af665b70af"}},"metadata":{"5e4c61cb911926e5cd396625d8c1eadb254e56cc05fd100eaea7a77de3dc7c28":{"links":[],"embeds":[],"tags":[],"headings":[{"line":0,"heading":"深入模板","level":1}]},"56e12f1ff6f8e3b7449539327cc7364175ac04e92703d00431e64192eb83a370":{"links":[],"embeds":[],"tags":[],"headings":[{"line":0,"heading":"cpp-templates-2nd","level":1}]},"86414b9ae404f2f11c5916871ff528a3c9dc2bd240a16efef49c96b00a9cc001":{"links":[],"embeds":[],"tags":[],"headings":[{"line":0,"heading":"第13章 模板中的名称","level":1},{"line":5,"heading":"13.1 名称的分类","level":2},{"line":32,"heading":"13.2 名称查找","level":2},{"line":104,"heading":"13.2.1 ADL","level":3},{"line":156,"heading":"13.2.2 友元声明的ADL","level":3},{"line":183,"heading":"13.2.3 注入式类名称","level":3},{"line":240,"heading":"13.2.4 当前实例","level":3},{"line":286,"heading":"13.3 解析模板","level":2},{"line":291,"heading":"13.3.1 非模板中的上下文相关性","level":3},{"line":391,"heading":"13.3.2 依赖型类型名称","level":3},{"line":459,"heading":"13.3.3 依赖型模板名称","level":3},{"line":497,"heading":"13.3.4 Using声明中的依赖型名称","level":3},{"line":559,"heading":"13.3.5 ADL与显式模板实参","level":3},{"line":584,"heading":"13.3.6 依赖型表达式","level":3},{"line":645,"heading":"13.3.7 编译错误","level":3},{"line":669,"heading":"13.4 派生和类模板","level":2},{"line":672,"heading":"13.4.1 非依赖型基类","level":3},{"line":704,"heading":"13.4.2 依赖型基类","level":3},{"line":814,"heading":"13.5 后记","level":2}]},"ef998dc023ee854299f7ccc882269b29cedc8ea8269be17ba6b399256d99c213":{"links":[],"embeds":[],"tags":[],"headings":[{"line":0,"heading":"第14章 实例化","level":1},{"line":5,"heading":"14.1 On-Demand实例化","level":2},{"line":61,"heading":"14.2 延迟实例化","level":2},{"line":66,"heading":"14.2.1 部分实例化和完整实例化","level":3},{"line":100,"heading":"14.2.2 实例化组件","level":3},{"line":181,"heading":"14.3 C++实例化模型","level":2},{"line":184,"heading":"14.3.1 两阶段查找","level":3},{"line":214,"heading":"14.3.2 POI","level":3},{"line":323,"heading":"14.3.3 包含式模型","level":3},{"line":328,"heading":"14.4 几种实现方案","level":2},{"line":385,"heading":"14.4.1 贪婪实例化","level":3},{"line":399,"heading":"14.4.2 查询实例化","level":3},{"line":415,"heading":"14.4.3 迭代实例化","level":3},{"line":433,"heading":"14.5 显式实例化","level":2},{"line":466,"heading":"14.5.1 手动实例化","level":3},{"line":521,"heading":"14.5.2 显式实例化声明","level":3},{"line":548,"heading":"14.6 编译期if语句","level":2},{"line":622,"heading":"14.7 标准库中的显式实例化","level":2},{"line":640,"heading":"14.8 后记","level":2}]},"1cfc8a548bb432072d2c55425995e1223ad9d604d8d6661ad1bdbdf1d0f5a793":{"links":[],"embeds":[],"tags":[],"headings":[{"line":0,"heading":"第12章 深入模板基础","level":1},{"line":3,"heading":"12.1 参数化声明","level":2},{"line":207,"heading":"12.1.1 虚成员函数","level":3},{"line":224,"heading":"12.1.2 模板的链接","level":3},{"line":298,"heading":"12.1.3 主模板","level":3},{"line":315,"heading":"12.2 模板参数(Template Parameters)","level":2},{"line":339,"heading":"12.2.1 类型参数","level":3},{"line":353,"heading":"12.2.2 非类型参数","level":3},{"line":407,"heading":"12.2.3 模板模板参数","level":3},{"line":463,"heading":"12.2.4 模板参数包","level":3},{"line":539,"heading":"12.2.5 默认模板实参","level":3},{"line":625,"heading":"12.3 模板实参(Template Arguments)","level":2},{"line":632,"heading":"12.3.1 函数模板实参","level":3},{"line":710,"heading":"12.3.2 类型实参","level":3},{"line":727,"heading":"12.3.3 非类型实参","level":3},{"line":823,"heading":"12.3.4 模板模板实参","level":3},{"line":887,"heading":"12.3.5 等效性(equivalent)","level":3},{"line":923,"heading":"12.4 可变模板","level":2},{"line":954,"heading":"12.4.1 包展开(Pack Expansions)","level":3},{"line":1008,"heading":"12.4.2 包展开可以在哪里出现?","level":3},{"line":1064,"heading":"12.4.3 函数参数包","level":3},{"line":1113,"heading":"12.4.4 多重与嵌套包展开","level":3},{"line":1161,"heading":"12.4.5 零尺寸包展开","level":3},{"line":1184,"heading":"12.4.6 折叠表达式","level":3},{"line":1257,"heading":"12.5 友元","level":2},{"line":1262,"heading":"12.5.1 类模板的友元类","level":3},{"line":1316,"heading":"12.5.2 类模板的友元函数","level":3},{"line":1392,"heading":"12.5.3 友元模板","level":3},{"line":1416,"heading":"12.6 后记","level":2}]},"22c10c65e0e2dae841c45a2d2abef3cc163c4e85e67c34f366af1b59afc30439":{"links":[],"embeds":[],"tags":[],"headings":[{"line":0,"heading":"第15章 模板实参推导","level":1},{"line":7,"heading":"15.1 推导过程","level":2},{"line":67,"heading":"15.2 推导上下文","level":2},{"line":129,"heading":"15.3 特殊的推导情况","level":2},{"line":171,"heading":"15.4 初始化列表(initializer list)","level":2},{"line":199,"heading":"15.5 参数包","level":2},{"line":258,"heading":"15.5.1 字面量操作符模板","level":3},{"line":293,"heading":"15.6 右值引用","level":2},{"line":296,"heading":"15.6.1 引用折叠法则","level":3},{"line":335,"heading":"15.6.2 转发引用","level":3},{"line":373,"heading":"15.6.3 完美转发","level":3},{"line":415,"heading":"可变模板的完美转发","level":4},{"line":471,"heading":"15.6.4 推导惊奇之处","level":3},{"line":505,"heading":"15.7 SFINAE(Substitution Failure Is Not An Error)","level":2},{"line":540,"heading":"15.7.1 即时上下文","level":3},{"line":596,"heading":"15.8 推导的限制","level":2},{"line":599,"heading":"15.8.1 合法的实参转换","level":3},{"line":651,"heading":"15.8.2 类模板实参","level":3},{"line":667,"heading":"15.8.3 默认调用实参","level":3},{"line":708,"heading":"15.8.4 异常规范","level":3},{"line":741,"heading":"15.9 显式的函数模板实参","level":2},{"line":861,"heading":"15.10 初始化器和表达式推导","level":2},{"line":864,"heading":"15.10.1 auto类型指示符","level":3},{"line":945,"heading":"返回类型推导","level":4},{"line":984,"heading":"可推导的非类型参数","level":4},{"line":1061,"heading":"15.10.2 用decltype表示表达式的类型","level":3},{"line":1129,"heading":"15.10.3 decltype(auto)","level":3},{"line":1232,"heading":"15.10.4 auto推导的特殊情况","level":3},{"line":1361,"heading":"15.10.5 结构化绑定","level":3}]},"126925bbd941b9dc54b326bfe56a5995acd57aa07939cc7fde81cfc1bd1f6d01":{"links":[],"embeds":[],"tags":[],"headings":[{"line":0,"heading":"第15章 模板实参推导","level":1},{"line":7,"heading":"15.1 推导过程","level":2},{"line":67,"heading":"15.2 推导上下文","level":2},{"line":129,"heading":"15.3 特殊的推导情况","level":2},{"line":171,"heading":"15.4 初始化列表(initializer list)","level":2},{"line":199,"heading":"15.5 参数包","level":2},{"line":258,"heading":"15.5.1 字面量操作符模板","level":3},{"line":293,"heading":"15.6 右值引用","level":2},{"line":296,"heading":"15.6.1 引用折叠法则","level":3},{"line":335,"heading":"15.6.2 转发引用","level":3},{"line":373,"heading":"15.6.3 完美转发","level":3},{"line":415,"heading":"可变模板的完美转发","level":4},{"line":471,"heading":"15.6.4 推导惊奇之处","level":3},{"line":505,"heading":"15.7 SFINAE(Substitution Failure Is Not An Error)","level":2},{"line":540,"heading":"15.7.1 即时上下文","level":3},{"line":596,"heading":"15.8 推导的限制","level":2},{"line":599,"heading":"15.8.1 合法的实参转换","level":3},{"line":651,"heading":"15.8.2 类模板实参","level":3},{"line":667,"heading":"15.8.3 默认调用实参","level":3},{"line":708,"heading":"15.8.4 异常规范","level":3},{"line":741,"heading":"15.9 显式的函数模板实参","level":2},{"line":861,"heading":"15.10 初始化器和表达式推导","level":2},{"line":864,"heading":"15.10.1 auto类型指示符","level":3},{"line":945,"heading":"返回类型推导","level":4},{"line":984,"heading":"可推导的非类型参数","level":4},{"line":1061,"heading":"15.10.2 用decltype表示表达式的类型","level":3},{"line":1129,"heading":"15.10.3 decltype(auto)","level":3},{"line":1232,"heading":"15.10.4 auto推导的特殊情况","level":3},{"line":1361,"heading":"15.10.5 结构化绑定","level":3}]},"d42a6ad8414413a1e684dfe2748767602e58f681cd7357e5a56dbb5cf6761a69":{"links":[],"embeds":[],"tags":[],"headings":[{"line":0,"heading":"第15章 模板实参推导","level":1},{"line":7,"heading":"15.1 推导过程","level":2},{"line":67,"heading":"15.2 推导上下文","level":2},{"line":129,"heading":"15.3 特殊的推导情况","level":2},{"line":171,"heading":"15.4 初始化列表(initializer list)","level":2},{"line":199,"heading":"15.5 参数包","level":2},{"line":258,"heading":"15.5.1 字面量操作符模板","level":3},{"line":293,"heading":"15.6 右值引用","level":2},{"line":296,"heading":"15.6.1 引用折叠法则","level":3},{"line":335,"heading":"15.6.2 转发引用","level":3},{"line":373,"heading":"15.6.3 完美转发","level":3},{"line":415,"heading":"可变模板的完美转发","level":4},{"line":471,"heading":"15.6.4 推导惊奇之处","level":3},{"line":505,"heading":"15.7 SFINAE(Substitution Failure Is Not An Error)","level":2},{"line":540,"heading":"15.7.1 即时上下文","level":3},{"line":596,"heading":"15.8 推导的限制","level":2},{"line":599,"heading":"15.8.1 合法的实参转换","level":3},{"line":651,"heading":"15.8.2 类模板实参","level":3},{"line":667,"heading":"15.8.3 默认调用实参","level":3},{"line":708,"heading":"15.8.4 异常规范","level":3},{"line":741,"heading":"15.9 显式的函数模板实参","level":2},{"line":861,"heading":"15.10 初始化器和表达式推导","level":2},{"line":864,"heading":"15.10.1 auto类型指示符","level":3},{"line":945,"heading":"返回类型推导","level":4},{"line":984,"heading":"可推导的非类型参数","level":4},{"line":1061,"heading":"15.10.2 用decltype表示表达式的类型","level":3},{"line":1129,"heading":"15.10.3 decltype(auto)","level":3},{"line":1232,"heading":"15.10.4 auto推导的特殊情况","level":3},{"line":1361,"heading":"15.10.5 结构化绑定","level":3}]},"8fc827d4fe49e099aea5e8e322a4a859a8ce19402743a82db3283bdc9eb0f2ff":{"links":[],"embeds":[],"tags":[],"headings":[{"line":0,"heading":"第15章 模板实参推导","level":1},{"line":7,"heading":"15.1 推导过程","level":2},{"line":67,"heading":"15.2 推导上下文","level":2},{"line":129,"heading":"15.3 特殊的推导情况","level":2},{"line":171,"heading":"15.4 初始化列表(initializer list)","level":2},{"line":199,"heading":"15.5 参数包","level":2},{"line":258,"heading":"15.5.1 字面量操作符模板","level":3},{"line":293,"heading":"15.6 右值引用","level":2},{"line":296,"heading":"15.6.1 引用折叠法则","level":3},{"line":335,"heading":"15.6.2 转发引用","level":3},{"line":373,"heading":"15.6.3 完美转发","level":3},{"line":415,"heading":"可变模板的完美转发","level":4},{"line":471,"heading":"15.6.4 推导惊奇之处","level":3},{"line":505,"heading":"15.7 SFINAE(Substitution Failure Is Not An Error)","level":2},{"line":540,"heading":"15.7.1 即时上下文","level":3},{"line":596,"heading":"15.8 推导的限制","level":2},{"line":599,"heading":"15.8.1 合法的实参转换","level":3},{"line":651,"heading":"15.8.2 类模板实参","level":3},{"line":667,"heading":"15.8.3 默认调用实参","level":3},{"line":708,"heading":"15.8.4 异常规范","level":3},{"line":741,"heading":"15.9 显式的函数模板实参","level":2},{"line":861,"heading":"15.10 初始化器和表达式推导","level":2},{"line":864,"heading":"15.10.1 auto类型指示符","level":3},{"line":945,"heading":"返回类型推导","level":4},{"line":984,"heading":"可推导的非类型参数","level":4},{"line":1061,"heading":"15.10.2 用decltype表示表达式的类型","level":3},{"line":1129,"heading":"15.10.3 decltype(auto)","level":3},{"line":1232,"heading":"15.10.4 auto推导的特殊情况","level":3},{"line":1361,"heading":"15.10.5 结构化绑定","level":3}]},"d4332e14f8205e9d7b9bfb6f1af426d7d01787bad13113a5596584f781fbea7f":{"links":[],"embeds":[],"tags":[],"headings":[{"line":0,"heading":"第15章 模板实参推导","level":1},{"line":7,"heading":"15.1 推导过程","level":2},{"line":67,"heading":"15.2 推导上下文","level":2},{"line":129,"heading":"15.3 特殊的推导情况","level":2},{"line":171,"heading":"15.4 初始化列表(initializer list)","level":2},{"line":199,"heading":"15.5 参数包","level":2},{"line":258,"heading":"15.5.1 字面量操作符模板","level":3},{"line":293,"heading":"15.6 右值引用","level":2},{"line":296,"heading":"15.6.1 引用折叠法则","level":3},{"line":335,"heading":"15.6.2 转发引用","level":3},{"line":373,"heading":"15.6.3 完美转发","level":3},{"line":415,"heading":"可变模板的完美转发","level":4},{"line":471,"heading":"15.6.4 推导惊奇之处","level":3},{"line":505,"heading":"15.7 SFINAE(Substitution Failure Is Not An Error)","level":2},{"line":540,"heading":"15.7.1 即时上下文","level":3},{"line":596,"heading":"15.8 推导的限制","level":2},{"line":599,"heading":"15.8.1 合法的实参转换","level":3},{"line":651,"heading":"15.8.2 类模板实参","level":3},{"line":667,"heading":"15.8.3 默认调用实参","level":3},{"line":708,"heading":"15.8.4 异常规范","level":3},{"line":741,"heading":"15.9 显式的函数模板实参","level":2},{"line":861,"heading":"15.10 初始化器和表达式推导","level":2},{"line":864,"heading":"15.10.1 auto类型指示符","level":3},{"line":945,"heading":"返回类型推导","level":4},{"line":984,"heading":"可推导的非类型参数","level":4},{"line":1061,"heading":"15.10.2 用decltype表示表达式的类型","level":3},{"line":1129,"heading":"15.10.3 decltype(auto)","level":3},{"line":1232,"heading":"15.10.4 auto推导的特殊情况","level":3},{"line":1361,"heading":"15.10.5 结构化绑定","level":3}]},"4e4884584a73644f2a1ece95b45a0da53239b024b73043066f45891b5202b0a1":{"links":[],"embeds":[],"tags":[],"headings":[{"line":0,"heading":"第15章 模板实参推导","level":1},{"line":7,"heading":"15.1 推导过程","level":2},{"line":67,"heading":"15.2 推导上下文","level":2},{"line":129,"heading":"15.3 特殊的推导情况","level":2},{"line":171,"heading":"15.4 初始化列表(initializer list)","level":2},{"line":199,"heading":"15.5 参数包","level":2},{"line":258,"heading":"15.5.1 字面量操作符模板","level":3},{"line":293,"heading":"15.6 右值引用","level":2},{"line":296,"heading":"15.6.1 引用折叠法则","level":3},{"line":335,"heading":"15.6.2 转发引用","level":3},{"line":373,"heading":"15.6.3 完美转发","level":3},{"line":415,"heading":"可变模板的完美转发","level":4},{"line":471,"heading":"15.6.4 推导惊奇之处","level":3},{"line":505,"heading":"15.7 SFINAE(Substitution Failure Is Not An Error)","level":2},{"line":540,"heading":"15.7.1 即时上下文","level":3},{"line":596,"heading":"15.8 推导的限制","level":2},{"line":599,"heading":"15.8.1 合法的实参转换","level":3},{"line":651,"heading":"15.8.2 类模板实参","level":3},{"line":667,"heading":"15.8.3 默认调用实参","level":3},{"line":708,"heading":"15.8.4 异常规范","level":3},{"line":741,"heading":"15.9 显式的函数模板实参","level":2},{"line":861,"heading":"15.10 初始化器和表达式推导","level":2},{"line":864,"heading":"15.10.1 auto类型指示符","level":3},{"line":945,"heading":"返回类型推导","level":4},{"line":984,"heading":"可推导的非类型参数","level":4},{"line":1061,"heading":"15.10.2 用decltype表示表达式的类型","level":3},{"line":1129,"heading":"15.10.3 decltype(auto)","level":3},{"line":1232,"heading":"15.10.4 auto推导的特殊情况","level":3},{"line":1361,"heading":"15.10.5 结构化绑定","level":3}]},"c224086b6b2b47d410dafd86e7ffd502f9d7c2d19a43c2829fdb44c5402197d9":{"links":[],"embeds":[],"tags":[],"headings":[{"line":0,"heading":"第15章 模板实参推导","level":1},{"line":7,"heading":"15.1 推导过程","level":2},{"line":67,"heading":"15.2 推导上下文","level":2},{"line":129,"heading":"15.3 特殊的推导情况","level":2},{"line":171,"heading":"15.4 初始化列表(initializer list)","level":2},{"line":199,"heading":"15.5 参数包","level":2},{"line":258,"heading":"15.5.1 字面量操作符模板","level":3},{"line":293,"heading":"15.6 右值引用","level":2},{"line":296,"heading":"15.6.1 引用折叠法则","level":3},{"line":335,"heading":"15.6.2 转发引用","level":3},{"line":373,"heading":"15.6.3 完美转发","level":3},{"line":415,"heading":"可变模板的完美转发","level":4},{"line":471,"heading":"15.6.4 推导惊奇之处","level":3},{"line":505,"heading":"15.7 SFINAE(Substitution Failure Is Not An Error)","level":2},{"line":540,"heading":"15.7.1 即时上下文","level":3},{"line":596,"heading":"15.8 推导的限制","level":2},{"line":599,"heading":"15.8.1 合法的实参转换","level":3},{"line":651,"heading":"15.8.2 类模板实参","level":3},{"line":667,"heading":"15.8.3 默认调用实参","level":3},{"line":708,"heading":"15.8.4 异常规范","level":3},{"line":741,"heading":"15.9 显式的函数模板实参","level":2},{"line":861,"heading":"15.10 初始化器和表达式推导","level":2},{"line":864,"heading":"15.10.1 auto类型指示符","level":3},{"line":945,"heading":"返回类型推导","level":4},{"line":984,"heading":"可推导的非类型参数","level":4},{"line":1061,"heading":"15.10.2 用decltype表示表达式的类型","level":3},{"line":1129,"heading":"15.10.3 decltype(auto)","level":3},{"line":1232,"heading":"15.10.4 auto推导的特殊情况","level":3},{"line":1361,"heading":"15.10.5 结构化绑定","level":3}]},"03e75d45acfcfa259e21b4bec89a52f73ea33277de91acf1b6838d24dba34cb6":{"links":[],"embeds":[],"tags":[],"headings":[{"line":0,"heading":"第15章 模板实参推导","level":1},{"line":7,"heading":"15.1 推导过程","level":2},{"line":67,"heading":"15.2 推导上下文","level":2},{"line":129,"heading":"15.3 特殊的推导情况","level":2},{"line":171,"heading":"15.4 初始化列表(initializer list)","level":2},{"line":199,"heading":"15.5 参数包","level":2},{"line":258,"heading":"15.5.1 字面量操作符模板","level":3},{"line":293,"heading":"15.6 右值引用","level":2},{"line":296,"heading":"15.6.1 引用折叠法则","level":3},{"line":335,"heading":"15.6.2 转发引用","level":3},{"line":373,"heading":"15.6.3 完美转发","level":3},{"line":415,"heading":"可变模板的完美转发","level":4},{"line":471,"heading":"15.6.4 推导惊奇之处","level":3},{"line":505,"heading":"15.7 SFINAE(Substitution Failure Is Not An Error)","level":2},{"line":540,"heading":"15.7.1 即时上下文","level":3},{"line":596,"heading":"15.8 推导的限制","level":2},{"line":599,"heading":"15.8.1 合法的实参转换","level":3},{"line":651,"heading":"15.8.2 类模板实参","level":3},{"line":667,"heading":"15.8.3 默认调用实参","level":3},{"line":708,"heading":"15.8.4 异常规范","level":3},{"line":741,"heading":"15.9 显式的函数模板实参","level":2},{"line":861,"heading":"15.10 初始化器和表达式推导","level":2},{"line":864,"heading":"15.10.1 auto类型指示符","level":3},{"line":945,"heading":"返回类型推导","level":4},{"line":984,"heading":"可推导的非类型参数","level":4},{"line":1061,"heading":"15.10.2 用decltype表示表达式的类型","level":3},{"line":1129,"heading":"15.10.3 decltype(auto)","level":3},{"line":1232,"heading":"15.10.4 auto推导的特殊情况","level":3},{"line":1361,"heading":"15.10.5 结构化绑定","level":3}]},"3904a60a93c2b02ac9023ab6fc752c227346f2f06f23215d005b2e4d349e543e":{"links":[],"embeds":[],"tags":[],"headings":[{"line":0,"heading":"第15章 模板实参推导","level":1},{"line":7,"heading":"15.1 推导过程","level":2},{"line":67,"heading":"15.2 推导上下文","level":2},{"line":129,"heading":"15.3 特殊的推导情况","level":2},{"line":171,"heading":"15.4 初始化列表(initializer list)","level":2},{"line":199,"heading":"15.5 参数包","level":2},{"line":258,"heading":"15.5.1 字面量操作符模板","level":3},{"line":293,"heading":"15.6 右值引用","level":2},{"line":296,"heading":"15.6.1 引用折叠法则","level":3},{"line":335,"heading":"15.6.2 转发引用","level":3},{"line":373,"heading":"15.6.3 完美转发","level":3},{"line":415,"heading":"可变模板的完美转发","level":4},{"line":471,"heading":"15.6.4 推导惊奇之处","level":3},{"line":505,"heading":"15.7 SFINAE(Substitution Failure Is Not An Error)","level":2},{"line":540,"heading":"15.7.1 即时上下文","level":3},{"line":596,"heading":"15.8 推导的限制","level":2},{"line":599,"heading":"15.8.1 合法的实参转换","level":3},{"line":651,"heading":"15.8.2 类模板实参","level":3},{"line":667,"heading":"15.8.3 默认调用实参","level":3},{"line":708,"heading":"15.8.4 异常规范","level":3},{"line":741,"heading":"15.9 显式的函数模板实参","level":2},{"line":861,"heading":"15.10 初始化器和表达式推导","level":2},{"line":864,"heading":"15.10.1 auto类型指示符","level":3},{"line":945,"heading":"返回类型推导","level":4},{"line":984,"heading":"可推导的非类型参数","level":4},{"line":1061,"heading":"15.10.2 用decltype表示表达式的类型","level":3},{"line":1129,"heading":"15.10.3 decltype(auto)","level":3},{"line":1232,"heading":"15.10.4 auto推导的特殊情况","level":3},{"line":1361,"heading":"15.10.5 结构化绑定","level":3}]},"fb333d89ef9cbf13be05870b47b82f103fe01e7ef5bba797ce6233a9f7770ed8":{"links":[],"embeds":[],"tags":[],"headings":[{"line":0,"heading":"第15章 模板实参推导","level":1},{"line":7,"heading":"15.1 推导过程","level":2},{"line":67,"heading":"15.2 推导上下文","level":2},{"line":129,"heading":"15.3 特殊的推导情况","level":2},{"line":171,"heading":"15.4 初始化列表(initializer list)","level":2},{"line":199,"heading":"15.5 参数包","level":2},{"line":258,"heading":"15.5.1 字面量操作符模板","level":3},{"line":293,"heading":"15.6 右值引用","level":2},{"line":296,"heading":"15.6.1 引用折叠法则","level":3},{"line":335,"heading":"15.6.2 转发引用","level":3},{"line":373,"heading":"15.6.3 完美转发","level":3},{"line":415,"heading":"可变模板的完美转发","level":4},{"line":471,"heading":"15.6.4 推导惊奇之处","level":3},{"line":505,"heading":"15.7 SFINAE(Substitution Failure Is Not An Error)","level":2},{"line":540,"heading":"15.7.1 即时上下文","level":3},{"line":596,"heading":"15.8 推导的限制","level":2},{"line":599,"heading":"15.8.1 合法的实参转换","level":3},{"line":651,"heading":"15.8.2 类模板实参","level":3},{"line":667,"heading":"15.8.3 默认调用实参","level":3},{"line":708,"heading":"15.8.4 异常规范","level":3},{"line":741,"heading":"15.9 显式的函数模板实参","level":2},{"line":861,"heading":"15.10 初始化器和表达式推导","level":2},{"line":864,"heading":"15.10.1 auto类型指示符","level":3},{"line":945,"heading":"返回类型推导","level":4},{"line":984,"heading":"可推导的非类型参数","level":4},{"line":1061,"heading":"15.10.2 用decltype表示表达式的类型","level":3},{"line":1129,"heading":"15.10.3 decltype(auto)","level":3},{"line":1232,"heading":"15.10.4 auto推导的特殊情况","level":3},{"line":1361,"heading":"15.10.5 结构化绑定","level":3}]},"6d9d194ef088a6006a344d16002f0063413bc1ae4ab057ad93fb6f9060364fa8":{"links":[],"embeds":[],"tags":[],"headings":[{"line":0,"heading":"第15章 模板实参推导","level":1},{"line":7,"heading":"15.1 推导过程","level":2},{"line":67,"heading":"15.2 推导上下文","level":2},{"line":129,"heading":"15.3 特殊的推导情况","level":2},{"line":171,"heading":"15.4 初始化列表(initializer list)","level":2},{"line":199,"heading":"15.5 参数包","level":2},{"line":258,"heading":"15.5.1 字面量操作符模板","level":3},{"line":293,"heading":"15.6 右值引用","level":2},{"line":296,"heading":"15.6.1 引用折叠法则","level":3},{"line":335,"heading":"15.6.2 转发引用","level":3},{"line":373,"heading":"15.6.3 完美转发","level":3},{"line":415,"heading":"可变模板的完美转发","level":4},{"line":471,"heading":"15.6.4 推导惊奇之处","level":3},{"line":505,"heading":"15.7 SFINAE(Substitution Failure Is Not An Error)","level":2},{"line":540,"heading":"15.7.1 即时上下文","level":3},{"line":596,"heading":"15.8 推导的限制","level":2},{"line":599,"heading":"15.8.1 合法的实参转换","level":3},{"line":651,"heading":"15.8.2 类模板实参","level":3},{"line":667,"heading":"15.8.3 默认调用实参","level":3},{"line":708,"heading":"15.8.4 异常规范","level":3},{"line":741,"heading":"15.9 显式的函数模板实参","level":2},{"line":861,"heading":"15.10 初始化器和表达式推导","level":2},{"line":864,"heading":"15.10.1 auto类型指示符","level":3},{"line":945,"heading":"返回类型推导","level":4},{"line":984,"heading":"可推导的非类型参数","level":4},{"line":1061,"heading":"15.10.2 用decltype表示表达式的类型","level":3},{"line":1129,"heading":"15.10.3 decltype(auto)","level":3},{"line":1232,"heading":"15.10.4 auto推导的特殊情况","level":3},{"line":1361,"heading":"15.10.5 结构化绑定","level":3}]},"58c9350ff20a60874dd15e29d97f7e75aae65dce33608f889465c786665d6ec3":{"links":[],"embeds":[],"tags":[],"headings":[{"line":0,"heading":"第15章 模板实参推导","level":1},{"line":7,"heading":"15.1 推导过程","level":2},{"line":67,"heading":"15.2 推导上下文","level":2},{"line":129,"heading":"15.3 特殊的推导情况","level":2},{"line":171,"heading":"15.4 初始化列表(initializer list)","level":2},{"line":199,"heading":"15.5 参数包","level":2},{"line":258,"heading":"15.5.1 字面量操作符模板","level":3},{"line":293,"heading":"15.6 右值引用","level":2},{"line":296,"heading":"15.6.1 引用折叠法则","level":3},{"line":335,"heading":"15.6.2 转发引用","level":3},{"line":373,"heading":"15.6.3 完美转发","level":3},{"line":415,"heading":"可变模板的完美转发","level":4},{"line":471,"heading":"15.6.4 推导惊奇之处","level":3},{"line":505,"heading":"15.7 SFINAE(Substitution Failure Is Not An Error)","level":2},{"line":540,"heading":"15.7.1 即时上下文","level":3},{"line":596,"heading":"15.8 推导的限制","level":2},{"line":599,"heading":"15.8.1 合法的实参转换","level":3},{"line":651,"heading":"15.8.2 类模板实参","level":3},{"line":667,"heading":"15.8.3 默认调用实参","level":3},{"line":708,"heading":"15.8.4 异常规范","level":3},{"line":741,"heading":"15.9 显式的函数模板实参","level":2},{"line":861,"heading":"15.10 初始化器和表达式推导","level":2},{"line":864,"heading":"15.10.1 auto类型指示符","level":3},{"line":945,"heading":"返回类型推导","level":4},{"line":984,"heading":"可推导的非类型参数","level":4},{"line":1061,"heading":"15.10.2 用decltype表示表达式的类型","level":3},{"line":1129,"heading":"15.10.3 decltype(auto)","level":3},{"line":1232,"heading":"15.10.4 auto推导的特殊情况","level":3},{"line":1361,"heading":"15.10.5 结构化绑定","level":3}]},"979db36954c42d4440af879fc0b7c9553513c3bf842cacad17b3804a00100be5":{"links":[],"embeds":[],"tags":[],"headings":[{"line":0,"heading":"第15章 模板实参推导","level":1},{"line":7,"heading":"15.1 推导过程","level":2},{"line":67,"heading":"15.2 推导上下文","level":2},{"line":129,"heading":"15.3 特殊的推导情况","level":2},{"line":171,"heading":"15.4 初始化列表(initializer list)","level":2},{"line":199,"heading":"15.5 参数包","level":2},{"line":258,"heading":"15.5.1 字面量操作符模板","level":3},{"line":293,"heading":"15.6 右值引用","level":2},{"line":296,"heading":"15.6.1 引用折叠法则","level":3},{"line":335,"heading":"15.6.2 转发引用","level":3},{"line":373,"heading":"15.6.3 完美转发","level":3},{"line":415,"heading":"可变模板的完美转发","level":4},{"line":471,"heading":"15.6.4 推导惊奇之处","level":3},{"line":505,"heading":"15.7 SFINAE(Substitution Failure Is Not An Error)","level":2},{"line":540,"heading":"15.7.1 即时上下文","level":3},{"line":596,"heading":"15.8 推导的限制","level":2},{"line":599,"heading":"15.8.1 合法的实参转换","level":3},{"line":651,"heading":"15.8.2 类模板实参","level":3},{"line":667,"heading":"15.8.3 默认调用实参","level":3},{"line":708,"heading":"15.8.4 异常规范","level":3},{"line":741,"heading":"15.9 显式的函数模板实参","level":2},{"line":861,"heading":"15.10 初始化器和表达式推导","level":2},{"line":864,"heading":"15.10.1 auto类型指示符","level":3},{"line":945,"heading":"返回类型推导","level":4},{"line":984,"heading":"可推导的非类型参数","level":4},{"line":1061,"heading":"15.10.2 用decltype表示表达式的类型","level":3},{"line":1129,"heading":"15.10.3 decltype(auto)","level":3},{"line":1232,"heading":"15.10.4 auto推导的特殊情况","level":3},{"line":1361,"heading":"15.10.5 结构化绑定","level":3}]},"b5712c9f230efa2862a7eb48bacab4fa464baebe65b8876d9ebe4d711b0c1036":{"links":[],"embeds":[],"tags":[],"headings":[{"line":0,"heading":"第15章 模板实参推导","level":1},{"line":7,"heading":"15.1 推导过程","level":2},{"line":67,"heading":"15.2 推导上下文","level":2},{"line":129,"heading":"15.3 特殊的推导情况","level":2},{"line":171,"heading":"15.4 初始化列表(initializer list)","level":2},{"line":199,"heading":"15.5 参数包","level":2},{"line":258,"heading":"15.5.1 字面量操作符模板","level":3},{"line":293,"heading":"15.6 右值引用","level":2},{"line":296,"heading":"15.6.1 引用折叠法则","level":3},{"line":335,"heading":"15.6.2 转发引用","level":3},{"line":373,"heading":"15.6.3 完美转发","level":3},{"line":415,"heading":"可变模板的完美转发","level":4},{"line":471,"heading":"15.6.4 推导惊奇之处","level":3},{"line":505,"heading":"15.7 SFINAE(Substitution Failure Is Not An Error)","level":2},{"line":540,"heading":"15.7.1 即时上下文","level":3},{"line":596,"heading":"15.8 推导的限制","level":2},{"line":599,"heading":"15.8.1 合法的实参转换","level":3},{"line":651,"heading":"15.8.2 类模板实参","level":3},{"line":667,"heading":"15.8.3 默认调用实参","level":3},{"line":708,"heading":"15.8.4 异常规范","level":3},{"line":741,"heading":"15.9 显式的函数模板实参","level":2},{"line":861,"heading":"15.10 初始化器和表达式推导","level":2},{"line":864,"heading":"15.10.1 auto类型指示符","level":3},{"line":945,"heading":"返回类型推导","level":4},{"line":984,"heading":"可推导的非类型参数","level":4},{"line":1061,"heading":"15.10.2 用decltype表示表达式的类型","level":3},{"line":1129,"heading":"15.10.3 decltype(auto)","level":3},{"line":1232,"heading":"15.10.4 auto推导的特殊情况","level":3},{"line":1361,"heading":"15.10.5 结构化绑定","level":3}]},"268e42b7b89a93322b6d3fe5f9abb5347e98232f506e0762bfdad96d44bd21a8":{"links":[],"embeds":[],"tags":[],"headings":[{"line":0,"heading":"第15章 模板实参推导","level":1},{"line":7,"heading":"15.1 推导过程","level":2},{"line":67,"heading":"15.2 推导上下文","level":2},{"line":129,"heading":"15.3 特殊的推导情况","level":2},{"line":171,"heading":"15.4 初始化列表(initializer list)","level":2},{"line":199,"heading":"15.5 参数包","level":2},{"line":258,"heading":"15.5.1 字面量操作符模板","level":3},{"line":293,"heading":"15.6 右值引用","level":2},{"line":296,"heading":"15.6.1 引用折叠法则","level":3},{"line":335,"heading":"15.6.2 转发引用","level":3},{"line":373,"heading":"15.6.3 完美转发","level":3},{"line":415,"heading":"可变模板的完美转发","level":4},{"line":471,"heading":"15.6.4 推导惊奇之处","level":3},{"line":505,"heading":"15.7 SFINAE(Substitution Failure Is Not An Error)","level":2},{"line":540,"heading":"15.7.1 即时上下文","level":3},{"line":596,"heading":"15.8 推导的限制","level":2},{"line":599,"heading":"15.8.1 合法的实参转换","level":3},{"line":651,"heading":"15.8.2 类模板实参","level":3},{"line":667,"heading":"15.8.3 默认调用实参","level":3},{"line":708,"heading":"15.8.4 异常规范","level":3},{"line":741,"heading":"15.9 显式的函数模板实参","level":2},{"line":861,"heading":"15.10 初始化器和表达式推导","level":2},{"line":864,"heading":"15.10.1 auto类型指示符","level":3},{"line":945,"heading":"返回类型推导","level":4},{"line":984,"heading":"可推导的非类型参数","level":4},{"line":1061,"heading":"15.10.2 用decltype表示表达式的类型","level":3},{"line":1129,"heading":"15.10.3 decltype(auto)","level":3},{"line":1232,"heading":"15.10.4 auto推导的特殊情况","level":3},{"line":1361,"heading":"15.10.5 结构化绑定","level":3}]},"2cc752dd6d71cbf430a809a8026525433e5087403a920f083db9daedcc1505eb":{"links":[],"embeds":[],"tags":[],"headings":[{"line":0,"heading":"第15章 模板实参推导","level":1},{"line":7,"heading":"15.1 推导过程","level":2},{"line":67,"heading":"15.2 推导上下文","level":2},{"line":129,"heading":"15.3 特殊的推导情况","level":2},{"line":171,"heading":"15.4 初始化列表(initializer list)","level":2},{"line":199,"heading":"15.5 参数包","level":2},{"line":258,"heading":"15.5.1 字面量操作符模板","level":3},{"line":293,"heading":"15.6 右值引用","level":2},{"line":296,"heading":"15.6.1 引用折叠法则","level":3},{"line":335,"heading":"15.6.2 转发引用","level":3},{"line":373,"heading":"15.6.3 完美转发","level":3},{"line":415,"heading":"可变模板的完美转发","level":4},{"line":471,"heading":"15.6.4 推导惊奇之处","level":3},{"line":505,"heading":"15.7 SFINAE(Substitution Failure Is Not An Error)","level":2},{"line":540,"heading":"15.7.1 即时上下文","level":3},{"line":596,"heading":"15.8 推导的限制","level":2},{"line":599,"heading":"15.8.1 合法的实参转换","level":3},{"line":651,"heading":"15.8.2 类模板实参","level":3},{"line":667,"heading":"15.8.3 默认调用实参","level":3},{"line":708,"heading":"15.8.4 异常规范","level":3},{"line":741,"heading":"15.9 显式的函数模板实参","level":2},{"line":861,"heading":"15.10 初始化器和表达式推导","level":2},{"line":864,"heading":"15.10.1 auto类型指示符","level":3},{"line":945,"heading":"返回类型推导","level":4},{"line":984,"heading":"可推导的非类型参数","level":4},{"line":1061,"heading":"15.10.2 用decltype表示表达式的类型","level":3},{"line":1129,"heading":"15.10.3 decltype(auto)","level":3},{"line":1232,"heading":"15.10.4 auto推导的特殊情况","level":3},{"line":1361,"heading":"15.10.5 结构化绑定","level":3}]},"613fdb3556f646a5de538c4b94b63323bbfe3c5bceb87ce3e2ba64b572c6cee4":{"links":[],"embeds":[],"tags":[],"headings":[{"line":0,"heading":"第15章 模板实参推导","level":1},{"line":7,"heading":"15.1 推导过程","level":2},{"line":67,"heading":"15.2 推导上下文","level":2},{"line":129,"heading":"15.3 特殊的推导情况","level":2},{"line":171,"heading":"15.4 初始化列表(initializer list)","level":2},{"line":199,"heading":"15.5 参数包","level":2},{"line":258,"heading":"15.5.1 字面量操作符模板","level":3},{"line":293,"heading":"15.6 右值引用","level":2},{"line":296,"heading":"15.6.1 引用折叠法则","level":3},{"line":335,"heading":"15.6.2 转发引用","level":3},{"line":373,"heading":"15.6.3 完美转发","level":3},{"line":415,"heading":"可变模板的完美转发","level":4},{"line":471,"heading":"15.6.4 推导惊奇之处","level":3},{"line":505,"heading":"15.7 SFINAE(Substitution Failure Is Not An Error)","level":2},{"line":540,"heading":"15.7.1 即时上下文","level":3},{"line":596,"heading":"15.8 推导的限制","level":2},{"line":599,"heading":"15.8.1 合法的实参转换","level":3},{"line":651,"heading":"15.8.2 类模板实参","level":3},{"line":667,"heading":"15.8.3 默认调用实参","level":3},{"line":708,"heading":"15.8.4 异常规范","level":3},{"line":741,"heading":"15.9 显式的函数模板实参","level":2},{"line":861,"heading":"15.10 初始化器和表达式推导","level":2},{"line":864,"heading":"15.10.1 auto类型指示符","level":3},{"line":945,"heading":"返回类型推导","level":4},{"line":984,"heading":"可推导的非类型参数","level":4},{"line":1061,"heading":"15.10.2 用decltype表示表达式的类型","level":3},{"line":1129,"heading":"15.10.3 decltype(auto)","level":3},{"line":1232,"heading":"15.10.4 auto推导的特殊情况","level":3},{"line":1361,"heading":"15.10.5 结构化绑定","level":3},{"line":1477,"heading":"","level":2}]},"a00c7565af0ef910574a8fe57f3fab2a26f45a61d663c29e3767b0c3c21e16ea":{"links":[],"embeds":[],"tags":[],"headings":[{"line":0,"heading":"第15章 模板实参推导","level":1},{"line":7,"heading":"15.1 推导过程","level":2},{"line":67,"heading":"15.2 推导上下文","level":2},{"line":129,"heading":"15.3 特殊的推导情况","level":2},{"line":171,"heading":"15.4 初始化列表(initializer list)","level":2},{"line":199,"heading":"15.5 参数包","level":2},{"line":258,"heading":"15.5.1 字面量操作符模板","level":3},{"line":293,"heading":"15.6 右值引用","level":2},{"line":296,"heading":"15.6.1 引用折叠法则","level":3},{"line":335,"heading":"15.6.2 转发引用","level":3},{"line":373,"heading":"15.6.3 完美转发","level":3},{"line":415,"heading":"可变模板的完美转发","level":4},{"line":471,"heading":"15.6.4 推导惊奇之处","level":3},{"line":505,"heading":"15.7 SFINAE(Substitution Failure Is Not An Error)","level":2},{"line":540,"heading":"15.7.1 即时上下文","level":3},{"line":596,"heading":"15.8 推导的限制","level":2},{"line":599,"heading":"15.8.1 合法的实参转换","level":3},{"line":651,"heading":"15.8.2 类模板实参","level":3},{"line":667,"heading":"15.8.3 默认调用实参","level":3},{"line":708,"heading":"15.8.4 异常规范","level":3},{"line":741,"heading":"15.9 显式的函数模板实参","level":2},{"line":861,"heading":"15.10 初始化器和表达式推导","level":2},{"line":864,"heading":"15.10.1 auto类型指示符","level":3},{"line":945,"heading":"返回类型推导","level":4},{"line":984,"heading":"可推导的非类型参数","level":4},{"line":1061,"heading":"15.10.2 用decltype表示表达式的类型","level":3},{"line":1129,"heading":"15.10.3 decltype(auto)","level":3},{"line":1232,"heading":"15.10.4 auto推导的特殊情况","level":3},{"line":1361,"heading":"15.10.5 结构化绑定","level":3},{"line":1477,"heading":"15.10.","level":3}]},"dfb678366d879972d33650794c465093e6dd351a5f1c6af6cb0ed93cecebc820":{"links":[],"embeds":[],"tags":[],"headings":[{"line":0,"heading":"第15章 模板实参推导","level":1},{"line":7,"heading":"15.1 推导过程","level":2},{"line":67,"heading":"15.2 推导上下文","level":2},{"line":129,"heading":"15.3 特殊的推导情况","level":2},{"line":171,"heading":"15.4 初始化列表(initializer list)","level":2},{"line":199,"heading":"15.5 参数包","level":2},{"line":258,"heading":"15.5.1 字面量操作符模板","level":3},{"line":293,"heading":"15.6 右值引用","level":2},{"line":296,"heading":"15.6.1 引用折叠法则","level":3},{"line":335,"heading":"15.6.2 转发引用","level":3},{"line":373,"heading":"15.6.3 完美转发","level":3},{"line":415,"heading":"可变模板的完美转发","level":4},{"line":471,"heading":"15.6.4 推导惊奇之处","level":3},{"line":505,"heading":"15.7 SFINAE(Substitution Failure Is Not An Error)","level":2},{"line":540,"heading":"15.7.1 即时上下文","level":3},{"line":596,"heading":"15.8 推导的限制","level":2},{"line":599,"heading":"15.8.1 合法的实参转换","level":3},{"line":651,"heading":"15.8.2 类模板实参","level":3},{"line":667,"heading":"15.8.3 默认调用实参","level":3},{"line":708,"heading":"15.8.4 异常规范","level":3},{"line":741,"heading":"15.9 显式的函数模板实参","level":2},{"line":861,"heading":"15.10 初始化器和表达式推导","level":2},{"line":864,"heading":"15.10.1 auto类型指示符","level":3},{"line":945,"heading":"返回类型推导","level":4},{"line":984,"heading":"可推导的非类型参数","level":4},{"line":1061,"heading":"15.10.2 用decltype表示表达式的类型","level":3},{"line":1129,"heading":"15.10.3 decltype(auto)","level":3},{"line":1232,"heading":"15.10.4 auto推导的特殊情况","level":3},{"line":1361,"heading":"15.10.5 结构化绑定","level":3},{"line":1477,"heading":"15.10.6","level":3}]},"78461220ee45122bb2f713b0ec68ab9f9fd286e6b512498eebe6a735814a297c":{"links":[],"embeds":[],"tags":[],"headings":[{"line":0,"heading":"第15章 模板实参推导","level":1},{"line":7,"heading":"15.1 推导过程","level":2},{"line":67,"heading":"15.2 推导上下文","level":2},{"line":129,"heading":"15.3 特殊的推导情况","level":2},{"line":171,"heading":"15.4 初始化列表(initializer list)","level":2},{"line":199,"heading":"15.5 参数包","level":2},{"line":258,"heading":"15.5.1 字面量操作符模板","level":3},{"line":293,"heading":"15.6 右值引用","level":2},{"line":296,"heading":"15.6.1 引用折叠法则","level":3},{"line":335,"heading":"15.6.2 转发引用","level":3},{"line":373,"heading":"15.6.3 完美转发","level":3},{"line":415,"heading":"可变模板的完美转发","level":4},{"line":471,"heading":"15.6.4 推导惊奇之处","level":3},{"line":505,"heading":"15.7 SFINAE(Substitution Failure Is Not An Error)","level":2},{"line":540,"heading":"15.7.1 即时上下文","level":3},{"line":596,"heading":"15.8 推导的限制","level":2},{"line":599,"heading":"15.8.1 合法的实参转换","level":3},{"line":651,"heading":"15.8.2 类模板实参","level":3},{"line":667,"heading":"15.8.3 默认调用实参","level":3},{"line":708,"heading":"15.8.4 异常规范","level":3},{"line":741,"heading":"15.9 显式的函数模板实参","level":2},{"line":861,"heading":"15.10 初始化器和表达式推导","level":2},{"line":864,"heading":"15.10.1 auto类型指示符","level":3},{"line":945,"heading":"返回类型推导","level":4},{"line":984,"heading":"可推导的非类型参数","level":4},{"line":1061,"heading":"15.10.2 用decltype表示表达式的类型","level":3},{"line":1129,"heading":"15.10.3 decltype(auto)","level":3},{"line":1232,"heading":"15.10.4 auto推导的特殊情况","level":3},{"line":1361,"heading":"15.10.5 结构化绑定","level":3},{"line":1477,"heading":"15.10.6 泛型","level":3}]},"887e8e343be404ea8d58f9deedf63a265ccc52497032b85c17fec6af665b70af":{"links":[],"embeds":[],"tags":[],"headings":[{"line":0,"heading":"第15章 模板实参推导","level":1},{"line":7,"heading":"15.1 推导过程","level":2},{"line":67,"heading":"15.2 推导上下文","level":2},{"line":129,"heading":"15.3 特殊的推导情况","level":2},{"line":171,"heading":"15.4 初始化列表(initializer list)","level":2},{"line":199,"heading":"15.5 参数包","level":2},{"line":258,"heading":"15.5.1 字面量操作符模板","level":3},{"line":293,"heading":"15.6 右值引用","level":2},{"line":296,"heading":"15.6.1 引用折叠法则","level":3},{"line":335,"heading":"15.6.2 转发引用","level":3},{"line":373,"heading":"15.6.3 完美转发","level":3},{"line":415,"heading":"可变模板的完美转发","level":4},{"line":471,"heading":"15.6.4 推导惊奇之处","level":3},{"line":505,"heading":"15.7 SFINAE(Substitution Failure Is Not An Error)","level":2},{"line":540,"heading":"15.7.1 即时上下文","level":3},{"line":596,"heading":"15.8 推导的限制","level":2},{"line":599,"heading":"15.8.1 合法的实参转换","level":3},{"line":651,"heading":"15.8.2 类模板实参","level":3},{"line":667,"heading":"15.8.3 默认调用实参","level":3},{"line":708,"heading":"15.8.4 异常规范","level":3},{"line":741,"heading":"15.9 显式的函数模板实参","level":2},{"line":861,"heading":"15.10 初始化器和表达式推导","level":2},{"line":864,"heading":"15.10.1 auto类型指示符","level":3},{"line":945,"heading":"返回类型推导","level":4},{"line":984,"heading":"可推导的非类型参数","level":4},{"line":1061,"heading":"15.10.2 用decltype表示表达式的类型","level":3},{"line":1129,"heading":"15.10.3 decltype(auto)","level":3},{"line":1232,"heading":"15.10.4 auto推导的特殊情况","level":3},{"line":1361,"heading":"15.10.5 结构化绑定","level":3},{"line":1477,"heading":"15.10.6 泛型lambda","level":3}]}},"algorithmVersion":10} -------------------------------------------------------------------------------- /第12章 深入模板基础.md: -------------------------------------------------------------------------------- 1 | # 第12章 深入模板基础 2 | 3 | 在本章中,我们将深入探讨本书第一部分中介绍的一些基础知识:模板的声明,模板参数(template paramenters)的限制(restrictions),模板实参(template arguments)的限制(constraints)等等。 4 | 5 | ## 12.1 参数化声明 6 | 7 | C++目前支持4种基础模板:类模板、函数模板、变量模板以及别名模板。每一种模板都既可以出现在命名空间作用域,也可以出现在类作用域。在类作用域中,它们作为嵌套的类模板、成员函数模板、静态数据成员模板以及成员别名模板。这些模板的声明与普通类、函数、变量以及类型别名(或者是它们的类成员副本)非常相似,只不过需要一个形如`template`的子句来做前置指引。 8 | 9 | 请注意,C++17引入了另一种带有参数化子句的结构:推导指引(deduction guides)(参考P42节2.9以及P314节15.12.1)。本书中它们不被称为模板(因为它们没有被实例化),但是这一语法的选择会让人联想到函数模板。 10 | 11 | 在下一节中,我们将重返实际的模板参数声明。首先,一些示例用以说明四种类型的模板。它们可以像这样在命名空间作用域(全局或是某个命名空间内)中出现: 12 | 13 | *details/definitions1.hpp* 14 | 15 | ```cpp 16 | template // a namespace scope class template 17 | class Data { 18 | public: 19 | static constexpr bool copyable = true; 20 | … 21 | }; 22 | 23 | template // a namespace scope function template 24 | void log (T x) { 25 | … 26 | } 27 | 28 | template // a namespace scope variable template (since C++14) 29 | T zero = 0; 30 | 31 | template // a namespace scope variable template (since C++14) 32 | bool dataCopyable = Data::copyable; 33 | 34 | template // a namespace scope alias template 35 | using DataList = Data; 36 | 37 | ``` 38 | 39 | 注意到示例中,静态数据成员`Data::copyable`并不是一个变量模板,尽管它是通过类模板`Data`参数所间接参数化的。然而,变量模板可以出现在类作用域中(下一个例子会展示),彼时它将作为一个静态数据成员模板。 40 | 41 | 下面展示了定义在所属类中的4种模板,它们都是类的成员: 42 | 43 | *details/definitions2.hpp* 44 | ```cpp 45 | class Collection { 46 | public: 47 | template // an in-class member class template definition 48 | class Node { 49 | ... 50 | }; 51 | 52 | template // an in-class (and therefore implicitly inline) 53 | T* alloc() { // member function template definition 54 | ... 55 | } 56 | 57 | template // a member variable template (since c++14) 58 | static T zero = 0; 59 | 60 | template // a member alias template 61 | using NodePtr = Node*; 62 | }; 63 | ``` 64 | 65 | 请注意,在C++17中,变量(包括静态数据成员)以及变量模板都可以是内联的,内联意味着它们的定义可以跨越多个编译单元重复。对于总能定义在多个编译单元中的变量模板来说,这是多余的。但类内定义的静态数据成员不会像成员函数一样内联,因此就要指定inline关键字。 66 | 67 | 最后,下面的代码演示了如何在类外定义别名模板以外的成员模板: 68 | 69 | *details/definitions3.hpp* 70 | ```cpp 71 | template // a namespace scope class template 72 | class List { 73 | public: 74 | List() = default; // because a template constructor is defined 75 | 76 | template // another member class template, 77 | class Handle; // without its defination 78 | 79 | template // a member function template 80 | List (List const&); // (constructor) 81 | 82 | template // a member variable template (since C++14) 83 | static U zero; 84 | }; 85 | 86 | template // out-of-class member class template definition 87 | template 88 | class List::Handle { 89 | ... 90 | }; 91 | 92 | template // out-of-class member function template definition 93 | template 94 | List::List(List const& b) 95 | { 96 | ... 97 | } 98 | 99 | template // out-of-class static data member template definition 100 | template 101 | U List::zero = 0; 102 | ``` 103 | 104 | 定义在类外的成员模板需要多个`template<... >`参数化子句:每个外围作用域的类模板一个,成员模板本身也需要一个。子句从类模板最外层开始逐行展示。 105 | 106 | 同时也注意到构造器模板(一种特殊的成员函数模板)会禁用掉隐式声明的默认构造器(因为只有在没有其他构造器被声明时,默认构造器才会被声明)。增加一个默认的声明: 107 | 108 | ```cpp 109 | List() = default; 110 | ``` 111 | 112 | 这确保了`List`的实例可以通过隐式声明的默认构造器构造出来。 113 | 114 | **联合体模板** 115 | 联合体模板(union templates)也是可行的(它们被视为一种类模板): 116 | 117 | ```cpp 118 | template 119 | union AllocChunk { 120 | T object; 121 | unsigned char bytes[sizeof(T)]; 122 | }; 123 | ``` 124 | 125 | **默认调用参数** 126 | 函数模板可以有默认参数,就如同普通的函数一样: 127 | 128 | ```cpp 129 | template 130 | void report_top(Stack const&, int number = 10); 131 | 132 | template 133 | void fill(Array&, T const& = T{}); // T{} is zero for built-in types 134 | ``` 135 | 136 | 第二个声明展示了默认调用参数可以依赖于模板参数。它也可以被定义成如下形式(在C++11之前唯一可行的方式,可以参考P68节5.2): 137 | 138 | ```cpp 139 | template 140 | void fill(Array&, T const& = T()); // T() is zero for built-in types 141 | ``` 142 | 143 | 当`fill()`函数被调用时,如果传入了第二个参数,那么默认参数不会实例化。这保证了如果默认调用参数对特定`T`无法实例化的情景下不会发生错误。例如: 144 | 145 | ```cpp 146 | class Value { 147 | public: 148 | explicit Value(int); // no default constructor 149 | }; 150 | 151 | void init(Array& array) 152 | { 153 | Value zero(0); 154 | 155 | fill(array, zero); // OK: default constructor not used 156 | fill(array); // ERROR: undefined default constructor for Value is used 157 | } 158 | ``` 159 | 160 | **类模板的非模板成员** 161 | 除了类内定义的4种基础模板以外,你还可以定义普通的类成员作为类的一部分。它们有时(错误地)也称为成员模板(member templates)。尽管它们可以被参数化,但这种定义并非是第一类模板(指上述的几种模板)。它们的参数完全由成员所在的模板本身决定。例如: 162 | 163 | ```cpp 164 | template 165 | class CupBoard 166 | { 167 | class Shelf; // ordinary class in class template 168 | void open(); // ordinary function in class template 169 | enum Wood : unsigned char; // ordinary enumeration type in class template 170 | static double totalWeight; // ordinary static data member in class template 171 | }; 172 | ``` 173 | 174 | 对应的定义仅仅只是为所属的类模板指定了参数化子句,但是却并没有为成员本身指定,因为其并非是一个模板(没有参数化子句与最后一个`::`之后出现的名称相关联)。 175 | 176 | ```cpp 177 | template // definition of ordinary class in class template 178 | class CupBoard::Shelf { 179 | ... 180 | }; 181 | 182 | template // definition of ordinary function in class template 183 | void CupBoard::open() 184 | { 185 | ... 186 | } 187 | 188 | template // definition of ordinary enumeration type class in class template 189 | enum CupBoard::Wood { 190 | Maple, Cherry, Oak 191 | }; 192 | 193 | template // definition of ordinary static member in class template 194 | double CupBoard::totalWeight = 0.0; 195 | 196 | ``` 197 | 198 | C++17之后,静态成员`totalWeight`可以在类模板内部使用`inline`关键字初始化。 199 | 200 | ```cpp 201 | template 202 | class CupBoard { 203 | ... 204 | inline static double totalWeight = 0.0; 205 | }; 206 | ``` 207 | 208 | 尽管这种参数化定义通常被称作模板,但这里的“模板”一词相当不合适。对于这种情况,有一个经常被推荐的词是"temploid"。C++17之后,C++标准定义了模板化实体(a templated entity)的概念,它包括templates和temploids,以及递归地包含模板化实体中创建或定义的任何实体(这包括,例如,一个类模板内定义的友元函数(参考P30节2.4)或是模板中出现的一个lambda表达式闭包)。不管是temploid还是templated entity目前都没有产生足够的吸引力,但是在未来,需要更精准的沟通C++模板时,这些术语可能会很有用。 209 | 210 | ### 12.1.1 虚成员函数 211 | 212 | 成员函数模板不能被声明为virtual。施加这一限制是因为虚函数调用机制的通用实现会使用一个固定大小的虚表,其中存储了每一个虚函数条目(译者注:虚函数指针)。然而,成员函数模板直到整个程序被编译之前,实例化的个数都无法固定。因此,成员函数模板支持virtual需要C++编译器和链接器支持一种全新的机制。 213 | 214 | 相反的,类模板的普通成员函数可以是virtual,因为它们的数量是固定的。 215 | 216 | ```cpp 217 | template 218 | class Dynamic { 219 | public: 220 | virtual ~Dynamic(); // OK: one destructor per instance of Dynamic 221 | 222 | template 223 | virtual void copy(T2 const&); // ERROR: unknown number of instances of copy() 224 | // given an instance of Dynamic 225 | }; 226 | ``` 227 | 228 | ### 12.1.2 模板的链接 229 | 230 | 每个模板都必须有一个名字,并且该名字必须是所属作用域内独一无二的,除了函数模板重载的情景(参考第16章)。特别要注意,与类类型不同,类模板无法与不同类型的实体共享名称: 231 | 232 | ```cpp 233 | int C; 234 | ... 235 | class C; // OK: class names and nonclass names are in a different "space" 236 | 237 | int X; 238 | ... 239 | template 240 | class X; // ERROR: conflict with variable X 241 | 242 | struct S; 243 | ... 244 | template 245 | class S; // ERROR: conflict with struct S 246 | ``` 247 | 248 | 模板名称具有链接,但是他们无法拥有C链接。非标准链接可能具有某个依赖于实现体的意义(然而我们并不知道某个实现体支持模板的非标准链接与否): 249 | 250 | ```cpp 251 | extern "C++" template 252 | void normal(); // this is the default: the linkage specification could be left out 253 | 254 | extern "C" template 255 | void invalid(); // ERROR: templates cannot have C linkage 256 | 257 | extern "Java" template 258 | void javaLink(); // nonstandard, but maybe some compiler will someday 259 | // support linkage compatible with Java generics 260 | ``` 261 | 262 | 模板通常有外部链接。唯一的一些例外是命名空间作用域中具有静态限定符的函数模板、匿名空间的直接或间接的成员的模板(它们拥有内部链接)以及匿名类的成员模板(它们没有链接)。 263 | 264 | 举个例子: 265 | 266 | ```cpp 267 | template // refers to the same entity as a declaration of the 268 | void external(); // same name (and scope) in another file 269 | 270 | template // unrelated to a template with the same name in 271 | static void internal(); // another file 272 | 273 | template // redeclaration of the previous declaration 274 | static void internal(); 275 | 276 | namespace { 277 | template // also unrelated to a template with the same name 278 | void otherInternal(); // in another file, even one that similarly appears 279 | } // in an unnamed namespace 280 | 281 | namespace { 282 | template // redeclaration of the previous template declaration 283 | void otherInternal(); 284 | } 285 | 286 | struct { 287 | template void f(T) {} // no linkage: cannot be redeclared 288 | } x; 289 | ``` 290 | 291 | 注意到最后面的成员模板没有链接,它必须在匿名类定义处定义,因为想要在类外部定义是不可能的。 292 | 293 | 当前,模板无法在函数作用域或局部类作用域中声明,但是泛化的lambda可以(参考P309节15.10.6),它有一个关联的闭包类型,其中包含了成员函数模板,其可以在局部作用域中出现,这实际上意味着一种局部成员函数模板。 294 | 295 | 模板实例的链接就是模板的链接。例如,函数`internal()`从上面声明的模板`internal`实例化出来,它会拥有一个内部链接。而对于变量模板来说,这会产生一个有趣的后果。实际上,考虑下例: 296 | 297 | ```cpp 298 | template T zero = T{}; 299 | ``` 300 | 301 | `zero`所有实例化的实例都拥有一个外部链接,即使哪怕形如`zero`也是如此。这可能对既定的拥有一个内部链接的`int const zero_int = int{};`来说是违反直觉的,毕竟它使用了一个`const`类型来做修饰。同样的,模板`template int const max_volume = 11;`实例化的所有实例也都拥有外部链接,尽管那些实例同样都是类型`int const`。 302 | 303 | ### 12.1.3 主模板 304 | 305 | 模板的一般性声明声明了主模板(primary templates)。如此声明的模板在模板名后无需书写尖括号模板参数子句。 306 | 307 | ```cpp 308 | template class Box; // OK: primary template 309 | template class Box; // ERROR: does not specialize 310 | 311 | template void translate(T); // OK: primary template 312 | template void translate(T); // ERROR: not allowed for functions 313 | 314 | template constexpr T zero = T{}; // OK: primary template 315 | template constexpr T zero = T{}; // ERROR: does not specialize 316 | 317 | ``` 318 | 319 | 非主模板会在声明类模板或变量模板的偏特化时出现。这些将在第16章讨论。函数模板始终必须是主模板(参考P356节17.3,这里讨论了未来语言变化的某种潜在可能)。 320 | 321 | ## 12.2 模板参数(Template Parameters) 322 | 323 | 有三种基本类型的模板参数: 324 | 1. 类型参数(目前最常用的) 325 | 2. 非类型模板参数 326 | 3. 模板模板参数 327 | 328 | 这些基本类型的模板参数中的任何一种都可以用作模板参数包的基础(参考P188节12.2.4)。 329 | 330 | 模板参数在模板声明的参数化引导子句中声明,该声明无需命名: 331 | 332 | ```cpp 333 | template 334 | class X; // X<> is parameterized by a type and an integer 335 | ``` 336 | 337 | 当然,参数是否需要名称取决于模板后面的语句。还要注意,模板参数名可以在后续参数声明中引用(但前置则不行): 338 | 339 | ```cpp 340 | template class Buf> // in the declaration of the third one 343 | class Structure; 344 | ``` 345 | 346 | ### 12.2.1 类型参数 347 | 348 | 类型参数由关键字`typename`或`class`所引导:二者是完全等价的。关键字后必须有一个简单的标识符,并且该标识符后必须带有逗号,以表示下一个参数声明的开始,闭合的尖括号`>`用以指示参数化子句的结束,`=`用以指示一个默认模板参数的起始。 349 | 350 | 在模板声明内,类型参数的行为与类型别名(type alias)非常相似(参考P38节2.8)。例如,当`T`是模板参数时,即使`T`是被某种类(class)类型替换,也不能使用形如`class T`的详尽名称: 351 | 352 | ```cpp 353 | template 354 | class List { 355 | class Allocator* allocptr; // ERROR: use "Allocator* allocptr" 356 | friend class Allocator; // ERROR: use "friend Allocator" 357 | ... 358 | }; 359 | ``` 360 | 361 | ### 12.2.2 非类型参数 362 | 363 | 非类型模板参数表示一个可以在编译期或链接期确定的常量值。这样的参数类型(换句话说,它所代表的值类型)必须是以下之一: 364 | - 整型或枚举型 365 | - 指针类型 366 | - 成员指针类型 367 | - 左值引用类型(既可以是对象引用,也可以是函数引用) 368 | - `std::nullptr_t` 369 | - 包含`auto`或`decltype(auto)`的类型(C++17后支持;可参考P296节15.10.1) 370 | 371 | 其他类型当前都不支持(尽管浮点数在未来会被支持;可参考P356节17.2)。 372 | 373 | 也许令人惊讶的是,在某些情况下,非类型模板参数的声明也可以以关键字`typename`开头: 374 | 375 | ```cpp 376 | template // a nontype parameter 378 | class List; 379 | 380 | template // a nontype parameter of pointer type 381 | class Y; 382 | ``` 383 | 384 | 这两种情形很容易辨别,因为第一种的后面跟随了一个简单的标识符,然后是一小段标记('='用以表示默认参数,','用以指示后面的另一个模板参数,'>'用以闭合模板参数列表)。P67节5.1和P229节13.3.2对第一个非类型模板参数的关键字`typename`做出了解释(译者注:这里的`typename`是用来表示`Allocator`是`T`内的一个类型,而非静态数据成员)。 385 | 386 | 函数和数组类型可以被指定,但是它们会通过退化(decay)隐式地调整为相应的指针类型: 387 | 388 | ```cpp 389 | template class Lexer; // buf is really an int* 390 | template class Lexer; // OK: this is a redeclaration 391 | 392 | template struct FuncWrap; // fun really has pointer to 393 | // function type 394 | template struct FuncWrap; // OK: this is a redeclaration 395 | ``` 396 | 397 | 非类型模板参数的声明与变量声明非常相似,但是它们不可以有非类型指示符,比如`static`、`mutable`等等。它们可以有`const`和`volatile`限定符,但是如果这种限定符出现在参数类型的最顶层,就会被忽略(译者注:换句话说,对左值引用或指针来说支持底层const): 398 | 399 | ```cpp 400 | template class Buffer; // const is useless here 401 | template class Buffer; // same as previous declaration 402 | ``` 403 | 404 | 最后,在表达式中使用时,非引用类型的非类型参数始终都是`prvalues`(译者注:pure right values,即纯右值)。它们的地址无法被窃取,也无法被赋值。而另一方面,左值引用类型的非类型参数是可以像左值一样使用的: 405 | 406 | ```cpp 407 | template 408 | struct LocalIncrement { 409 | LocalIncrement() { Counter = Counter + 1; } // OK: reference to an integer 410 | ~LocalIncrement() { Counter = Counter - 1; } 411 | }; 412 | ``` 413 | 414 | 右值引用是不被允许的。 415 | 416 | ### 12.2.3 模板模板参数 417 | 418 | 模板模板参数是类或别名模板的占位符。它们的声明与类模板很像,但是不能使用关键字`struct`或`union`: 419 | 420 | ```cpp 421 | template class C> // OK 422 | void f(C* p); 423 | 424 | template struct C> // ERROR: struct not valid here 425 | void f(C* p); 426 | 427 | template union C> // ERROR: union not valid here 428 | void f(C* p); 429 | ``` 430 | 431 | 从C++17开始允许使用`typename`替代这里的`class`,驱使这一改动的原因在于:模板模板参数不仅可以由类模板替代,还可以由别名模板(可以实例化为任意类型)替代。因此,在C++17中,我们的上例可以改写成如下形式: 432 | 433 | ```cpp 434 | template typename C> // OK since C++17 435 | void f(C* p); 436 | ``` 437 | 438 | 在其声明的作用域内,模板模板参数用起来就像另一个类模板或是别名模板一样。 439 | 440 | 模板模板参数的参数可以有默认模板参数。在使用模板模板参数而未指定相应的参数时,这些默认参数会生效: 441 | 442 | ```cpp 443 | template class Container> 445 | 446 | class Adaptation { 447 | Container storage; // implicitly equivalent to Container 448 | ... 449 | }; 450 | ``` 451 | 452 | `T`和`A`都是模板模板参数`Container`的模板参数名称。这些名称仅可以在该模板模板参数的其他参数声明中使用。下面的模板阐释了这一概念: 453 | 454 | ```cpp 455 | template class Buf> // OK 456 | class Lexer { 457 | static T* storage; // ERROR: a template template parameter cannot be used here 458 | ... 459 | }; 460 | ``` 461 | 462 | 但是,通常在其他模板参数的声明中不需要模板模板参数的模板参数名称,因此常常根本不命名。例如,我们早期的`Adaptation`模板可以按如下声明: 463 | 464 | ```cpp 465 | template class Container> 467 | class Adaptation { 468 | Container storage; // implicitly equivalent to Container 469 | ... 470 | }; 471 | ``` 472 | 473 | ### 12.2.4 模板参数包 474 | 475 | 从C ++ 11开始,可以通过在模板参数名称之前引入省略号(…)来将任何类型的模板参数转换为模板参数包(如果模板参数匿名,那么就在模板参数名称本该出现的位置之前): 476 | 477 | ```cpp 478 | template // declares a template parameter pack named Types 479 | class Tuple; 480 | ``` 481 | 482 | 模板参数包的行为与其基础模板参数类似,但有一个关键的区别:普通的模板参数严格匹配某一个模板实参(template argument),而模板参数包可以匹配任意数量的模板实参。这意味着上面声明的`Tuple`类模板可以接受任意数量任意类型(很可能彼此不一样)的模板实参: 483 | 484 | ```cpp 485 | using IntTuple = Tuple; // OK: one template argument 486 | using IntCharTuple = Tuple; // OK: two template arguments 487 | using IntTriple = Tuple; // OK: three template arguments 488 | using EmptyTuple = Tuple<>; // OK: zero templates arguments 489 | ``` 490 | 491 | 同样,非类型参数和模板模板参数的模板参数包可以分别接受任意数量的非类型或模板模板实参,分别为: 492 | 493 | ```cpp 494 | template 495 | class MultiArray; // OK: declares a nontype template parameter pack 496 | 497 | using TransformMatrix = MultiArray; // OK: 3x3 matrix 498 | 499 | template... Containers> 500 | void testContainers(); // OK: declares a template template parameter pack 501 | ``` 502 | 503 | `MultiArray`示例需要全部的非类型模板实参均为相同的`unsigned`类型。C++17引入了非类型模板实参的推导,这将允许我们解除这一限制而做一些扩展(参考P298节15.10.1了解更多细节)。 504 | 505 | 主模板中的类模板、变量模板和别名模板至多只可以有一个模板参数包,且模板参数包必须作为最后一个模板参数。函数模板则少些限制:允许多个模板参数包,只要模板参数包后面的每个模板参数都具有默认值(请参阅下一节)或可以推导(参考第15章): 506 | 507 | ```cpp 508 | template 509 | class LastType; // ERROR: template parameter pack is not the last template parameter 510 | 511 | template 512 | void runTests(T value); // OK: template parameter pack is followed 513 | // by a deducible template parameter 514 | template struct Tensor; 515 | template 516 | auto compose(Tensor, Tensor); // OK: the tensor dimensions can be deduced 517 | ``` 518 | 519 | 最后一个例子使用了返回类型推导——C++14的特性。可以参考P296节15.10.1。 520 | 521 | 类和变量模板的偏特化声明(参考第16章)可以有多个参数包,这与主模板不同。这是因为偏特化是通过与函数模板几乎相同的推导过程所选择的。 522 | 523 | ```cpp 524 | template Typelist; 525 | template struct Zip; 526 | template 527 | struct Zip, Typelist>; 528 | // OK: partial specialization uses deduction to determine 529 | // the Xs and Ys substitutions 530 | ``` 531 | 532 | 也许不足为奇的是,类型参数包不能在其自己的参数子句中进行扩展。例如: 533 | 534 | ```cpp 535 | template struct StaticValues {}; 536 | // ERROR: Ts cannot be expanded in its own parameter list 537 | ``` 538 | 539 | 然而,嵌套模板可以实现有效的类似情景: 540 | 541 | ```cpp 542 | template struct ArgList { 543 | template struct Vals {}; 544 | }; 545 | ArgList::Vals<3, 'x', 'y'> tada; 546 | ``` 547 | 548 | 包含模板参数包的模板被称为可变参数模板(variadic template),因为它接受可变数量的模板参数。第4章和P200节12.4介绍了可变参数模板的使用。 549 | 550 | ### 12.2.5 默认模板实参 551 | 552 | 非模板参数包的任何类别的模板参数都可以配置默认参数,尽管它必须与相应的参数匹配(例如,类型参数不能有一个非类型默认实参)。默认实参不能依赖于其自身的参数,因为参数的名称直到默认实参之后才在作用域内生效。然而,他可以依赖前面的参数: 553 | 554 | ```cpp 555 | template> 556 | class List; 557 | ``` 558 | 559 | 当且仅当还为后续参数提供了默认参数时,类模板、变量模板或别名模板的模板参数才可以具有默认模板实参。(对默认函数调用参数来说有着相似的限制条件。)通常在同一模板声明中提供后续所有的默认值,但也可以在该模板的先前声明中声明它们。下面的例子可以清楚地做出解释: 560 | 561 | ```cpp 562 | template 564 | class Quintuple; // OK 565 | 566 | template 568 | class Quintuple; // OK: T4 and T5 already have defaults 569 | 570 | template 572 | class Quintuple; // ERROR: T1 cannot have a default argument 573 | // because T2 doesn't have a default 574 | ``` 575 | 576 | 函数模板的模板参数的默认模板实参,则不受这样的约束: 577 | 578 | ```cpp 579 | template 580 | R* addressof(T& value); // OK: if not explicitly specified, R will be void 581 | ``` 582 | 583 | 默认模板实参不允许重复声明: 584 | 585 | ```cpp 586 | template 587 | class Value; 588 | 589 | template 590 | class Value; // ERROR: repeated default argument 591 | ``` 592 | 593 | 许多上下文不允许使用默认模板实参: 594 | - 偏特化: 595 | ```cpp 596 | template 597 | class C; 598 | ... 599 | template 600 | class C; // ERROR 601 | ``` 602 | - 参数包: 603 | ```cpp 604 | template struct X; // ERROR 605 | ``` 606 | - 类模板成员类外定义: 607 | ```cpp 608 | template 609 | struct x 610 | { 611 | T f(); 612 | }; 613 | 614 | template // ERROR 615 | T X::f() { 616 | ... 617 | } 618 | ``` 619 | - 友元类模板声明: 620 | ```cpp 621 | struct S { 622 | template friend struct F; 623 | }; 624 | ``` 625 | - 友元函数模板声明,除非它是一个定义并且它在编译单元的其他任何地方都没有声明: 626 | ```cpp 627 | struct S{ 628 | template friend void f(); // ERROR: not a definition 629 | template friend void g() { // OK so far 630 | } 631 | }; 632 | 633 | template void g(); // ERROR: g() was given a default template argument 634 | // when defined; no other declaration may exist here 635 | ``` 636 | 637 | ## 12.3 模板实参(Template Arguments) 638 | 639 | 实例化模板时,模板实参会替换模板参数。模板实参可以被各种不同类型的机制所判定: 640 | - 显式模板实参:模板名称后可以跟随在尖括号内显式指定的模板实参。这种名称被叫做模板ID(template-id)。 641 | - 注入式类名:在具有模板参数`P1,P2 ...`的类模板`X`的作用域内,该模板(`X`)的名称可以等价于模板ID `X`。可以参考P221节13.2.3了解更多细节。 642 | - 默认模板实参:如果默认模板实参可用,则可以在模板实例化时省略显式的模板实参。然而,对于类模板或别名模板来说,即使模板参数有默认值,尖括号也不能省略(其内可以为空)。 643 | - 实参推导:没有被显式指定的函数模板参数会通过函数调用的实参类型来进行推导。在第15章对细节进行了描述。在一些其他情景中也会完成推导。如果所有的模板实参都可以被推导,那么函数模板的名称后就无需书写尖括号子句。C++17还引入了从变量声明或函数符号类型转换的初始化器中推导类模板实参的能力。可以参考P313节15.12中对此的一个探讨。 644 | 645 | ### 12.3.1 函数模板实参 646 | 647 | 函数模板的模板实参可以被显式地指定,它会按模板被使用的方式来推导,或者直接使用默认模板实参。例如: 648 | 649 | *details/max.cpp* 650 | ```cpp 651 | template 652 | T max(T a, T b) 653 | { 654 | return b < a ? a : b; 655 | } 656 | 657 | int main() 658 | { 659 | ::max(1.0, -3.0); // explicitly specify template argument 660 | ::max<1.0, -3.0); // template argument is implicitly deduce to be double 661 | ::max(1.0, 3.0); // the explicit inhibits the deduction; 662 | // hence the result has type int 663 | } 664 | ``` 665 | 666 | 某些模板实参永远不会被推导,这可能是因为它们所对应的模板参数并没有在函数参数类型中出现或是一些其他原因(参考P271节15.2)。这种参数通常应放在模板参数列表的开头,使其能被显式地指定,而于此同时也让其他参数能够完成推导。例如: 667 | 668 | *details/implicit.cpp* 669 | ```cpp 670 | template 671 | DstT implicit_cast (SrcT const& x) // SrcT can be deduced, but DstT cannot 672 | { 673 | return x; 674 | } 675 | 676 | int main() 677 | { 678 | double value = implicit_cast(-1); 679 | } 680 | ``` 681 | 682 | 如果我们反转示例中模板参数的顺序(换句话说,写成`template`),`implicit_cast`的调用就必须同时显式地指定两个参数。 683 | 684 | 此外,这样的参数不能合法地放在模板参数包之后或在偏特化中出现,因为无法明确地指定或推导它们。 685 | 686 | ```cpp 687 | template 688 | void f(double (&)[N+1], Ts... ps); // useless declaration because N 689 | // cannot be specified or deduced 690 | ``` 691 | 692 | 由于函数模板可以重载,为函数模板显式地指定所有的实参可能也无法充分指定某一个特定函数:在某些场景中,它选中了一个函数集。下面的例子阐述了这一现象: 693 | 694 | ```cpp 695 | template 696 | void apply(Func funcPtr, T x) 697 | { 698 | funcPtr(x); 699 | } 700 | 701 | template void single(T); 702 | 703 | template void multi(T); 704 | template void multi(T*); 705 | 706 | int main() 707 | { 708 | apply(&single, 3); // OK 709 | apply(&multi, 7); // ERROR: no single multi 710 | } 711 | ``` 712 | 713 | 本例中,第一个`apply()`调用可以成功是因为表达式`&single`没有歧义。如此,模板实参值`Func`就可以被轻易的推断。在第二个调用中,`&multi`可能是2种不同的类型,因此`Func`无法被推导。 714 | 715 | 更进一步,在函数模板中替换模板实参可能会导致尝试构造无效的C++类型或表达式。考虑下面的重载函数模板(`RT1`和`RT2`没有指定类型): 716 | 717 | ```cpp 718 | template RT1 test(typename T::X const*); 719 | template RT2 test(...); 720 | ``` 721 | 722 | 表达式`test`对于上述两种函数模板的前者来说都是没有意义的,因为类型`int`并没有成员类型`X`。然而,后者没有这样的问题。因此,表达式`&test`标志了一个特定函数的地址。将`int`替换第一个函数模板失败的事实并不会使表达式无效。这一SFINAE (substitution failure is not an error)原则对函数模板的重载来说是非常关键的一部分,我们会在P129节8.4和P284节节15.7中讨论。 723 | 724 | ### 12.3.2 类型实参 725 | 726 | 模板类型实参是模板类型参数的选定“值”。任何类型(包括`void`,函数类型,引用类型等等)通常来说都可以作为模板实参,但是它们对模板参数的替换构成必须是合法的: 727 | 728 | ```cpp 729 | template 730 | void clear(T p) 731 | { 732 | *p = 0; // requires that the unary * be applicable to T 733 | } 734 | 735 | int main() 736 | { 737 | int a; 738 | clear(a); // ERROR: int doesn't support the unary * 739 | } 740 | ``` 741 | 742 | ### 12.3.3 非类型实参 743 | 744 | 非类型实参是指那些替换非类型模板参数的值。这种值必须是以下其中一项: 745 | - 另一个具有正确类型的非类型模板参数。 746 | - 整型(或枚举)类型的编译器常量。只有在相应的参数具有一个匹配该类型或是一个无需塌缩(narrowing)而可以被隐式转换到该类型的值的时候才可以接受。例如,`char`值可以提供给`int`参数,但是`500`对于`char`这一8位参数来说却是无效的。 747 | - 外部变量或函数的名称,其前面带有内置的一元`&`(“取址”)运算符。对于函数和数组变量,可以省略`&`。此类模板实参与指针类型的非类型参数匹配。 C++17放宽了此要求,允许任何的常量表达式产生一个指向函数或变量的指针。 748 | - 对于引用类型的非类型参数,前一种(但不带`&`运算符)实参是有效实参。同样地,C++17在这里也放宽了约束,允许任意的常量表达式`glvalue`应用于函数或变量。 749 | - 成员指针常量;换句话说,表达式形如`&C::m`,其中`C`是类类型,`m`是非静态成员(数据或函数)。这只会匹配成员指针类型的非类型参数。同样的,在C++17中,实际的语法形式不再受限制:对匹配的成员指针常量的任何常量表达式求值都会被允许。 750 | - 空指针常量对指针或成员指针的非类型参数来说都是合法的。 751 | 752 | 对整型类型的非类型参数来说(可能也是最常用的非类型参数),到这一参数类型的隐式转换是可行的。随着C++ 11中constexpr转换函数的引入,这意味着转换前的参数可以具有类类型。 753 | 754 | C++17之前,将实参与作为指针或引用的参数进行匹配时,不会考虑用户定义的转换(单参数构造函数和转换运算符)和派生类到基类的转换,即使在其他情况下它们是有效的隐式转换。使得实参更`const`和/或更`volatile`的隐式转换是可行的。 755 | 756 | 下面是一些有效的非类型模板实参的例子: 757 | 758 | ```cpp 759 | template 760 | class C; 761 | 762 | C* c1; // integer type 763 | int a; 764 | C* c2; // address of an external variable 765 | 766 | void f(); 767 | void f(int); 768 | C* c3; // name of a function: overload resolution selects 769 | // f(int) in this case; the & is implied 770 | 771 | template void templ_func(); 772 | C>* c4; // function template instantiations are functions 773 | struct X { 774 | static bool b; 775 | int n; 776 | constexpr operator int() const { return 42; } 777 | }; 778 | 779 | C* c5; // static class members are acceptable variable/function names 780 | 781 | C* c6; // an example of a pointer-to-member constant 782 | 783 | C* c7; // OK: X is the first converted to int via a constexpr conversion 784 | // function and then to long via a standard integer conversion 785 | ``` 786 | 787 | 模板实参的一个通用限制在于编译器或链接器必须在程序构建时有能力表示它们的值。在程序运行前无法知晓的值(例如,局部变量的地址)在程序构建时与模板实例化的概念是不相容的。 788 | 789 | 尽管如此,还是有着一些常量目前是无效的,这可能会令人惊讶: 790 | - 浮点数 791 | - 字符串字面量(C++11之前,空指针常量也不行) 792 | 793 | 字符串字面量的一个问题在于两个相同的字面量可以存储在不同的地址上。对常量字符串做模板实例化有另一种迂回的方法(但麻烦),这涉及了引入一个附加变量来保存字符串: 794 | 795 | ```cpp 796 | template 797 | class Message { 798 | ... 799 | }; 800 | 801 | extern char const hello[] = "Hello Wolrd!"; 802 | char const hello11[] = "Hello World!"; 803 | 804 | void foo() 805 | { 806 | static char const hello17[] = "Hello World!"; 807 | 808 | Message msg03; // OK in all versions 809 | Message msg11; // OK since C++11 810 | Message msg17; // OK since C++17 811 | } 812 | ``` 813 | 814 | 必要条件是声明为引用或指针的非类型模板参数必须是一个在C++全版本中拥有外部链接的常量表达式,自C++11起内部链接亦可,而C++17之后则只要求有任意的某个链接就行。 815 | 816 | 参考P354节17.2对这一领域未来可能发生变化的一个讨论。 817 | 818 | 这里有些(少得可怜)非法的示例: 819 | 820 | ```cpp 821 | template 822 | class C; 823 | 824 | struct Base { 825 | int i; 826 | } base; 827 | 828 | struct Derived : public Base { 829 | } derived; 830 | 831 | C* err1; // ERROR: derived-to-base conversions are not considered 832 | 833 | C* err2; // ERROR: fields of variables aren't considered to be variables 834 | 835 | int a[10]; 836 | C* err3; // ERROR: addresses of array elements aren't acceptable either 837 | ``` 838 | 839 | ### 12.3.4 模板模板实参 840 | 841 | 模板模板实参通常必须是一个严格匹配类模板或别名模板的模板参数的实参替换。C++17之前,模板模板实参的默认参数会被忽略(但是如果模板模板参数有默认参数,它们会在模板实例化时被考虑)。C++17放宽了这一匹配规则,它只需要模板模板参数至少被相应的模板模板实参特化(参考P330节16.2.2)。 842 | 843 | 在C++17之前下面的例子是非法的: 844 | 845 | ```cpp 846 | #include 847 | // declares in namespace std: 848 | // template> 849 | // class list; 850 | template class Cont> // Cont expects one parameter 851 | class Rel { 852 | ... 853 | }; 854 | 855 | Rel rel; // ERROR before C++17: std::list has more than 856 | // one template parameter 857 | ``` 858 | 859 | 示例中的问题在于`std::list`这一标准库模板拥有多于一个的模板参数。第二个参数(描述一个`allocator`)拥有一个默认值,但是在C++17之前,在匹配`std::list`为`Container`参数时这并不会被考虑。 860 | 861 | 可变模板模板参数是C++17之前上述描述的“严格匹配”规则的一个例外,同时它也有一个解除这一限制的方案:它们对模板模板实参启用更通用的匹配。模板模板参数包可以匹配零到多个模板模板实参中的相同种类的模板参数。 862 | 863 | > 译者注:这里相同种类不是指狭义的数据类型,而是指类型参数、非类型参数、函数模板参数、模板模板参数这些不同的类别(也就是12.3分开讨论的这些)。 864 | 865 | ```cpp 866 | #include 867 | 868 | template class Cont> // Cont expects any number of 870 | class Rel { // type parameters 871 | ... 872 | }; 873 | 874 | Rel rel; // OK: std::list has two template parameters 875 | // but can be used with one argument 876 | ``` 877 | 878 | 模板参数包只能匹配相同种类的模板参数。例如,下面的类模板可以使用仅有一个模板参数类型的任意类模板或别名模板实例化,因为模板类型参数包在这里传递的`TT`可以匹配零到多个模板类型参数: 879 | 880 | ```cpp 881 | #include 882 | #include 883 | // declares in namespace std; 884 | // template, 886 | typename Allocator = allocator>> 887 | // class map; 888 | #include 889 | // declares in namespace std; 890 | // template 891 | // class array; 892 | 893 | template class TT> 894 | class AlmostAnyTmpl { 895 | }; 896 | 897 | AlmostAnyTmpl withVector; // two type parameters 898 | AlmostAnyTmpl witMap; // four type parameters 899 | AlmostAnyTmpl withArray; // ERROR: a template type parameter pack 900 | // doesn't match a nontype template parameter 901 | ``` 902 | 903 | 在C++17之前,声明模板模板参数只能使用关键字`class`,但这并不代表仅允许将用关键字class声明的类模板用作替换参数。实际上,`struct`,`union`以及别名模板也都是模板模板参数的合法实参(别名模板是C++11后才出现并支持)。这类似于这一现象:任何类型都可以用作关键字`class`声明的模板类型参数的实参。 904 | 905 | ### 12.3.5 等价性(equivalent) 906 | 907 | 当两组模板实参的每一对参数值都相同时,它们被视为等价的。对于类型参数,类型别名无关紧要:最终比较的是类型别名所声明的底层类型。对于整型非类型实参,参数的值会被比较;这个值如何表示无关紧要。下面的例子阐释了这一概念: 908 | 909 | ```cpp 910 | template 911 | class Mix; 912 | 913 | using Int = int; 914 | 915 | Mix* p1; 916 | Mix* p2; // p2 has the same type as p1 917 | ``` 918 | 919 | (正如这一示例所澄清,无需模板定义即可确定模板参数列表的等价性。) 920 | 921 | 在模板依赖上下文中,模板实参的”值“却是无法一直被明确确定的,且对等价性来说这里的规则更加复杂。考虑下例: 922 | 923 | ```cpp 924 | template struct I {}; 925 | 926 | template void f(I); // #1 927 | template void f(I); // #2 928 | 929 | template void f(I); // #3 ERROR 930 | ``` 931 | 932 | 谨慎声明#1和#2,你将注意到它们仅仅是交换重命名了的`M`和`N`,你得到了相同的声明:二者是等价的,它们声明了相同的模板`f`。表达式`M+N`和`N+M`在这两个声明中被视为等价的。 933 | 934 | 然而#3的声明,确是有着巧妙的不同:只有操作数被翻转。这会让表达式`N+M`与前两者都不等价。然而,对于任意的模板参数值,最终产生的都是相同的结果,因此,这些表达式在功能上也是等价的(functionally equivalent)。以此差别而声明模板是错误的行径,尽管它们不等价但在功能上确是等价的。然而,编译器无需诊断此类错误。这是因为某些编译器可能,举例来说,在内部将`N+1+1`表示为等同的`N+2`,但其他编译器则不然。C++标准没有强行规定某种特定的实现方式,而是两者皆允,同时要求程序员对这一领域保持谨慎。 935 | 936 | 函数模板生成的函数与普通的函数永远不是等价的,尽管他们可能有相同的类型和名称。这对类成员来说产生了两个重要影响: 937 | 1. 成员函数模板生成的函数永远不会覆盖(override)虚函数。 938 | 2. 构造器模板生成的构造器永远不会是拷贝或移动构造器。类似的,赋值操作符模板生成的赋值操作符函数也永远不会是拷贝赋值或是移动赋值操作符函数。(然而,由于隐式调用拷贝赋值或移动赋值操作符函数的情景相对少,所以这一般不会引起问题。)这一事实各有优劣。可以参考P95节6.2和P102节6.4了解更多细节。 939 | 940 | ## 12.4 可变模板 941 | 942 | 在P55节4.1中介绍的可变模板参数,是指那些至少包含一个模板参数包(参考P188节12.2.4)的模板。当模板的行为可以泛化为任意数量实参时可变模板将非常有用。P188节12.2.4引入的`Tuple`类模板就是一个可变模板,因为一个tuple可以有任意数量的元素,它们被同等对待。我们也可以想象一个简单的`print()`函数,它携带任意数量的参数并按顺序打印每一个。 943 | 944 | 当可变模板的模板实参被确定时,可变模板的每个模板参数包都将匹配连续的零到多个模板实参。我们将此模板实参序列称为实参包(argument pack)。下面的例子阐述了模板参数包`Types`是如何根据`Tuple`所提供的模板实参而匹配不同的实参包的。 945 | 946 | ```cpp 947 | template 948 | class Tuple { 949 | // provides operations on the list of types in Types 950 | }; 951 | 952 | int main() { 953 | Tuple<> t0; // Types contains an empty list 954 | Tuple t1; // Types contains int 955 | Tuple t2; // Types contains int and float 956 | } 957 | ``` 958 | 959 | 由于模板参数包代表了若干个而不是单一的模板实参,它必须在实参包中所有参数都被应用的相同语法结构上下文中使用。其中之一就是`sizeof...`操作符,它会对实参包中实参的个数进行计数。 960 | 961 | ```cpp 962 | template 963 | class Tuple { 964 | public: 965 | static constexpr std::size_t length = sizeof...(Types); 966 | }; 967 | 968 | int a1[Tuple::length]; // array of the integer 969 | int a3[Tuple::length]; // array of three integers 970 | ``` 971 | 972 | ### 12.4.1 包展开(Pack Expansions) 973 | 974 | `sizeof...`表达式是包展开的一个例子。包展开是一种把一个实参包展开成独立实参的结构。`sizeof...`执行这一展开只是为了去计数独立实参的个数,其他形式的实参包——那些在C++渴望一个列表的场合——可以将列表展开成多个元素。这样的包展开由列表中元素右侧的省略号(...)标识。这里有一个简单的例子,我们创建了一个新的类模板`MyTuple`,它传递实参给`Tuple`的同时也从`Tuple`类继承: 975 | 976 | ```cpp 977 | template 978 | class MyTuple : public Tuple { 979 | // extra operations provided only for MyTuple 980 | }; 981 | 982 | MyTuple t2; // inherits from Tuple 983 | ``` 984 | 985 | 模板实参`Types...`是一个包展开,它产生了一个模板实参序列,实参包中的每个实参都用于取代`Types`。如例子中所展示,实例化的类型`MyTuple`的模板类型参数包`types`被实参包`int, float`所取代。当出现在参数展开`Types...`时,我们得到一个模板实参`int`和另一个模板实参`float`,因此`MyTuple`从`Tuple`处继承。 986 | 987 | 理解包展开的一种直观方法是根据语法展开来思考它们,模板参数包将被正确数量的(非包)模板参数替换,并且包展开被写为单独的参数,每个非包类型的模板参数各一个。例如,`MyTuple`被展开成两个参数应该长这个样子: 988 | 989 | ```cpp 990 | template 991 | class MyTuple : public Tuple { 992 | // extra operations provided only for MyTuple 993 | }; 994 | ``` 995 | 996 | 三个参数则长这样子: 997 | 998 | ```cpp 999 | template 1000 | class MyTuple : public Tuple { 1001 | // extra operations provided only for MyTuple 1002 | }; 1003 | ``` 1004 | 1005 | 然而请注意,你无法直接通过名字来访问参数包中的独立元素,因为`T1, T2`等名字并没有在可变模板中定义。如果你需要类型,唯一可以做的事就是传递它们(非递归地)给另一个类或函数。 1006 | 1007 | 每个包展开都有一个模式(pattern),它是一个被实参包的每个实参所替换的类型或表达式,并且通常出现在表示包展开的省略号之前。我们前面的例子都只有些无关紧要的模式——参数包的名称——但是模式可以更为复杂。例如,我们可以定义一个新类型`PtrTuple`,它继承于实参类型的指针所构成的`Tuple`: 1008 | 1009 | ```cpp 1010 | template 1011 | class PtrTuple : public Tuple { 1012 | // extra operations provided only for PtrTuple 1013 | }; 1014 | 1015 | PtrTuple t3; // Inherits from Tuple 1016 | ``` 1017 | 1018 | 包展开`Types*...`的模式是`Types*`。该模式产生了一个模板类型实参替换的序列,每个实参的类型都被其对应的指针类型所取代,并应用于Types中。在包展开的语法解释下,这是如果将`PtrTuple`扩展为三个参数时看起来的样子: 1019 | 1020 | ```cpp 1021 | template 1022 | class PtrTuple : public Tuple { 1023 | // extra operations provided only for PtrTuple 1024 | }; 1025 | ``` 1026 | 1027 | ### 12.4.2 包展开可以在哪里出现? 1028 | 1029 | 我们目前的例子都是聚焦于使用包展开来产生一个模板实参序列。实际上,包展开基本上可以在语法提供逗号分隔列表的任何位置使用,这包括: 1030 | - 基类列表 1031 | - 构造器中的基类初始化列表(initializer) 1032 | - 调用实参列表(模式就是实参表达式) 1033 | - 初始化列表(例如,在花括号初始化列表(initializer list)) 1034 | - 类、函数或别名模板的模板参数列表 1035 | - 函数可以抛出的异常列表(自C++11起不建议使用、C++17后不再允许) 1036 | - 在属性内,如果属性本身支持包展开(尽管在C++标准中没有定义这样的属性) 1037 | - 指定某个声明的对齐方式时 1038 | - 指定lambda表达式捕获列表时 1039 | - 函数类型的参数列表 1040 | - 在`using`声明中(自C++17起支持;参考P65节4.4.5)。我们已经提到过`sizeof...`作为一种包展开机制,它并不会真正产生一个列表,C++17也增加了表达式折叠(fold expressions),这是另一种不产生逗号分隔的列表的机制(参考P207节12.4.6) 1041 | 1042 | 上述包展开所在的某些上下文只是为了归纳的完整性,因此,我们仅将注意力集中在那些在实践中往往有用的包展开上下文上。毕竟包展开在所有上下文中都遵循相同的原则和语法,你大可从此处给出的示例推断出是否需要更深奥的包展开上下文。 1043 | 1044 | 在基类列表中的包展开会扩展成多个直接基类。这种扩展对于通过mixins聚合外部提供的数据和功能很有用,mixins是旨在“混合到”类层次结构中以提供新行为的类。例如,下面的`Point`类在多个不同上下文中使用了包展开以允许任意的mixins: 1045 | ```cpp 1046 | template 1047 | class Point : public Mixins... { // base class pack expansion 1048 | double x, y, z; 1049 | public: 1050 | Point() : Mixins()... { } // base class initializer pack expansion 1051 | template 1052 | void visitMixins(Visitor visitor) { 1053 | visitor(static_cast(*this)...); // call argument pack expansion 1054 | } 1055 | }; 1056 | 1057 | struct Color { char red, green, blue; }; 1058 | struct Label { std::string name; }; 1059 | Point p; // inherits from both Color and Label 1060 | ``` 1061 | 1062 | `Point`类使用包扩展来获取每个提供的mixin,并将其扩展为公有继承的基类。`Point`的默认构造器在类初始化列表中使用了包展开,对mixin机制引入的每个基类进行了值初始化。 1063 | 1064 | 成员函数模板`visitMixins`最有趣,它使用了包展开的结果作为调用参数。通过转换`*this`为每一种mixin类型,包展开生成了每个基类对应mixin类型的调用参数。P204节12.4.3中介绍了实际上与`visitMixins`一起使用而编写的visitor,它可以使用任意数量的函数调用参数。 1065 | 1066 | 包展开也在模板参数列表中创建非类型模板参数包时使用: 1067 | 1068 | ```cpp 1069 | template 1070 | struct Values { 1071 | template 1072 | struct Holder { 1073 | }; 1074 | }; 1075 | 1076 | int i; 1077 | Values::Holder<'a', 17, &i> valueHolder; 1078 | ``` 1079 | 1080 | 注意一旦`Values<..>`的类型实参被确定,`Values<...>::Holder`的非类型实参列表就是固定的尺寸;参数包`Vs`就不是一个变长参数包。 1081 | 1082 | `Values`是一个非类型模板参数包,其中每个真实的模板实参都可以是不同的类型,它们由模板类型参数包`Types`提供的类型所指定。请注意,`Values`声明中的省略号起着双重作用,既将模板参数声明为模板参数包,又将该模板参数包的类型声明为一个包展开。这种模板参数包在实践中非常罕见,而在一个更加常见的上下文——函数参数中这种规则同样生效。 1083 | 1084 | ### 12.4.3 函数参数包 1085 | 1086 | 函数参数包(function parameter pack)是一个匹配零到多个函数调用实参的函数参数。与模板参数包相似,函数参数包通过在函数参数名前使用前置省略号引入,同样地,函数参数包在使用时必须由包展开来扩展。模板参数包和函数参数包被统一称作参数包(parameter packs)。 1087 | 1088 | 与模板参数包不同的是,函数参数包始终都是包展开,因此它们声明的类型必须包含至少一个参数包。下面的例子中,我们引入一个新的`Point`构造器,使用提供的构造器实参来拷贝初始化每一个mixin: 1089 | 1090 | ```cpp 1091 | template 1092 | class Point : public Mixins... 1093 | { 1094 | double x, y, z; 1095 | public: 1096 | // default constructor, visitor function, etc. elided 1097 | Point(Mixins... mixin) // mixins is a function parameter pack 1098 | : Mixins(mixins)...{ } // initialize each base with the supplied mixin value 1099 | }; 1100 | 1101 | struct Color { char red, green, blue; }; 1102 | struct Label { std::string name; }; 1103 | Point p({0x7F, 0, 0x7F}, {"center"}); 1104 | ``` 1105 | 1106 | 函数模板的函数参数包可能依赖于模板中声明的模板参数包,这使得函数模板可以接受任意数量的调用实参而不会损失类型信息: 1107 | 1108 | ```cpp 1109 | template 1110 | void print(Types... values); 1111 | 1112 | int main 1113 | { 1114 | std::string welcome("Welcome to "); 1115 | print(welcome, "C++", 2011, '\n'); // calls print 1117 | } 1118 | ``` 1119 | 1120 | 当使用多个实参调用函数模板`print()`时,实参的类型将放置在参数包中,以取代模板类型参数包`Types`,而实参本身则放入参数包中,以代替函数参数包`Values`。调用实参被确定的过程在第15章对细节进行了描述。当前,只要了解`Types`中的第`i`个类型对应`Values`的第`i`个值即可,并且这些参数包的每一对在函数模板`print()`内都是可用的。 1121 | 1122 | `print()`的真正实现使用了递归的模板实例化,这是一种模板元编程技术,在P123节8.1和第23章中有所描述。 1123 | 1124 | 在参数列表末尾出现的匿名函数参数包与C样式的“ vararg”参数之间在语法上存在歧义。例如: 1125 | 1126 | ```cpp 1127 | template void c_style(int, T...); 1128 | template void pack(int, T...); 1129 | ``` 1130 | 1131 | 前者的`T`被视为`T, ...`:一个匿名参数类型`T`跟着一个C风格的vararg参数。后者的`T...`结构被视为一个函数参数包,因为`T`是一个合法的展开模式。可以通过在省略号前强制添加一个逗号(这保证了省略号被认作C风格vararg参数)或在省略号后跟随一个标识符——这意味着它是一个命名函数参数包来消除歧义。请注意,在通用的lambda中,如果紧随其后的类型(没有中间逗号)包含auto,则尾随的`…`将被视为表示参数包。 1132 | 1133 | ### 12.4.4 多重与嵌套包展开 1134 | 1135 | 包展开的模式可以随意复杂且可以包含多重、不同的参数包。当实例化包含多重参数包的包展开时,所有的参数包都必须有相同的尺寸。从每个参数包的第一个实参开始进行模式替换,然后是每个参数包的第二个实参,以此类推,最终组织成类型或值的序列。例如,下面的函数在转发所有实参给函数对象`f`之前,对他们进行了拷贝: 1136 | 1137 | ```cpp 1138 | template 1139 | void forwardCopy(F f, Types const&... values) { 1140 | f(Types(values)...); 1141 | } 1142 | ``` 1143 | 1144 | 调用实参包展开命名了两个实参包,`Types`和`values`。当实例化该模板时,`Types`和`values`参数包的逐个元素会产生一系列对象构造体,它们使用`Types`的第i个类型创建了`values`的第i个值。在包展开的语法解析下,三个实参的`forwardCopy`可能长这个样子: 1145 | 1146 | ```cpp 1147 | template 1148 | void forwardCopy(F f, T1 const& v1, T2 const& v2, T3 const& v3) { 1149 | f(T1(v1), T2(v2), T3(v3)); 1150 | } 1151 | ``` 1152 | 1153 | 包展开本身也可以嵌套。此时,每个参数包都可以由最近的一个闭合的包展开所扩展(也只能是这个包展开)。下面的例子阐释了引入3个不同参数包的嵌套包展开: 1154 | 1155 | ```cpp 1156 | template 1157 | class Nested { 1158 | template 1159 | void f(InnerTypes const&... innerValues) { 1160 | g(OuterTypes(InnerTypes(innerValues)...)...); 1161 | } 1162 | }; 1163 | ``` 1164 | 1165 | `g()`的调用中,模式`InnerTypes(innerValues)`的包展开是最内层的,它扩展了`InnerTypes`和`innerValues`并为`OuterTypes`表示的对象产生了一个函数调用实参序列。外层的包展开模式包含内层包展开,为函数`g()`产生了一个调用参数集,它们由内层包展开生成的函数调用实参序列所形成的`OuterTypes`中的每一种实例化类型所创造。在这种包展开的语法解析下,当`OuterTypes`有2个实参,`InnerTypes`和`innerValues`都有3个实参时,嵌套会变得更加明显: 1166 | 1167 | ```cpp 1168 | template 1169 | class Nested { 1170 | template 1171 | void f(I1 const& iv1, I2 const& iv2, I3 const& iv3) { 1172 | g(O1(I1(iv1), I2(iv2), I3(iv3)), 1173 | O2(I1(iv1), I2(iv2), I3(iv3))); 1174 | } 1175 | }; 1176 | ``` 1177 | 1178 | > 这里作者多写了一行O3 1179 | 1180 | 多重与嵌套包展开是一个非常强力的工具(例如,参考P608节26.2)。 1181 | 1182 | ### 12.4.5 零尺寸包展开 1183 | 1184 | 包展开的语法解析对于理解不同实参数量的可变模板实例化的方式非常有用。然而,对于零尺寸实参包来说语法解析经常会失败。为了说明这一点,请考虑P202节12.4.2中的`Point`类模板,该模板在语法上用零个实参替换: 1185 | 1186 | ```cpp 1187 | template<> 1188 | class Point : { 1189 | Point() : { } 1190 | }; 1191 | ``` 1192 | 1193 | 上面编写的代码格式不正确,因为模板参数列表现在为空,并且空的基类和基类初始化器列表每个都有一个冒号。 1194 | 1195 | 包展开实际上是语义结构,任意尺寸实参包的替换并不会影响包展开(或其封闭的可变参数模板)的解析。当包扩展展开成一个空列表时,程序的表现(语义上)就好像该列表不曾存在。实例化`Point <>`最终没有基类,并且其默认构造函数没有基类初始化程序,但其格式正确。这一语法规则使得即使是零尺寸的包展开也可以被完美定义(但有所区别)。例如: 1196 | 1197 | ```cpp 1198 | template 1199 | void g(Types... values) { 1200 | T v(values...); 1201 | } 1202 | ``` 1203 | 1204 | 可变函数模板`g()`创造了一个值`v`,它使用传入的`values`一系列值来直接初始化。如果`values`是空的,那么`v`在语法上看起来就好像是一个函数声明`T v()`。然而,因为包展开的替换是一种语法且解析时不会产生影响其他类型的实体,`v`会通过零个实参进行初始化,也就是说,这依然还是值初始化。 1205 | 1206 | ### 12.4.6 折叠表达式 1207 | 1208 | 对一连串的值进行同一模式的递归处理被称做操作的折叠。例如,对序列`x[1],x[2],...,x[n-1],x[n]`进行函数`fn`右折叠会得到`fn(x[1],fn(x[2], fn(...,fn(x[n-1],x[n])...)))`。在探索一种新的语言特性时,C++委员会遇到了需要特殊处理的结构:应用于包展开的二元逻辑运算符(即`&&`或`||`)。在没有额外的语法特性时,我们需要编写下面的代码来实现`&&`操作: 1209 | 1210 | ```cpp 1211 | bool and_all() { return ture; } 1212 | template 1213 | bool and_all(T cond) { return cond; } 1214 | template 1215 | bool and_all(T cond, Ts... conds) { 1216 | return cond && and_all(conds...); 1217 | } 1218 | ``` 1219 | 1220 | C++17引入了一种新的特性——折叠表达式(fold expressions)(参考P58节4.2)。它可以应用于除了`.`,`->`和`[]`以外的所有的二元操作符。 1221 | 1222 | 给定一个未展开表达式模式`pack`和一个非模式表达式`value`,C++17允许我们使用任意操作符`op`写出: 1223 | 1224 | ```cpp 1225 | (pack op ... op value) 1226 | ``` 1227 | 1228 | 作为一个操作符右折叠(称作二元右折叠),或者写出: 1229 | 1230 | ```cpp 1231 | (value op ... op pack) 1232 | ``` 1233 | 1234 | 作为一个操作符左折叠(称作二元左折叠)。参考P58节4.2了解更多基本示例。 1235 | 1236 | 折叠操作应用于一个序列,对包进行展开并从最后一个(右折叠)或第一个(左折叠)序列中的元素施加`value`。 1237 | 1238 | 有了这一特性,如下代码: 1239 | 1240 | ```cpp 1241 | template bool g() { 1242 | return and_all(trait()...); 1243 | } 1244 | ``` 1245 | 1246 | (`and_all`在上面代码中定义),就可以被替换写成: 1247 | 1248 | ```cpp 1249 | template bool g() { 1250 | return (trait() && ... && true); 1251 | } 1252 | ``` 1253 | 1254 | 如你所愿,折叠表达式是包展开。注意即使包为空,折叠表达式的类型仍然可以借由非包操作数(上例中是`value`)来确定。 1255 | 1256 | 然而,这一特性的设计者还希望增加一个摆脱`value`操作数的选项。在C++17中还支持另外两种形式:一元右折叠`(pack op ...)`和一元左折叠(`... op pack`)。 1257 | 1258 | 此时小括号依然是必须的。很明显对于空展开来说这产生了一个问题:如何确定它们的类型或是值呢?答案就是对于一元折叠表达式来说,空展开通常来说会导致一个错误,除了以下三种特例: 1259 | - 单一折叠`&&`对空展开产生一个值`true`。 1260 | - 单一折叠`||`对空展开产生一个值`false`。 1261 | - 单一折叠`,`会产生表达式`void`。 1262 | 1263 | 注意,如果你重载上述某个特殊的操作符时(通常不太常见),可能会出乎意料,例如: 1264 | 1265 | ```cpp 1266 | struct BooleanSymbol { 1267 | ... 1268 | }; 1269 | 1270 | BooleanSymbol operator||(BooleanSymbol, BooleanSymbol); 1271 | 1272 | template void symbolic(BTs... ps) { 1273 | BooleanSymbol result = (ps || ...); 1274 | ... 1275 | } 1276 | ``` 1277 | 1278 | 假设我们用从`BooleanSymbol`继承的类型来调用`symbolic`。对所有展开来说,除了空展开以外,都会产生一个`BooleanSymbol`值(空展开产生的是布尔值)。我们要注意一元折叠表达式的使用,并推荐以二元折叠表达式作为替代(显式地指定空展开值)。 1279 | 1280 | ## 12.5 友元 1281 | 1282 | 声明友元的初衷非常简单:在某个类中标记友元函数或友元类以使其获得访问特权。由于以下两个因素,事情变得有些复杂: 1283 | 1. 友元的声明必须是唯一的。 1284 | 2. 友元函数声明时可以直接定义。 1285 | 1286 | ### 12.5.1 类模板的友元类 1287 | 1288 | 友元类声明时不能定义,因此很少出问题。在模板的上下文中,友元类声明的唯一新奇之处是在于能够将类模板的特定实例声明为友元: 1289 | 1290 | ```cpp 1291 | template 1292 | class Node; 1293 | 1294 | template 1295 | class Tree { 1296 | friend class Node; 1297 | ... 1298 | }; 1299 | ``` 1300 | 1301 | 请注意,类模板必须在其实例之一成为类或类模板的友元时是可见的。对普通类来说,则没有这种要求: 1302 | 1303 | ```cpp 1304 | template 1305 | class Tree { 1306 | friend class Factory; // OK even if first declaration of Factory 1307 | friend class Node; // error if Node isn't visible 1308 | }; 1309 | ``` 1310 | 1311 | P220节13.2.2对此有更多描述。 1312 | 1313 | P75节5.5引入了一个例子,给出了其他类模板实例做友元时的声明: 1314 | 1315 | ```cpp 1316 | template 1317 | class Stack { 1318 | public: 1319 | ... 1320 | // assign stack of elements of type T2 1321 | template 1322 | Stack& operator=(Stack const&); 1323 | // to get access to private members of Stack for any type T2: 1324 | template friend class Stack; 1325 | ... 1326 | }; 1327 | ``` 1328 | 1329 | C++11也增加了让模板参数作友元的语法: 1330 | 1331 | ```cpp 1332 | template 1333 | class Wrap { 1334 | friend T; 1335 | ... 1336 | }; 1337 | ``` 1338 | 1339 | 对任何类型`T`来说这都是合法的,如果`T`不是一个类类型的话,友元就会被忽略(译者注:基础类型不需要声明为友元)。 1340 | 1341 | ### 12.5.2 类模板的友元函数 1342 | 1343 | 函数模板的实例可以作为友元,只要保证友元函数名称后跟着一个尖括号子句即可。尖括号子句可以包含模板实参,但是如果实参可以被推导,那么尖括号就可以留空: 1344 | 1345 | ```cpp 1346 | template 1347 | void combine(T1, T2); 1348 | 1349 | class Mixer { 1350 | friend void combine<>(int&, int&); // OK: T1 = int&, T2 = int& 1351 | friend void combine(int, int); // OK: T1 = int, T2 = int 1352 | friend void combine(char, int); // OK: T1 = char, T2 = int 1353 | friend void combine(char&, int); // ERROR: doesn't match combine() template 1354 | friend void combine<>(long, long) { ... } // ERROR: definition not allowed! 1355 | }; 1356 | ``` 1357 | 1358 | 请注意,我们无法定义模板实例(最多可以定义一个特化体),因此友元声明不能是一个定义。 1359 | 1360 | 如果名称后没有跟尖括号子句,那么有两种可能: 1361 | 1. 如果名字没有限定符(换句话说,不包含`::`),它永远不会是一个模板实例。如果友元声明时不存在可见的匹配的非模板函数,此处的友元声明就作为该函数的第一次声明。该声明也可以是一个定义。 1362 | 2. 如果名称带有限定符(包含`::`),该名称必须可以引用到一个此前声明过的函数或函数模板。非模板函数会比函数模板优先匹配。然而,这里的友元声明不能是一个定义。这里有个例子来说明这一区别: 1363 | 1364 | ```cpp 1365 | void multiply(void*); // ordinary function 1366 | 1367 | template 1368 | void multiply(T); // function template 1369 | 1370 | class Comrades { 1371 | friend void multiply(int) { } // defines a new function ::multiply(int) 1372 | 1373 | friend void ::multiply(void*); //refers to the ordinary function above, 1374 | // not the the multiply instance 1375 | friend void ::multiply(int); // refers to an instance of the template 1376 | friend void ::multiply(double*); // qualified names can also have angle brackets, 1377 | // but a template must be visible 1378 | friend void ::error() { } // ERROR: a qualified friend cannot be a definition 1379 | }; 1380 | ``` 1381 | 1382 | 在前例中,我们在一个普通的类中声明了友元函数。在类模板中声明友元函数规则也是如此,只不过模板参数可以参与到函数声明中: 1383 | 1384 | ```cpp 1385 | template 1386 | class Node { 1387 | Node* allocate(); 1388 | ... 1389 | }; 1390 | 1391 | template 1392 | class List { 1393 | friend Node* Node::allocate(); 1394 | }; 1395 | ``` 1396 | 1397 | 函数模板也可以在类模板中定义,此时只有在它真正被使用到时才会实例化。通常,这要求友元函数以友元函数的类型使用类模板本身,这使得在类模板上表示函数变得更容易,就好像它们在命名空间中可见一样: 1398 | 1399 | ```cpp 1400 | template 1401 | class Creator { 1402 | friend void feed(Creator) { //every T instantiates a different function ::feed() 1403 | ... 1404 | } 1405 | }; 1406 | 1407 | int main() 1408 | { 1409 | Creator one; 1410 | feed(one); // instantiates ::feed(Creator) 1411 | Creator two; 1412 | feed(two); // instantiates ::feed(Creator) 1413 | } 1414 | ``` 1415 | 1416 | 示例中,`Creator`的每个实例都会生成一个不同的函数。请注意,即使这些函数是作为模板实例化的一部分生成的,这些函数本身也只是普通的函数,并不是模板的实例。然而,这种情况被视为模板实体(templated entities, 参考P181节12.1),它们仅在被使用到时才会被定义。同时也注意到由于这些函数的函数体在类定义域内被定义,所以它们是内联(inline)的。因此,两个不同编译单元生成该相同的函数并不会引起错误。可以参考P220节13.2.2和P497节21.2.1来了解该话题的更多信息。 1417 | 1418 | ### 12.5.3 友元模板 1419 | 1420 | 通常在声明一个函数或类模板的实例为友元时,我们可以严格地表示哪个实体才是友元。尽管如此,有些时候对某种模板的所有实例都设为友元也是很有用的。这就需要使用友元模板(friend template)。例如: 1421 | 1422 | ```cpp 1423 | class Manager { 1424 | template 1425 | friend class Task; 1426 | 1427 | template 1428 | friend void Schedule::dispatch(Task*); 1429 | 1430 | template 1431 | friend int ticket() { 1432 | return ++Manager::counter; 1433 | } 1434 | 1435 | static int counter; 1436 | }; 1437 | ``` 1438 | 1439 | 与普通的友元声明一样,当名称是不含限定符的函数名时友元模板也可以是一个定义,函数名后不接尖括号子句。 1440 | 1441 | 友元模板只能定义主模板和主模板的成员。主模板的偏特化和显式特化也都会被自动的视作友元。 1442 | 1443 | ## 12.6 后记 1444 | 1445 | C++模板的通用语法和概念自80年代起就相对保持稳定。类模板和函数模板是最开始时构成模板的两部分。类型模板和非类型模板也是。 1446 | 1447 | 然而,受C++标准库的需求所驱动,后来新增了一些重大的特性。成员模板可能是这些添加中最基础的。搞笑的是,只有成员函数模板被正式票入C++标准。成员类模板在社论监督下才成为标准的一部分。 1448 | 1449 | 友元模板,默认模板实参,模板模板参数是在C++98标准化后出现的。声明模板模板参数的能力有时被成为高阶泛型(higher-order genericity)。引入它们原本是为了支持一个已有的C++标准库的分配器(allocator)模型,然而这个分配器模型后来被另一个不依赖模板模板参数的模型取代了。后来,模板模板参数距离被踢出语言标准越来越近,因为它们的规范并不完整,直到非常晚才出现的1998标准化进程。最终大多数委员会成员投票表示保留它们,而它们的规范也完整制定。 1450 | 1451 | 别名模板是在2011标准引入的。别名模板为需要`typedef templates`特性而简化书写模板的场合提供了相同的服务,它仅仅是一个现有类模板的另一种拼写。规范(N2258)(作者是 Gabriel Dos Reis 和 Bjarne Stroustrup;)把它加入到标准。 Mat Marcus 也贡献了这一提议的一些早期草稿。Gaby 还为C++14(N3651)的可变模板提议处理了很多细节内容。本来,该提议仅仅想要支持`constexpr`变量,但是这一限制在标准制定阶段被解除了。 1452 | 1453 | 可变模板由C++11标准库和Boost库所驱动,C++模板库此前一直使用一种递进型高级技巧来支持接受任意数量模板参数。 Doug Gregor, Jaakko J¨arvi, Gary Powell, Jens Maurer, 和 Jason Merrill 为标准化提供了初始的规范(N2242)。当这一规范问世时,Doug 还开发了这一特性的原始实现代码(在GNU的GCC中),为标准库使用这一特性提供了极大助力。 1454 | 1455 | 折叠表达式是 Andrew Sutton 和 Richard Smith 的作品:它们通过N4191文献引入到C++17。 --------------------------------------------------------------------------------