├── .gitignore ├── .gitattributes ├── .remarkrc ├── theme ├── assets │ └── 1200px-ISO_C++_Logo.svg.png └── main.html ├── requirements.txt ├── .editorconfig ├── package.json ├── .github └── workflows │ ├── lint.yml │ └── build_doc.yml ├── .vscode └── settings.json ├── Source ├── index.md ├── Chapter0 │ └── README.md ├── Chapter1 │ └── README.md ├── Chapter5 │ └── README.md ├── Chapter3 │ └── README.md ├── Chapter4 │ └── README.md └── Chapter2 │ └── README.md ├── mkdocs.yml └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | /venv 4 | /node_modules 5 | /site 6 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.remarkrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "remark-preset-lint-mkdocs-material" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /theme/assets/1200px-ISO_C++_Logo.svg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nolongerwait/cpp_lambda_story_chinese_edition/HEAD/theme/assets/1200px-ISO_C++_Logo.svg.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | mkdocs-material-extras >= 0.0.5 2 | 3 | mkdocs-minify-plugin >= 0.5 4 | mkdocs-redirects >= 1.0 5 | mkdocs-git-revision-date-localized-plugin >= 1.0.0 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 4 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.yml] 13 | indent_size = 2 14 | -------------------------------------------------------------------------------- /theme/main.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block scripts %} 4 | {{ super() }} 5 | 10 | {% endblock %} 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "packageManager": "pnpm@8.15.1", 4 | "scripts": { 5 | "build": "mkdocs build", 6 | "dev": "mkdocs serve", 7 | "lint": "remark ./Source/**/*.md --quiet --frail", 8 | "lint:fix": "remark ./Source/**/*.md --quiet --frail --output" 9 | }, 10 | "dependencies": { 11 | "remark-cli": "^10.0.1", 12 | "remark-preset-lint-mkdocs-material": "^0.6.0" 13 | }, 14 | "devDependencies": { 15 | "pnpm": "^8.15.1", 16 | "taze": "^0.13.3" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://json-schema.org/draft-07/schema# 2 | name: Lint 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | pull_request: 9 | branches: 10 | - main 11 | 12 | jobs: 13 | build: 14 | name: Lint documentation 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout repository 18 | uses: actions/checkout@v2 19 | 20 | - uses: pnpm/action-setup@v2 21 | with: 22 | version: latest 23 | 24 | - name: Set up Node.js 25 | uses: actions/setup-node@v2 26 | with: 27 | node-version: 16 28 | cache: pnpm 29 | 30 | - name: Install Node dependencies 31 | run: | 32 | pnpm i 33 | 34 | - name: Lint 35 | run: | 36 | pnpm lint 37 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "files.associations": { 4 | ".remarkrc": "json" 5 | }, 6 | "cSpell.words": [ 7 | "arithmatex", 8 | "autohide", 9 | "betterem", 10 | "ceret", 11 | "escapeall", 12 | "fontawesome", 13 | "hardbreak", 14 | "inlinehilite", 15 | "linenums", 16 | "magiclink", 17 | "materialx", 18 | "mathjax", 19 | "mkdocs", 20 | "nbsp", 21 | "noopener", 22 | "progressbar", 23 | "pymdownx", 24 | "Roboto", 25 | "saneheaders", 26 | "smartsymbols", 27 | "striphtml", 28 | "superfences", 29 | "tasklist", 30 | "twemoji", 31 | "uslugify" 32 | ], 33 | "yaml.customTags": [ 34 | "!ENV scalar", 35 | "!ENV sequence", 36 | "tag:yaml.org,2002:python/name:materialx.emoji.to_svg", 37 | "tag:yaml.org,2002:python/name:materialx.emoji.twemoji", 38 | "tag:yaml.org,2002:python/name:pymdownx.superfences.fence_code_format", 39 | "tag:yaml.org,2002:python/name:pymdownx.slugs.uslugify", 40 | "tag:yaml.org,2002:python/name:pymdownx.arithmatex.inline_mathjax_format", 41 | "tag:yaml.org,2002:python/name:pymdownx.arithmatex.fence_mathjax_format" 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /.github/workflows/build_doc.yml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://json-schema.org/draft-07/schema# 2 | name: Build Doc 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | pull_request: 9 | branches: 10 | - main 11 | workflow_dispatch: 12 | 13 | jobs: 14 | build-doc: 15 | name: Build documentation 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout repository 19 | uses: actions/checkout@v2 20 | with: 21 | fetch-depth: 0 22 | 23 | - name: Set up Python runtime 24 | uses: actions/setup-python@v3 25 | with: 26 | python-version: 3.9 27 | 28 | - name: Install Python dependencies 29 | run: | 30 | pip install -r requirements.txt 31 | 32 | - uses: pnpm/action-setup@v2 33 | with: 34 | version: latest 35 | 36 | - name: Set up Node.js 37 | uses: actions/setup-node@v2 38 | with: 39 | node-version: 16 40 | cache: pnpm 41 | 42 | - name: Install Node dependencies 43 | run: | 44 | pnpm i 45 | 46 | - name: Build 47 | env: 48 | GOOGLE_ANALYTICS_KEY: ${{ secrets.GOOGLE_ANALYTICS_KEY }} 49 | ENABLE_MATHJAX: false 50 | run: | 51 | pnpm build 52 | 53 | - name: Deploy to gh-pages 54 | if: ${{ github.event_name != 'pull_request' }} 55 | uses: peaceiris/actions-gh-pages@v3 56 | with: 57 | github_token: ${{ secrets.GITHUB_TOKEN }} 58 | publish_dir: site 59 | force_orphan: true 60 | user_name: "github-actions[bot]" 61 | user_email: "github-actions[bot]@users.noreply.github.com" 62 | commit_message: 🚀 Deploying to gh-pages @ ${{ env.GITHUB_SHA }} 63 | -------------------------------------------------------------------------------- /Source/index.md: -------------------------------------------------------------------------------- 1 | # C++ Lambda Story - From C++98 to C++20 2 | 3 | ## 介绍 4 | 5 | 本文为《C++ Lambda Story》的中文翻译,如果您觉得此书有价值,可以在 [https://leanpub.com/cpplambda](https://leanpub.com/cpplambda) 上支持下原作者。 6 | 7 | 或者如果您认识相关的翻译工作者或者出版社,可以积极联系原作者与出版社进行正规的中文翻译并出版。 8 | 9 | ## 译者 10 | 11 | | Chapter | Translator | 12 | | :----------------------------------------: | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | 13 | | [关于此书](./Chapter0/README.md) | @nolongerwait | 14 | | [Lambda in C++98/03](./Chapter1/README.md) | @nolongerwait | 15 | | [Lambda in C++11](./Chapter2/README.md) | @nolongerwait | 16 | | [Lambda in C++14](./Chapter3/README.md) | @nolongerwait | 17 | | [Lambda in C++17](./Chapter4/README.md) | @Dup4 | 18 | | [Lambda in C++20](./Chapter5/README.md) | @nolongerwait | 19 | -------------------------------------------------------------------------------- /Source/Chapter0/README.md: -------------------------------------------------------------------------------- 1 | # 关于此书 2 | 3 | ## 成书渊源 4 | 5 | ## 阅读对象 6 | 7 | 本书适用于所有喜欢了解现代 C++ 特性:Lambda 表达式 的 C++ 开发人员。 8 | 9 | ## 读者反馈 10 | 11 | 如果您发现错误、拼写错误、语法错误……或其他任何需要更正的(特别是逻辑问题!),请将您的反馈发送到 bartlomiej.filipek@bfilipek.com。 12 | 13 | 您也可以使用这个地方: 14 | 15 | * [Leanpub Book 的反馈页面 - C++ Lambda Story](https://leanpub.com/cpplambda) 16 | 17 | 更重要的是,这本书在 *GoodReads* 上有一个专门的页面。请在那里分享您的见解: 18 | 19 | * [C++ Lambda Story @GoodReads](https://www.goodreads.com/book/show/53609731-c-lambda-story) 20 | 21 | ## 代码证书 22 | 23 | 这本书的代码在 **知识共享许可(Creative Commons License)** 下可用 24 | 25 | ## 代码格式 26 | 27 | ## 语法高亮限制 28 | 29 | ## 在线编译器 30 | 31 | 你可以使用一些在线编译器,这样就不用在本地创建项目来尝试运行和解读这些示例代码了。 32 | 33 | 这些在线编译器提供基础的文本编辑器,并且通常允许你自行编写源文件进行编译。 34 | 35 | 对于一些简短的代码而言,使用在线编译器来说是十分方便的,可以快速查看代码的运行结果,甚至你可以快速在不同版本,不同环境,不同编译器之间进行切换使用。 36 | 37 | 本书中大部分的代码都附有在线编译器的链接,当然,不同的代码使用的不同的编译器。 38 | 39 | 这是本书中所使用过的全部在线编译器服务: 40 | 41 | * [Coliru](http://coliru.stacked-crooked.com/)- 使用 GCC 9.2.0 版本(截止 2020 年 06 月),功能简洁但十分高效 42 | * [Wandbox](https://wandbox.org/)- 提供了大部分的编译器,包含了绝大多数的 Clang 和 GCC 版本,使用了 boost 的库,支持多文件编译。并且你可以生成链接来分享你的代码。 43 | * [Compiler Explorer](https://gcc.godbolt.org/)- 提供多种编译器,显示生成的汇编代码,可以执行代码,甚至进行静态代码分析。 44 | * [CppBench](https://quick-bench.com/)- 可以运行简单的 C++ 性能测试(基于 Google Benchmark)。 45 | * [C++ Insights](https://cppinsights.io/)- 基于 Clang 的 源码转义工具,可以展示编译器视角下的代码,比如将源代码进行预编译展开。你可以在这查看 Lambda 表达式,auto 关键字,结构化绑定,模板推断,可变参数包,范围式循环等的展开结果。 46 | 47 | 当然,如果想尝试其他 C++ 的在线编译器,你也可以在这个网站查看:[List of Online C++ Compilers by arnemertz](https://arnemertz.github.io/online-compilers/) 48 | 49 | ## 关于作者 50 | 51 | **Bartłomiej (Bartek) Filipekis**,一个拥有超过 12 年专业经验的 C++ 软件开发工程师。2010 年在 Cracow, Poland 毕业自 Jagiellonian University,拥有计算机科学的硕士学位。 52 | 53 | 现就职于 Xara,负责开发高级文档编辑器。 54 | 55 | 同时,拥有桌面图形程序、游戏开发、大型航空系统、图形驱动甚至生物反馈方面的开发经验。 56 | 57 | 早前,在 Cracow 当地的大学中教授编程(游戏编程和图形编程)课程。 58 | 59 | 从 2011 年起,Bartek 开始在 [bfilipek.com](http://bfilipek.com) 上撰写博客。 60 | 61 | 起初,博文主题围绕图形编程,但是现在更多聚焦于 C++ 核心内容。 62 | 63 | 同时,他也是 [Crocow C++](https://www.meetup.com/C-User-Group-Cracow/) 开发者组织的联合组织者。 64 | 65 | 你可以在 [@CppCast](https://cppcast.com/bartlomiej-filipek/) 找到他关于 C++17,博客和文本处理相关的内容。 66 | 67 | 从 2018 年 10 月起,Bartek 开始在 Polish National Body 就任 C++ 专家一职,这是一家直接与 ISO/IEC JTC 1/SC 22 (C++ Standardisation Committee) 工作的公司。 68 | 69 | 同月,Bartek 获得了 Microsoft 授予的 2019/2020 年度的 MVP 头衔荣誉。 70 | 71 | 在空闲时间,喜欢和他心爱的小儿子一起收集和拼装乐高模型。 72 | 73 | Bartek 也是《[C++ 17 In Detail](https://leanpub.com/cpp17indetail)》的作者。 74 | 75 | ## 致谢 76 | 77 | 如果没有 C++ 专家 Tomasz Kamiński 的宝贵意见,本书就不可能完成(参见 [Tomek 在 Linkedin 上的简介](https://www.linkedin.com/in/tomasz-kami%C5%84ski-208572b1/))。 78 | 79 | Tomek 在我们位于克拉科夫的 Local C++ 用户组中主持了关于 Lambda“历史”的现场编码演示:[Lambdas: From C++11 to C++20](https://www.meetup.com/pl-PL/C-User-Group-Cracow/events/258795519/)。 80 | 81 | 本书中使用的很多例子都来自那次会议。 82 | 83 | 尽管本书的初版相对较短,但后续扩展版本(额外的 100 页)是我从 JFT(John Taylor)那得到返回和鼓励的结果。 84 | 85 | John 花费了大量时间寻找可以改进和扩展的细节。 86 | 87 | 此外,我要对提供了很多有关 Lambda 返回内容的 [Dawid Pilarski](panicsoftware.com/about-me) 表示感谢。 88 | 89 | 最后也是相当重要的,我从博客读者、Patreon 论坛以及 C++ Polska 的讨论中获得了大量反馈和评论。 90 | 91 | 谢谢你们! 92 | 93 | ## 校阅历史 94 | 95 | * 2019 年 03 月 25 日 - 第一版上线! 96 | * 2020 年 01 月 05 日 - 语法、更好的例子、措辞、IIFE 部分、C++20 更新。 97 | * 2020 年 04 月 17 日 - C++20 章节重写、语法、措辞、布局。 98 | * 2020 年 04 月 30 日 - 从 C++11、C++17 和 C++20 中的 lambda 派生 99 | * 2020 年 06 月 19 日 - 主要更新: 100 | * 改进了 C++03 章节,添加了有关标准库中的辅助函数对象的部分。 101 | * 添加了有关如何操作的新部分从 C++14 章节中不推荐使用的 bind1stin 转换为现代替代方案。 102 | * C++11 和 C++17 章节中改进和扩展的 IFFE 部分 103 | * 带有 lambda 技术列表的新附录 104 | * 带有五大 lambda 功能列表的新附录,改编自博客文章 105 | * 带有更新副标题的新标题图片 106 | * 整本书的许多较小改进 107 | * 2020 年 08 月 03 日 - 主要更新,Kindle 版本上线可用: 108 | * 大多数代码示例现在在标题中都有指向在线编译器版本的链接 109 | * 改进了 Lambda 语法的描述 110 | * 在 C++17 和 C++20 章节中增添了新的内容。 111 | * 新部分:如何在容器中存储 lambda,Lambda 和异步执行,递归 lambda,类型系统中的异常规范 112 | * 更新了关于 C++14 和 C++17 中可变参数泛型 lambda 的部分 113 | * 更新了关于 C++11 和 C++20 中可变参数包的新部分 114 | * 如果可能的话,在更长的例子中使用 const 和 noexcept 115 | * 细节描述上的措词更正、全书目录结构布局的微调。 116 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: C++ Lambda Story 2 | site_url: https://nolongerwait.github.io/cpp_lambda_story_chinese_edition/ 3 | site_author: nolongerwait 4 | site_description: >- 5 | C++ Lambda Story 6 | 7 | # Repository 8 | repo_name: cpp-lambda-story-chinese-edition 9 | repo_url: https://github.com/nolongerwait/cpp_lambda_story_chinese_edition 10 | edit_uri: "" 11 | 12 | # Copyright 13 | copyright: Copyright © 2022 nolongerwait 14 | 15 | # Configuration 16 | theme: 17 | name: material 18 | custom_dir: theme 19 | 20 | # Static files 21 | static_templates: 22 | - 404.html 23 | 24 | # Don't include MkDocs' JavaScript 25 | include_search_page: false 26 | search_index_only: true 27 | 28 | language: zh 29 | 30 | features: 31 | - header.autohide 32 | # - navigation.instant 33 | # - navigation.expand 34 | # - navigation.sections 35 | - navigation.tracking 36 | # - navigation.tabs 37 | # - navigation.tabs.sticky 38 | - navigation.top 39 | # - navigation.indexes 40 | - search.highlight 41 | - search.share 42 | - search.suggest 43 | - toc.integrate 44 | - content.code.annotate 45 | 46 | # insiders only 47 | # - content.tabs.link 48 | 49 | palette: 50 | - media: "(prefers-color-scheme: light)" 51 | scheme: default 52 | primary: light blue 53 | accent: deep purple 54 | toggle: 55 | icon: material/weather-sunny 56 | name: Switch to dark mode 57 | - media: "(prefers-color-scheme: dark)" 58 | scheme: dracula 59 | primary: deep purple 60 | accent: deep purple 61 | toggle: 62 | icon: material/weather-night 63 | name: Switch to light mode 64 | 65 | font: 66 | text: Roboto 67 | code: Roboto Mono 68 | favicon: assets/1200px-ISO_C++_Logo.svg.png 69 | icon: 70 | repo: fontawesome/brands/github 71 | logo: assets/1200px-ISO_C++_Logo.svg.png 72 | 73 | # Plugins 74 | plugins: 75 | - search: 76 | lang: ja 77 | - git-revision-date-localized: 78 | type: date 79 | enable_creation_date: true 80 | - minify: 81 | minify_html: true 82 | - mkdocs-material-extras: 83 | 84 | # Customization 85 | extra: 86 | generator: false 87 | analytics: 88 | provider: google 89 | property: !ENV GOOGLE_ANALYTICS_KEY 90 | social: 91 | - icon: fontawesome/brands/github 92 | link: https://github.com/nolongerwait/cpp_lambda_story_chinese_edition 93 | 94 | # Extensions 95 | markdown_extensions: 96 | - admonition: 97 | - abbr: 98 | - attr_list: 99 | - def_list: 100 | - footnotes: 101 | - md_in_html: 102 | - meta: 103 | - markdown.extensions.smarty: 104 | smart_quotes: false 105 | - markdown.extensions.tables: 106 | - markdown.extensions.toc: 107 | slugify: !!python/name:pymdownx.slugs.uslugify 108 | permalink: "" 109 | toc_depth: 3 110 | - pymdownx.arithmatex: 111 | - pymdownx.betterem: 112 | - pymdownx.caret: 113 | - pymdownx.critic: 114 | - pymdownx.keys: 115 | - pymdownx.tilde: 116 | - pymdownx.mark: 117 | - pymdownx.details: 118 | - pymdownx.emoji: 119 | emoji_index: !!python/name:materialx.emoji.twemoji 120 | emoji_generator: !!python/name:materialx.emoji.to_svg 121 | - pymdownx.highlight: 122 | linenums: true 123 | linenums_style: pymdownx-inline 124 | anchor_linenums: true 125 | - pymdownx.inlinehilite: 126 | custom_inline: 127 | - name: math 128 | class: arithmatex 129 | format: !!python/name:pymdownx.arithmatex.inline_mathjax_format 130 | - pymdownx.magiclink: 131 | repo_url_shortener: true 132 | repo_url_shorthand: true 133 | social_url_shorthand: true 134 | social_url_shortener: true 135 | normalize_issue_symbols: true 136 | - pymdownx.smartsymbols: 137 | - pymdownx.superfences: 138 | preserve_tabs: true 139 | custom_fences: 140 | - name: mermaid 141 | class: mermaid 142 | format: !!python/name:pymdownx.superfences.fence_code_format 143 | - name: math 144 | class: arithmatex 145 | format: !!python/name:pymdownx.arithmatex.fence_mathjax_format 146 | - pymdownx.tabbed: 147 | alternate_style: true 148 | - pymdownx.tasklist: 149 | custom_checkbox: true 150 | clickable_checkbox: false 151 | - pymdownx.escapeall: 152 | hardbreak: true 153 | nbsp: true 154 | - pymdownx.progressbar: 155 | - pymdownx.striphtml: 156 | - pymdownx.snippets: 157 | check_paths: true 158 | - pymdownx.saneheaders: 159 | 160 | docs_dir: Source 161 | 162 | nav: 163 | - Getting Started: index.md 164 | - 关于此书: Chapter0/README.md 165 | - Lambda in C++98/03: Chapter1/README.md 166 | - Lambda in C++11: Chapter2/README.md 167 | - Lambda in C++14: Chapter3/README.md 168 | - Lambda in C++17: Chapter4/README.md 169 | - Lambda in C++20: Chapter5/README.md 170 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # C++ Lambda Story - From C++98 to C++20 2 | ## 介绍 3 | 本文为《C++ Lambda Story》的中文翻译,如果您觉得此书有价值,可以在 [https://leanpub.com/cpplambda](https://leanpub.com/cpplambda) 上支持下原作者。 4 | 5 | 或者如果您认识相关的翻译工作者或者出版社,可以积极联系原作者与出版社进行正规的中文翻译并出版。 6 | 7 | ## 阅读 8 | [在线阅览](https://nolongerwait.com/cpp_lambda_story_chinese_edition/) 9 | 10 | ## 目录 11 | 12 | - [关于此书](Source/Chapter0/README.md) 13 | - [成书渊源](Source/Chapter0/README.md#成书渊源) 14 | - [阅读对象](Source/Chapter0/README.md#阅读对象) 15 | - [读者反馈](Source/Chapter0/README.md#读者反馈) 16 | - [代码证书](Source/Chapter0/README.md#代码证书) 17 | - [代码格式](Source/Chapter0/README.md#代码格式) 18 | - [语法高亮限制](Source/Chapter0/README.md#语法高亮限制) 19 | - [在线编译器](Source/Chapter0/README.md#在线编译器) 20 | - [关于作者](Source/Chapter0/README.md#关于作者) 21 | - [致谢](Source/Chapter0/README.md#致谢) 22 | - [校阅历史](Source/Chapter0/README.md#校阅历史) 23 | - 一、[Lambda in C++98/03](Source/Chapter1/README.md) 24 | - [C++98/03 中的可调用对象](Source/Chapter1/README.md#1-C++98/03-中的可调用对象) 25 | - [仿函数的一些问题](Source/Chapter1/README.md#2-仿函数的一些问题) 26 | - [使用辅助函数](Source/Chapter1/README.md#3-使用辅助函数) 27 | - [新特性的动机](Source/Chapter1/README.md#4-新特性的动机) 28 | - 二、[Lambda in C++11](Source/Chapter2/README.md) 29 | - [Lambda 表达式的语法](Source/Chapter2/README.md#1-Lambda-表达式的语法) 30 | - [Lambda 表达式的一些例子](Source/Chapter2/README.md#Lambda-表达式的一些例子) 31 | - [Lambda 在编译器的展开](Source/Chapter2/README.md#Lambda-在编译器的展开) 32 | - [Lambda 表达式的类型](Source/Chapter2/README.md#2-Lambda-表达式的类型) 33 | - [构造,还是拷贝?](Source/Chapter2/README.md#构造还是拷贝) 34 | - [调用操作符](Source/Chapter2/README.md#3-调用操作符) 35 | - [重载](Source/Chapter2/README.md#重载) 36 | - [其他修饰符](Source/Chapter2/README.md#其他修饰符) 37 | - [捕获](Source/Chapter2/README.md#4-捕获) 38 | - [`mutable` 关键字](Source/Chapter2/README.md#mutable-关键字) 39 | - [调用计数器 - 捕获变量的一个例子](Source/Chapter2/README.md#调用计数器---捕获变量的一个例子) 40 | - [捕获全局变量](Source/Chapter2/README.md#捕获全局变量) 41 | - [捕获静态变量](Source/Chapter2/README.md#捕获静态变量) 42 | - [捕获类成员和 `this` 指针](Source/Chapter2/README.md#捕获类成员和-this-指针) 43 | - [只能移动的对象](Source/Chapter2/README.md#只能移动的对象) 44 | - [保留常量](Source/Chapter2/README.md#保留常量) 45 | - [捕获参数包](Source/Chapter2/README.md#捕获参数包) 46 | - [返回类型](Source/Chapter2/README.md#5-返回类型) 47 | - [尾部返回类型语法](Source/Chapter2/README.md#尾部返回类型语法) 48 | - [转化为函数指针](Source/Chapter2/README.md#6-转化为函数指针) 49 | - [一个有趣的例子](Source/Chapter2/README.md#一个有趣的例子) 50 | - [IIFE - 立即调用函数表达式](Source/Chapter2/README.md#7-IIFE---立即调用函数表达式) 51 | - [可读性提示](Source/Chapter2/README.md#可读性提示) 52 | - [Lambda 继承](Source/Chapter2/README.md#8-Lambda-继承) 53 | - [在容器中存储 Lambda](Source/Chapter2/README.md#9-在容器中存储-Lambda) 54 | - [总结](Source/Chapter2/README.md#10.-总结) 55 | - 三、[Lambda in C++14](Source/Chapter3/README.md) 56 | - [为 Lambda 增加默认参数](Source/Chapter3/README.md#1.-为-Lambda-增加默认参数) 57 | - [返回类型](Source/Chapter3/README.md#2.-返回类型) 58 | - [带有初始化的捕获](Source/Chapter3/README.md#3-带有初始化的捕获) 59 | - [限制](Source/Chapter3/README.md#限制) 60 | - [对现有问题的改进](Source/Chapter3/README.md#对现有问题的改进) 61 | - [泛型 Lambda](Source/Chapter3/README.md#4-泛型-Lambda) 62 | - [可变泛型参数](Source/Chapter3/README.md#可变泛型参数) 63 | - [使用泛型 Lambda 进行完美转发](Source/Chapter3/README.md#使用泛型-Lambda-进行完美转发) 64 | - [减少一些隐蔽的类型纠正](Source/Chapter3/README.md#减少一些隐蔽的类型纠正) 65 | - [使用 Lambda 代替 std::bind1st 和 std::bind2nd](Source/Chapter3/README.md#5-使用-Lambda-代替-std::bind1st-和-std::bind2nd) 66 | - [使用现代 C++ 技术](Source/Chapter3/README.md#使用现代-C++-技术) 67 | - [函数组合](Source/Chapter3/README.md#函数组合) 68 | - [Lambda 提升(LIFTing with Lambda)](Source/Chapter3/README.md#6-Lambda-提升LIFTing-with-Lambda) 69 | - [递归 Lambda](Source/Chapter3/README.md#7-递归-Lambda) 70 | - [利用 std::function](Source/Chapter3/README.md#利用-std::function) 71 | - [内部 Lambda 和泛型参数](Source/Chapter3/README.md#内部-Lambda-和泛型参数) 72 | - [更多技巧](Source/Chapter3/README.md#更多技巧) 73 | - [使用递归 Lambda 是最好的选择吗?](Source/Chapter3/README.md#使用递归-Lambda-是最好的选择吗) 74 | - [总结](Source/Chapter3/README.md#8-总结) 75 | - 四、[Lambda in C++17](Source/Chapter4/README.md) 76 | - [Lambda 语法更新](Source/Chapter4/README.md#1-Lambda-语法更新) 77 | - [类型系统中的异常规范](Source/Chapter4/README.md#2-类型系统中的异常规范) 78 | - [constexpr Lambda 表达式](Source/Chapter4/README.md#3-constexpr-Lambda-表达式) 79 | - [用例](Source/Chapter4/README.md#用例) 80 | - [捕获变量](Source/Chapter4/README.md#捕获变量) 81 | - [constexpr 总结](Source/Chapter4/README.md#constexpr-总结) 82 | - [捕获 *this](Source/Chapter4/README.md#4-捕获-this) 83 | - [一些指导性意见](Source/Chapter4/README.md#一些指导性意见) 84 | - [IIFE 更新](Source/Chapter4/README.md#5-IIFE-更新) 85 | - [可变泛型 Lambda 的更新](Source/Chapter4/README.md#6-可变泛型-Lambda-的更新) 86 | - [从多个 Lambda 派生](Source/Chapter4/README.md#7-从多个-Lambda-派生) 87 | - [自定义模板参数推导规则](Source/Chapter4/README.md#自定义模板参数推导规则) 88 | - [聚合初始化的扩展](Source/Chapter4/README.md#聚合初始化的扩展) 89 | - [std::variant 和 std::visit 的例子](Source/Chapter4/README.md#std::variant-和-std::visit-的例子) 90 | - [使用 Lambda 进行并发编程](Source/Chapter4/README.md#8-使用-Lambda-进行并发编程) 91 | - [Lambda 和 std::thread](Source/Chapter4/README.md#Lambda-和-std::thread) 92 | - [Lambda 和 std::async](Source/Chapter4/README.md#Lambda-和-std::async) 93 | - [Lambda 和 C++17 的并行算法](Source/Chapter4/README.md#Lambda-和-C++17-的并行算法) 94 | - [Lambda 和异步 - 总结](Source/Chapter4/README.md#Lambda-和异步---总结) 95 | - [总结](Source/Chapter4/README.md#9-总结) 96 | - 五、[Lambda in C++20](Source/Chapter5/README.md) 97 | - [Lambda 语法更新](Source/Chapter5/README.md#1-Lambda-语法更新) 98 | - [更新快览](Source/Chapter5/README.md#2-更新快览) 99 | - [consteval Lambda](Source/Chapter5/README.md#3-consteval-Lambda) 100 | - [捕获参数包](Source/Chapter5/README.md#4-捕获参数包) 101 | - [模板 Lambda](Source/Chapter5/README.md#5-模板-Lambda) 102 | - [Concept 和 Lambda](Source/Chapter5/README.md#6-Concept-和-Lambda) 103 | - [无状态 Lambda 的变更](Source/Chapter5/README.md#7-无状态-Lambda-的变更) 104 | - [补充一些关于“未评估的 concept”](Source/Chapter5/README.md#补充一些关于未评估的-concept) 105 | - [Lambda 和 constexpr 算法](Source/Chapter5/README.md#8-Lambda-和-constexpr-算法) 106 | - [C++20 对重载模式的更新](Source/Chapter5/README.md#9-C++20-对重载模式的更新) 107 | - [总结](Source/Chapter5/README.md#10-总结) 108 | - 附录A - 技术名录 109 | - 附录B - 五大使用C++ Lambda的优势 110 | - 参考 111 | - 笔记 112 | 113 | ## 致谢 114 | 本书 Chapter 4 C++17 章节翻译感谢 [@Dup4](https://github.com/Dup4) 同学的支持。 115 | 在线阅览能力也感谢 [@Dup4](https://github.com/Dup4) 同学的支持。 116 | -------------------------------------------------------------------------------- /Source/Chapter1/README.md: -------------------------------------------------------------------------------- 1 | # 一、Lambda in C++98/03 2 | 3 | 凡是在开始之前,对主题的背景做出一些介绍总是好的。 4 | 5 | 所以,我们首先会聊一聊在没有现代 C++ 之前的那些 C++ 代码。 6 | 7 | 在本章,你可以学到: 8 | 9 | * 如何从标准库传递一个仿函数给算法 10 | * 仿函数和函数指针的局限性 11 | * 为什么辅助函数不够好使 12 | * C++0x/C++11 中添加新特性的动机 13 | 14 | ## 1. C++98/03 中的可调用对象 15 | 16 | 首先来聊聊标准库中基本思想之一的算法,像 `std::sort`,`std::for_each`,`std::transform` 等,可以调用任何可调用对象以及调用输入容器中的一个元素。 17 | 18 | 然而,在 C++98/03 中,这些操作只包含指向函数的指针或者仿函数。 19 | 20 | 举一个例子,我们来看一看一个打印 `vector` 中全部元素的应用程序。 21 | 22 | 在第一版中,我们将使用规范的函数: 23 | 24 | > 代码 1-1 [基础输出函数](https://wandbox.org/permlink/XiMBBTOG122vplUS) 25 | 26 | ```c++ 27 | #include 28 | #include 29 | #include 30 | 31 | void PrintFunc(int x) { 32 | std::cout << x < v; 37 | v.push_back(1); 38 | v.push_back(2); 39 | std::for_each(v.begin(), v.end(), PrintFunc); 40 | } 41 | ``` 42 | 43 | 上面的代码使用了 `std::for_each` 来从 `vector` 中迭代每个元素(请注意此时的 C++ 为 98/03 版本,尚不支持范围式循环),同时传递了一个可调用对象 `PrintFunc`。 44 | 45 | 我们可以将这个简单的函数转化为一个仿函数: 46 | 47 | > 代码 1-2 [基础输出仿函数](https://wandbox.org/permlink/7OGJzJlfg40SSQUG) 48 | 49 | ```cpp 50 | #include 51 | #include 52 | #include 53 | 54 | struct PrintFunctor { 55 | void operator()(int x) const { 56 | std::cout << x < v; 62 | v.push_back(1); 63 | v.push_back(2); 64 | std::for_each(v.begin(), v.end(), PrintFunctor()); 65 | } 66 | ``` 67 | 68 | 本用例重载了操作符 `()` 来定义了一个简单的仿函数。 69 | 70 | 相较于通常无状态的函数指针,仿函数能够持有成员变量来允许存储状态。 71 | 72 | 一个例子:统计在算法中调用可调用对象的次数。 73 | 74 | 这需要在仿函数中存储一个计数器,并且在每次 lambda 调用时更新计数: 75 | 76 | > 代码 1-3 [带有状态的仿函数]() 77 | 78 | ```cpp 79 | #include 80 | #include 81 | #include 82 | 83 | struct PrintFunctor { 84 | PrintFunctor(): numCalls(0){} 85 | 86 | void operator()(int x) const { 87 | std::cout << x < v; 96 | v.push_back(1); 97 | v.push_back(2); 98 | const PrintFunctor visitor = std::for_each(v.begin(), v.end(), PrintFunctor()); 99 | std::cout << "num calls: " << visitor.numCalls << '\n'; 100 | } 101 | ``` 102 | 103 | 在上面的例子中,我们使用了成员变量 `numCalls` 来统计调用操作符被调用的次数。 104 | 105 | 由于调用操作符是一个 `const` 成员函数,我使用了 `mutable` 类型的变量。 106 | 107 | 如您所料,我们得到的输出结果就是: 108 | 109 | ```plaintext 110 | 1 111 | 2 112 | num calls: 2 113 | ``` 114 | 115 | 我们也可以从调用范围中「捕获」变量。 116 | 117 | 想要达到这个效果,我们需要在仿函数中创建一个成员变量并且在构造器中初始化它。 118 | 119 | > 代码 1-4 [带有“捕获”变量的仿函数](https://wandbox.org/permlink/ogenCfT7ZCTbRIkZ) 120 | 121 | ```cpp 122 | #include 123 | #include 124 | #include 125 | #include 126 | 127 | struct PrintFunctor { 128 | PrintFunctor(const std::string& str) : strText(str), numCalls(0) {} 129 | void operator()(int x) const { 130 | std::cout << strText << x << '\n'; 131 | ++numCalls; 132 | } 133 | std::string strText; 134 | mutable int numCalls; 135 | }; 136 | 137 | int main() { 138 | std::vector v; 139 | v.push_back(1); 140 | v.push_back(2); 141 | const std::string introText("Elem: "); 142 | const PrintFunctor visitor = std::for_each(v.begin(), v.end(), PrintFunctor(introText)); 143 | std::cout << "num calls: " << visitor.numCalls << '\n'; 144 | } 145 | ``` 146 | 147 | 在这个版本中,`PrintFunctor` 使用了一个额外的参数来初始化成员变量。 148 | 149 | 然后这个变量在调用操作符中被使用。所以最终期望的输出是: 150 | 151 | ```plaintext 152 | Elem: 1 153 | Elem: 2 154 | num calls: 2 155 | ``` 156 | 157 | ## 2. 仿函数的一些问题 158 | 159 | 如您所见,仿函数的功能很强大。他们由一个独立的类所表示,您可以根据您的需要来设计、改造并使用它。 160 | 161 | 然而 C++98/03 问题在于需要在不同的地方编写一个函数或者仿函数,而不是算法调用对象本身。 162 | 163 | 这意味着这段代码会在源文件的中占用几十到上百行,而且这样分离的写法并不利于日后代码的维护。 164 | 165 | 一个可行的解决办法,那就是再编写一个本地仿函数类,因为 C++ 支持这样的语法,但是这不意味着它能如预期一样工作。 166 | 167 | 来看看这段代码: 168 | 169 | > 代码 1-5 本地仿函数类 170 | 171 | ```cpp 172 | int main() { 173 | struct PrintFunctor { 174 | void operator()(int x) const { 175 | std::cout << x << std::endl; 176 | } 177 | }; 178 | std::vector v(10, 1); 179 | std::for_each(v.begin(), v.end(), PrintFunctor()); 180 | } 181 | ``` 182 | 183 | 您可以用 GCC 来尝试编译它(带上 C++98 的标签 `-std=c++98`),当然不出意外,将会出现如下的编译错误: 184 | 185 | ```plaintext 186 | error: template argument for 187 | 'template _Funct 188 | std::for_each(_IIter, _IIter, _Funct)' 189 | uses local type 'main()::PrintFunctor' 190 | ``` 191 | 192 | 在 C++98/03 中,你不能用本地类型来初始化一个模板。 193 | 194 | 当认识到并理解了这些限制产生的原因,C++ 开发者就可以在 C++98/03 中找到一种解决办法:使用一组辅助函数。 195 | 196 | ## 3. 使用辅助函数 197 | 198 | 使用一些辅助函数或者预定义好的仿函数会如何呢? 199 | 200 | 如果您查阅过标准库中 `` 头文件的源码,你会发现一些可在标准算法中被立即使用的类型或者函数。 201 | 202 | 例如: 203 | 204 | * `std::plus()`- 传入两个参数并返回他们的和 205 | * `std::minus()`- 传入两个参数并返回他们的差 206 | * `std::less()`- 传入两个参数并判断第一个参数是否小于第二个参数 207 | * `std::greater_equal()`- 传入两个参数并判断第一个参数是否大于等于第二个参数 208 | * `std::bind1st`- 用给定的第一个参数创建一个可调用对象 209 | * `std::bind2nd`- 用给定的第二个参数创建一个可调用对象 210 | * 等等 211 | 212 | 让我们编写一些充分利用这些辅助函数的代码: 213 | 214 | > 代码 1-6 [使用旧 C++98/03 的辅助函数](https://wandbox.org/permlink/9KgfRwwC3Dza2ZVh) 215 | 216 | ```cpp 217 | #include 218 | #include 219 | #include 220 | 221 | int main() { 222 | std::vector v; 223 | v.push_back(1); 224 | v.push_back(2); 225 | // .. push back until 9... 226 | const size_t smaller5 = std::count_if(v.begin(), v.end(), std::bind2nd(std::less(), 5)); 227 | return smaller5; 228 | } 229 | ``` 230 | 231 | 这个例子使用 `std::less` 并且用 `std::bind2nd` 来固定第二个参数,同时,这个整体又被传入了 `std::count_if`。 232 | 233 | 您可能会猜到,这个代码可以展开为一个用来简单判断大小关系的函数:`return x < 5;` 234 | 235 | 如果你准备好使用等多的辅助函数,您也可以看看 `boost` 库,例如 `boost::bind`。 236 | 237 | 不幸的是,最主要的问题是这种方式十分的复杂并且语法不易学习。 238 | 239 | 举个例子,使用更多的辅助函数将会导致代码变得很不自然。 240 | 241 | 来看看这个: 242 | 243 | > 代码 1-7 [组合使用辅助函数](https://wandbox.org/permlink/D7XjbyM0i2nslhRU) 244 | 245 | ```cpp 246 | using std::placeholders::_1; 247 | std::vector v; 248 | v.push_back(1); 249 | v.push_back(2); 250 | // .. push back until 9... 251 | const size_t val = std::count_if(v.begin(), v.end(), 252 | std::bind( 253 | std::logical_and(), std::bind(std::greater(), _1, 2), std::bind(std::less_equal(), _1, 6))); 254 | // _1 comes from the std::placeholder namespace 255 | ``` 256 | 257 | 这个组合使用 `std::bind`(当然了 `std::bind` 是 C++11 的功能,而不是 C++98/03)并结合 `std::greater` 和 `std::less_equal` 甚至联系到 `std::logical_and`。 258 | 259 | 哦对,`_1` 是一个第一输入参数的占位符。 260 | 261 | 尽管上述代码有效,并且您可以在本地定义它,但您不得不忍痛它很复杂且语法不自然。 262 | 263 | 更何况这个组合只代表一个简单的条件:`return x > 2 && x <= 6;`。 264 | 265 | 有什么更好以及更自然的方式吗? 266 | 267 | ## 4. 新特性的动机 268 | 269 | 在 C++98/03 中,有很多方式来声明或者传递一个可调用对象给标准库的算法或者公用组件。 270 | 271 | 然而,所有的这些都一些限制。例如,你不能声明一个本地的仿函数对象,以及使用辅助函数组合起来的一个复杂表达。 272 | 273 | 幸运的是,在 C++11 中我们有了很多新的提升。 274 | 275 | 首先,C++ 委员会解除了使用本地类型的模板进行实例化的限制。 276 | 277 | 现在你可以在你需要的地方编写本地仿函数了。 278 | 279 | 还有,C++11 带来了另一个想法:如果编译器可以为开发人员“编写”小巧简洁的仿函数呢? 280 | 281 | 这意味着通过一些新语法,我们可以“就地”创建仿函数。 282 | 283 | C++ 从此开启了更简洁、更紧凑的语法的新篇章。 284 | 285 | 这就是 Lambda 表达式的诞生。 286 | 287 | 如果我们回头看看 [N3337](https://timsong-cpp.github.io/cppwp/n3337/) 草案——C++11 的最终草案,我们可以看到关于 lambda 的独立章节 [\[expr.prim.lambda\]](https://timsong-cpp.github.io/cppwp/n3337/expr.prim.lambda) 288 | 289 | 下个章节,我们将一起看看这个新的 C++ 特性。 290 | -------------------------------------------------------------------------------- /Source/Chapter5/README.md: -------------------------------------------------------------------------------- 1 | # 五、Lambda in C++20 2 | 3 | 2020 年 2 月,在捷克首都布拉格的会议上,ISO 委员会最终通过 C++20 标准,并宣布其将于 2020 年末正式发布。 4 | 5 | 新的标准规范为 C++ 语言本身和标准库都带来了诸多显著性的提升和改进!Lambda 表达式也得到了一些更新。 6 | 7 | 本章中,主要关注下列内容: 8 | 9 | * C++20 中的变化 10 | * 新的选择 - 捕获 `this` 指针 11 | * 模板 Lambda 12 | * 如何通过 `concepts` 提高泛型 Lambda 13 | * 如何在 Lambda 中使用 `constexpr` 算法 14 | * 如何使 `overloaded` 模式更加简短 15 | 16 | 你可以在 [N4681](https://timsong-cpp.github.io/cppwp/n4861/) 中的 [\[expr.prim.lambda\]](https://timsong-cpp.github.io/cppwp/n4861/expr.prim.lambda) 章节查阅标准规范中 Lambda 相关的内容。 17 | 18 | ## 1. Lambda 语法更新 19 | 20 | 在 C++20 中,Lambda 的语法得到了改进: 21 | 22 | * 现在可以在参数列表后添加 `consteval` 关键字 23 | * 现在明确模板尾(template tail)是可选的 24 | * 现在在尾部返回后,可以添加 `requires` 声明 25 | 26 | ```cpp 27 | [] () specifiers exception attr -> ret requires { /*code; */ } 28 | ^ ^ ^ ^ ^ 29 | | | | | | 30 | | | | | optional: trailing return type 31 | | | | | 32 | | | | optional: mutable, constexpr, consteval, noexcept, attributes 33 | | | | 34 | | | parameter list (optional when no specifiers added) 35 | | | 36 | | optional: template parameter list 37 | | 38 | lambda introducer with an optional capture list 39 | ``` 40 | 41 | ## 2. 更新快览 42 | 43 | C++20 中 Lambda 表达式的相关特性: 44 | 45 | * 允许 `[=, this]` 作为 Lambda 捕获 -[P0409R2](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0409r2.html) 并且弃用了通过 `[=]` 隐式捕获 `this`-[P0806](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0806r2.html) 46 | * 初始化捕获中的包扩展:`[...args = std::move(args)](){}`-[P0780](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0780r2.html) 47 | * `static`,`thread_local` 和 Lambda 捕获的结构化绑定 -[P1091](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p1091r3.html) 48 | * 模板 Lambda(带有 `concepts`)-[P0428R2](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0428r2.pdf) 49 | * 简化显式的 Lambda 捕获 -[P0588R1](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0588r1.html) 50 | * 默认可构造和可分配的无状态 Lambda -[P0624R2](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0624r2.pdf) 51 | * 未评估上下文的 Lambda -[P0315R4](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0315r4.pdf) 52 | * constexpr 算法 - 十分重要 [P0202](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0202r3.html),[P0879](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0879r0.html) 和 [P1645](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p1645r1.html) 53 | 54 | 如果想了解更多 C++20 的内容,你可以阅读此篇比较 C++17 和 C++20 的文章:[Changes between C++17 and C++20](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2020/p2131r0.html) 55 | 56 | 当然你也可以阅读我关于 C++20 语言和标准库特性的的卡片笔记:[Bartek's coding blog: C++20 Reference Card](https://www.cppstories.com/2020/01/cpp20refcard.html/) 57 | 58 | 快速预览下这些新的改变: 59 | 60 | 新添加的功能“清理”了 Lambda 语法。同时,C++20 也增强了部分功能,允许我们在高级场景中使用 Lambda。 61 | 62 | 例如,根据 [P1091](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p1091r3.html),我们可以捕获一个结构化绑定: 63 | 64 | > 代码 5-1 [在 Lambda 中捕获结构化绑定](https://wandbox.org/permlink/7d8oK3o5nP3wYbWB) 65 | 66 | ```cpp 67 | #include 68 | #include 69 | 70 | auto GetParams() { 71 | return std::tuple{std::string{"Hello World"}, 42}; 72 | } 73 | 74 | int main() { 75 | auto [x, y] = GetParams(); 76 | const auto ParamLength = [&x, &y]() { 77 | return x.length() + y; 78 | }(); 79 | return ParamLength; 80 | } 81 | ``` 82 | 83 | 一些编译器(如 GCC)甚至在 C++17 中就支持了捕获结构化绑定,即便当时的标准并未强制哟求。 84 | 85 | C++20 标准也有关于 `*this` 捕获的阐明。现在在方法中进行值捕获 `[=]` 会收到一条警告: 86 | 87 | > 代码 5-2 [隐式捕获 `*this` 的警告](https://wandbox.org/permlink/yRosU85B0Q9LnwOv) 88 | 89 | ```cpp 90 | struct Baz { 91 | auto foo() { 92 | return [=] { std::cout < 代码 5-3 [一个简单的即时 Lambda 函数](https://wandbox.org/permlink/3lFNMB080LBz2d1z) 117 | 118 | ```cpp 119 | int main() { 120 | const int x = 10; 121 | auto lam = [](int x) consteval { 122 | return x + x; 123 | }; 124 | return lam(x); 125 | } 126 | ``` 127 | 128 | 我们将新的关键字 `consteval` 放在了 Lambda 的参数列表之后,类似于 `constexpr` 的用法。严格的区别就在于,如果你将 `x` 的 `const` 移除,那么 `constexpr` Lambda 表达式仍旧可以在运行时工作,但是即时 Lambda 函数将无法成功编译。 129 | 130 | 默认情况下,如果 Lambda 函数体中遵循 `constexpr` 函数的规则,那么编译器会将调用操作符标记为隐式的 `constexpr`。 131 | 132 | 这并非 `consteval` 案例,因为它对类似这样的代码拥有更强的限制。 133 | 134 | 当然,这两个关键字无法同时使用。在草案 [P1073R3](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p1073r3.html) 中你可以找到与此相关的全部描述。 135 | 136 | ## 4. 捕获参数包 137 | 138 | C++20 中还对 Lambda 中初始化捕获的包扩展带来了一个提升: 139 | 140 | ```cpp 141 | template 142 | void call(Args&& ... args) { 143 | auto ret = [...capturedArgs = std::move(args)](){}; 144 | } 145 | ``` 146 | 147 | 先前,在 C++20 之前,这段代码是无法通过编译的(参考 C++11 章节中 [这部分](../Chapter2/README.md#捕获参数包) 内容),为了解决这个问题,需要将参数打包进一个单独的元组中去。 148 | 149 | 关于捕获限制相关的历史内容,你可以参考 [P0780](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0780r2.html) 中的描述。 150 | 151 | 综上所述,我们可以使用在 C++11 章节中有关捕获一个可变参数包的例子并在 C++20 中新特性的加持下实践下。 152 | 153 | 看下面的例子,利用折叠表达式来打印每个被捕获的参数: 154 | 155 | > 代码 5-4 [捕获可变参数包](https://wandbox.org/permlink/8Bjc78jm2OpfcOcN) 156 | 157 | ```cpp 158 | #include 159 | #include 160 | 161 | template 162 | void captureTest(First&& first, Args&&... args) { 163 | const auto printer = [first = std::move(first), ... capturedArgs = std::move(args)] { 164 | std::cout << first; 165 | ((std::cout << ", " << capturedArgs), ...); 166 | std::cout << '\n'; 167 | }; 168 | printer(); 169 | } 170 | 171 | int main() { 172 | auto ptr = std::make_unique(10); 173 | captureTest(std::move(ptr), 2, 3, 4); 174 | captureTest(std::move(ptr), 'a', 'b'); 175 | } 176 | ``` 177 | 178 | 输出: 179 | 180 | ```plaintext 181 | 0x1f0cb20, 2, 3, 4 182 | 0, a, b 183 | ``` 184 | 185 | 在示例中,我们使用了一个 `printer` 对象,它很类似在 C++17 中写过的那样,但是在这儿我们用来捕获变量而不是作为转发 Lambda 参数使用。 186 | 187 | 代码中甚至传递了一个 `unique` 指针。我们传递了两次并且你可以看到在第二次调用时得到的结果为 `0`,因为此时指针已经丢失了它对那块内存块的所有权。 188 | 189 | ## 5. 模板 Lambda 190 | 191 | C++14 中就已经引入了泛型 Lambda,并且可以在模板中将参数类型也声明为 `auto` 类型。 192 | 193 | 例如: 194 | 195 | ```cpp 196 | [] (auto x) { x; }; 197 | ``` 198 | 199 | 编译器会生成一个调用操作符对应以下的模板方法: 200 | 201 | ```cpp 202 | template 203 | void operator ()(T x) { x; } 204 | ``` 205 | 206 | 但是,这似乎没有办法去直接改变这个模板的参数,并且使用“真实”的模板参数。 207 | 208 | C++20 下,这都是可能的。 209 | 210 | 比如,如何限制 Lambda 仅对 `vector` 类型生效呢? 211 | 212 | 如下,有一个泛型 Lambda: 213 | 214 | ```cpp 215 | auto foo = [](auto& vec) { 216 | std::cout << std::size(vec) << '\n'; 217 | std::cout << vec.capacity() << '\n'; 218 | }; 219 | ``` 220 | 221 | 但是,如果你调用它并传入一个 `int` 参数(如 `foo(10)`),那你可能会遇到“晦涩难懂”的错误提示: 222 | 223 | ```cpp 224 | test.cc: In instantiation of 225 | 'main():: [with auto:1 = int]': 226 | test.cc:16:11: required from here 227 | test.cc:11:30: error: no matching function for call to 'size(const int&)' 228 | 11 | std::cout<< std::size(vec) << '\n'; 229 | ``` 230 | 231 | 在 C++20 中,可以这样写: 232 | 233 | ```cpp 234 | auto foo = [](std::vector const& vec) { 235 | std::cout << std::size(vec) << '\n'; 236 | std::cout << vec.capacity() << '\n'; 237 | }; 238 | ``` 239 | 240 | 它所对应的模板调用操作符为: 241 | 242 | ```cpp 243 | 244 | void operator()(std::vector const& s) { ... } 245 | ``` 246 | 247 | 这样模板参数就在捕获子句 `[]` 之后了。 248 | 249 | 现在进行类似 `foo(10)` 的调用,那么会收到一个较人性化的消息: 250 | 251 | ```plaintext 252 | note: mismatched types 'const std::vector'and 'int' 253 | ``` 254 | 255 | [上述例子](https://wandbox.org/permlink/gupbJfUfHHQ2y48q) 中,编译器会警告我们关于 Lambda 接口中的这个错误的匹配。 256 | 257 | 另外有一个重要的方面就是,在泛型 Lambda 的示例中,你只拥有一个变量而不是它的模板类型。 258 | 259 | 如果要访问类型,则需要使用 `decltype(x)`(对于带有 `auto x` 参数的 Lambda)。 260 | 261 | 这将会使得你的代码变得冗长。 262 | 263 | 例如(使用了 [P0428](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0428r2.pdf) 中的代码): 264 | 265 | > 代码 5-5 从泛型参数中推断 266 | 267 | ```cpp 268 | auto f = [](auto const& x) { 269 | using T = std::decay_t; 270 | T copy = x; 271 | T::static_function(); 272 | using Iterator = typenameT::iterator; 273 | } 274 | ``` 275 | 276 | 现在可以这样编写: 277 | 278 | > 代码 5-6 使用模板 Lambda 279 | 280 | ```cpp 281 | auto f = [](T const& x) { 282 | T copy = x; 283 | T::static_function(); 284 | using Iterator = typenameT::iterator; 285 | } 286 | ``` 287 | 288 | 和明显,在第一种写法中,我们不得不使用 289 | 290 | ```cpp 291 | using T = std::decay_t; 292 | ``` 293 | 294 | 为了得到输入参数的类型,在 C++20 版本中,没有必要去访问模板参数了。 295 | 296 | 除此之外,还有一个重要的使用场景就是在可变泛型 Lambda 中进行完美转发: 297 | 298 | ```cpp 299 | // C++17 300 | auto ForwardToTestFunc = [](auto&&... args) { 301 | // what's the type of `args` ? 302 | return TestFunc(std::forward(args)...); 303 | }; 304 | ``` 305 | 306 | 每次你想要访问模板参数的类型是,你都需要去使用 decltype (),但是在模板 lambda 中就不需要了: 307 | 308 | ```cpp 309 | // C++20 310 | auto ForwardToTestFunc = [](T && ... args) { 311 | return TestFunc(std::forward(args)...); // we have allthe types! 312 | }; 313 | ``` 314 | 315 | 怎么样?模板 Lambda 提供了更为清晰的语法和更好的访问参数类型的途径。 316 | 317 | 当然,这还不够,你甚至也可以在 Lambda 使用 `concept`,咱们接着往下看。 318 | 319 | ## 6. Concept 和 Lambda 320 | 321 | `concept` 是编写模板的一项革命性进步。 322 | 323 | 它将允许你对模板参数进行约束,这可以极大提高代码的可读性,可能提升编译速度甚至能够提供更友善的错误信息。 324 | 325 | 话不多说,看个简单的示例吧: 326 | 327 | > 代码 5-7 一个普通的 `concept` 声明 328 | 329 | ```cpp 330 | // define a concept: 331 | template 332 | concept SignedIntegral = std::is_integral_v && std::is_signed_v; 333 | // use: 334 | template 335 | void signedIntsOnly(T val) {} 336 | ``` 337 | 338 | 我们首先创建了一个 `concept` 描述类型为有符号的并且是整形。 339 | 340 | 请注意我们可以已有的类型特征。 341 | 342 | 之后,我们使用她来定义一个仅支持能匹配 `concept` 类型的模板函数。 343 | 344 | 在这我们没有使用 `typename T`,但是我们可以引用一个 `concept` 名字。 345 | 346 | 好了,简单了解了 `concept` 之后,那么怎么跟 Lambda 关联起来呢? 347 | 348 | 关键部分就在于精炼语法以及约束 `auto` 模板参数。 349 | 350 | ### 简化和精炼的语法 351 | 352 | 得益于 `concept` 精炼的语法特性,你也可以不用在编写模板时候带有 `template` 部分了。 353 | 354 | 使用无约束的 `auto`: 355 | 356 | ```cpp 357 | void myTemplateFunc (auto param) {} 358 | ``` 359 | 360 | 使用有约束的 auto: 361 | 362 | ```cpp 363 | void signedIntsOnly (SignedIntegral auto val) {} 364 | void floatsOnly (std::floating_point auto fp) {} 365 | ``` 366 | 367 | 这些语法跟在 C++14 中编写泛型 Lambda 时很像,当然,现在你可以这样做: 368 | 369 | ```cpp 370 | void myTemplateFunction (auto val) {} 371 | ``` 372 | 373 | 换句话说,对于 lambda,我们可以利用它精炼的风格,例如对泛型 Lambda 参数添加额外的限制。 374 | 375 | ```cpp 376 | auto GenLambda = [](SignedIntegral auto param) { return param * param + 1; } 377 | ``` 378 | 379 | 上面的例子利用 `SignedIntegral` 来限制 `auto` 参数。 380 | 381 | 但是整个表达式比起模板 Lambda 看上去更加的可读,这就是为什么我们要着重讨论的点了。 382 | 383 | 来一个有点难度的例子吧,我们甚至可以为一些类的接口定义 `concept`: 384 | 385 | > 代码 5-8 IRenderable concept, with requires keyword 386 | 387 | ```cpp 388 | template 389 | concept IRenderable = requires(T v) { 390 | { v.render() } -> std::same_as; 391 | { v.getVertCount() } -> std::convertible_to; 392 | }; 393 | ``` 394 | 395 | 上面这个例子定义了一个带有 render () 和 getVertCount () 成员函数,用来匹配全部类型的 concept。 396 | 397 | 使用它来写一个泛型 Lambda 试试: 398 | 399 | > 代码 5-9 [IRenderable concept/Interface 的实现](https://wandbox.org/permlink/5jLMVJIckSvDdgMv) 400 | 401 | ```cpp 402 | #include 403 | #include 404 | 405 | template 406 | concept IRenderable = requires(T v) { 407 | { v.render() } -> std::same_as; 408 | { v.getVertCount() } -> std::convertible_to; 409 | }; 410 | 411 | struct Circle { 412 | void render() { 413 | std::cout << "drawing circle\n"; 414 | } 415 | size_t getVertCount() const { 416 | return 10; 417 | }; 418 | }; 419 | 420 | struct Square { 421 | void render() { 422 | std::cout << "drawing square\n"; 423 | } 424 | size_t getVertCount() const { 425 | return 4; 426 | }; 427 | }; 428 | 429 | int main() { 430 | const auto RenderCaller = [](IRenderable auto& obj) { 431 | obj.render(); 432 | }; 433 | Circle c; 434 | RenderCaller(c); 435 | Square s; 436 | RenderCaller(s); 437 | } 438 | ``` 439 | 440 | 这个例子中 `RenderCaller` 就是一个泛型 `Lambda`,并且支持类型必须满足 `IRenderable concept`。 441 | 442 | ## 7. 无状态 Lambda 的变更 443 | 444 | 也许你会想起来 C++11 中我们提过的无状态、甚至没有默认构造化的 Lambda。 445 | 446 | 然而,这个限制在 C++20 中被解除了。 447 | 448 | 这就是为什么假如你的 Lambda 没有捕获任何东西的情况下,你也可以写下如下的代码: 449 | 450 | > 代码 5-10 [一个无状态 Lambda](https://wandbox.org/permlink/nWXXJiZyk8ZhVej9) 451 | 452 | ```cpp 453 | #include 454 | #include 455 | #include 456 | 457 | struct Product { 458 | std::string _name; 459 | int _id{0}; 460 | double _price{0.0}; 461 | }; 462 | 463 | int main() { 464 | const auto nameCmp = [](const auto& a, const auto& b) { 465 | return a._name < b._name; 466 | }; 467 | const std::set prodSet{ 468 | {"Cup", 10, 100.0}, {"Book", 2, 200.5}, {"TV set", 1, 2000}, {"Pencil", 4, 10.5}}; 469 | for (const auto& elem : prodSet) 470 | std::cout << elem._name << '\n'; 471 | } 472 | ``` 473 | 474 | 例子中我声明了一个集合用来存储一系列的产品。 475 | 476 | 同时我需要一个办法来比较这些产品,所以我传入了一个无状态的 Lambda 用来比较他们的产品名。 477 | 478 | 如果用 C++17 编译,那么你会收获如下关于使用了删除默认构造器的错误说明: 479 | 480 | ```plaintext 481 | test.h: In constructor 482 | 'std::set<_Key, _Compare, _Alloc>... 483 | [with _Key = Product; 484 | _Compare = main()::; 485 | ...' 486 | test.h:244:29: error: use of deleted function 487 | 'main()::::()' 488 | ``` 489 | 490 | 但是在 C++20 中,你可以存储无状态 Lambda,甚至可以拷贝他们: 491 | 492 | > 代码 5-11 [存储无状态 Lambda](https://wandbox.org/permlink/wTMFVluKdDbsLyOK) 493 | 494 | ```cpp 495 | template 496 | struct Product { 497 | int _id{0}; 498 | double _price{0.0}; 499 | F _predicate; 500 | }; 501 | 502 | int main() { 503 | const auto idCmp = [](const auto& a) noexcept { 504 | return a._id != 0; 505 | }; 506 | Product p{10, 10.0, idCmp}; 507 | [[maybe_unused]] auto p2 = p; 508 | } 509 | ``` 510 | 511 | ### 补充一些关于“未评估的 concept” 512 | 513 | 还有一些与高级用例相关的变化,比如未评估的 `concept`。 514 | 515 | 连同默认的可构造 Lambda,您现在可以编写这样的代码: 516 | 517 | ```cpp 518 | std::mapy; })> map; 519 | ``` 520 | 521 | 如您所见,现在可以在声明映射容器中指定 Lambda。它可以用作比较器仿函数。 522 | 523 | 这种“未评估 `concept`”对于高级模板元编程特别方便。 524 | 525 | 例如,在该功能的提案中,作者提到在编译时使用断言对元组对象进行排序,该断言是一个 Lambda。 526 | 527 | 更多的内容可以参考 [P0315R2](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0315r2.pdf)。 528 | 529 | ## 8. Lambda 和 `constexpr` 算法 530 | 531 | 回想一下之前章节中的内容,自 C++17 依赖,我们可以使用 `constexpr` Lambda。 532 | 533 | 并且,由于这项功能,我们可以传递 Lambda 给一个需要在编译器评估的函数。 534 | 535 | 在 C++20 中大多数标注算法都可以被关键字 `constexpr` 标记,这使得 `constexpr` Lambda 用起来更加方便了。 536 | 537 | 看一些例子吧还是。 538 | 539 | > 代码 5-12 [在普通的 constexpr Lambda 中使用 std::accumulate ()](https://godbolt.org/z/Tqkphs) 540 | 541 | ```cpp 542 | #include 543 | #include 544 | int main() { 545 | constexpr std::array arr{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; 546 | // with constexpr lambda 547 | static_assert(std::accumulate(begin(arr), end(arr), 0, [](auto a, auto b) noexcept { 548 | return a + b; 549 | }) == 55); 550 | return arr[0]; 551 | } 552 | ``` 553 | 554 | 本例中,在 Lambda 中使用 `std::accumulate`,实际上使用的还是 `std::plus` 操作。 555 | 556 | 下个例子中,使用了一个带有 `cmp` 比较器 `cout_if` 算法的 `constexpr` 函数。 557 | 558 | > 代码 5-13 [给普通函数中传入一个 `constexpr` Lambda](https://godbolt.org/z/ouJ_4q) 559 | 560 | ```cpp 561 | #include 562 | #include 563 | constexpr auto CountValues(auto container, auto cmp) { 564 | return std::count_if(begin(container), end(container), cmp); 565 | } 566 | int main() { 567 | constexpr auto minVal = CountValues(std::array{-10, 6, 8, 4, -5, 2, 4, 6}, [](auto a) { 568 | return a >= 0; 569 | }); 570 | return minVal; 571 | } 572 | ``` 573 | 574 | > 哪些标准算法是可以 `constexpr` 的呢? 575 | > 所有 ``,`` 和 `` 头文件中的算法现在都可以被关键字 `constexpr` 标记。除了 `shuffle`,`sample`,`stable_sort`,`stable_partition`,`inplace_merge` 这些,以及接受执行策略参数的函数或重载函数。 576 | > 具体的内容可以查阅 [P0202](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0202r3.html),[P0879](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0879r0.html) 和 [P1645](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p1645r1.html)。 577 | 578 | ## 9. C++20 对重载模式的更新 579 | 580 | 在前一章中,学习过如何从多个 Lambda 表达式派生并通过重载模式暴露它们。 581 | 582 | 这种技术对于 `std::variant` 访问很方便。 583 | 584 | 得益于 C++20 中类模板参数推断(CTAD,Class Template Argument Deduction)的更新,现在可以用更简短的语法来实现了。 585 | 586 | 为什么? 587 | 588 | 这是因为在 C++20 中有 CTAD 的扩展并且会自动处理聚合。 589 | 590 | 这意味着无需编写自定义的推断。 591 | 592 | 来一个简单的例子: 593 | 594 | ```cpp 595 | template 596 | struct Triple { T t; U u; V v; }; 597 | ``` 598 | 599 | 在 C++20 中的写法: 600 | 601 | ```cpp 602 | Triple ttt { 10.0f, 90, std::string{"hello"}}; 603 | ``` 604 | 605 | `T` 将被自动推断为 `float`,`U` 为 `int`,`V` 为 `std::string`。 606 | 607 | C++20 中的重载模式: 608 | 609 | ```cpp 610 | template struct overload: Ts... { using Ts:: operator()...; }; 611 | ``` 612 | 613 | 这个特性的草案可以在 [P1021](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p1021r5.html) 和 [P1816](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p1816r0.pdf) 中查阅。 614 | 615 | > GCC10 似乎实现了这个提议,但是它不适用于继承的高级案例。因此我们需要等待 GCC 对该特性进行完整的支持。 616 | 617 | ## 10. 总结 618 | 619 | 在本章中,我们回顾了 C++20 带来的变化。 620 | 621 | 首先,一些澄清和改进:例如捕获 `this`、捕获结构化绑定或默认构造无状态 Lambda 的能力。 622 | 623 | 更重要的是,还有更多重要的补充! 624 | 625 | 现在突出的功能之一是模板 Lambdas 和概念。 626 | 627 | 这样您就可以更好地控制通用 Lambdas。 628 | 629 | 总而言之,使用 C++20 及其所有功能,使得 Lambda 愈发成为更强大的工具! 630 | -------------------------------------------------------------------------------- /Source/Chapter3/README.md: -------------------------------------------------------------------------------- 1 | # 三、Lambda in C++14 2 | 3 | C++14 为 Lambda 表达式提供了两个显著的增强特性 4 | 5 | * 带有初始化的捕获 6 | * 泛型 Lambda 7 | 此外,该标准还更新了一些规则,例如: 8 | * Lambda 表达式的默认参数 9 | * `auto` 返回类型 10 | 11 | 这些新增特性可以在 [N4140](https://timsong-cpp.github.io/cppwp/n4140/) 中的 Lambda 部分 [\[expr.prim.lambda\]](https://timsong-cpp.github.io/cppwp/n4140/expr.prim.lambda) 找到。 12 | 13 | 在本章中,你将学到: 14 | 15 | * 捕获成员变量 16 | * 用现代 C++ 技术代替旧功能,如 `std::bind1st` 17 | * LIFTING 18 | * 递归 Lambda 19 | 20 | ## 1. 为 Lambda 增加默认参数 21 | 22 | 让我们从小的变化说起吧: 23 | 24 | 在 C++14 中,你可以在 Lambda 调用中使用默认参数了。这一小小的更新让 Lambda 函数更像一个常规函数了。 25 | 26 | > 代码 3-1 [带有默认参数的 Lambda](https://wandbox.org/permlink/T2u5iuGqi3fHaN9q) 27 | 28 | ```cpp 29 | #include 30 | 31 | int main() { 32 | const auto lam = [](int x = 10) { 33 | std::cout << x << '\n'; 34 | }; 35 | lam(); 36 | lam(100); 37 | } 38 | ``` 39 | 40 | 见用例所示,我们可以调用这个 Lambda 两次:第一次不携带任何参数,结果将输出默认的 `10`,第二次我们传递参数 `100` 进去,结果会输出 `100`。 41 | 42 | 不过,这一特性早已在 GCC 和 Clang 的 C++11 版本中被支持了。 43 | 44 | ## 2. 返回类型 45 | 46 | 如果你还记得之前章节的内容,那么你一定知道,对于一个简单的 Lambda,编译器可以推断出它的返回类型。 47 | 48 | 这个功能是在常规函数上“扩展”的,在 C++14 中你可以使用 `auto` 作为返回类型 49 | 50 | ```cpp 51 | auto myFunction() { 52 | int x =computeX(...); 53 | int y =computeY(...); 54 | return x +y; 55 | } 56 | ``` 57 | 58 | 如上,编译器会推断返回类型为 `int`。 59 | 60 | 推断返回类型的这部分内容在 C++14 中得到了改善和扩展。对于 Lambda 表达式来说,这意味着他们可以和常规函数享有同样的 `auto` 返回类型([\[expr.prim.lambda\]](https://timsong-cpp.github.io/cppwp/n4140/expr.prim.lambda#4)): 61 | 62 | > 如果 Lambda 返回类型是 `auto`,那么它会被尾部返回类型所替代(如果提供了)或者从 `return` 语句中推导。详见 [\[dcl.spec.auto\]](https://timsong-cpp.github.io/cppwp/n4140/dcl.spec.auto) 63 | 64 | 如果在 Lambda 中有多条返回语句,他们必须能够推断出同样的类型: 65 | 66 | ```cpp 67 | auto foo = [](int x){ 68 | if (x < 0) 69 | return x * 1.1f 70 | else 71 | return x * 2.1 72 | } 73 | ``` 74 | 75 | 这段代码就无法成功编译了,因为第一条返回语句返回 `float` 类型但第二条返回 `double` 类型。 76 | 77 | 编译器无法决定出到底应该将返回类型定为哪个,所以您必须选择其中一个,保证返回类型的唯一性。 78 | 79 | 尽管推断整形和双精度型也是很有用的,但是推断返回类型之所以有更显著的价值,是因为它可以在模板代码这种“未知”领域发挥极大地在作用。 80 | 81 | 举个例子,Lambda 闭包类型是匿名的,并且我们无法显式的明确它。 82 | 83 | 但是如果你想从函数中返回一个 Lambda 呢?你要如何明明确这个类型? 84 | 85 | 在 C++14 之前,你可以用 `std::function`: 86 | 87 | > 代码 3-2 [返回 `std::function`](https://wandbox.org/permlink/oCij1KoIB8RVOvSI) 88 | 89 | ```cpp 90 | #include 91 | #include 92 | 93 | std::function CreateMulLambda(int x) { 94 | return [x](int param) noexcept { 95 | return x * param; 96 | }; 97 | } 98 | 99 | int main() { 100 | const auto lam = CreateMulLambda(10); 101 | std::cout << sizeof(lam); 102 | return lam(2); 103 | } 104 | ``` 105 | 106 | 然而,上面这种方法并不足够直接。它要求你明确了一个函数签名,甚至包含了额外的头文件 ``。如果你还记得 C++11 的内容的话,`std::function` 是一个“笨重”的对象(在 GCC9 中,`function` 的 `sizeof` 是 32 bytes)。并且,它需要一些高级的内部机制,以便它可以处理任何可调用的对象。 107 | 108 | 感谢 C++14 带来的改进,我们可以极大的简化上面的代码: 109 | 110 | > 代码 3-3 [Lambda 推断的 `auto` 返回类型](https://wandbox.org/permlink/RLEHfrCk29aqRn8X) 111 | 112 | ```cpp 113 | #include 114 | 115 | auto CreateMulLambda(int x) noexcept { 116 | return [x](int param) noexcept { 117 | return x * param; 118 | }; 119 | } 120 | 121 | int main() { 122 | const auto lam = CreateMulLambda(10); 123 | std::cout << sizeof(lam); 124 | return lam(2); 125 | } 126 | ``` 127 | 128 | 现在我们就可以完全依靠编译时的类型推导,不需要其他辅助类型。 129 | 130 | 在 GCC 上,最后 `lam` 这个返回的 Lambda 对象的大小仅为 4 字节,并且比使用 `std::function` 的解决方案便宜得多。 131 | 132 | 这里有一点需要注意,我们也可以将 `CreateMulLambda` 标记为 `noexcept`,这样无论如何它都不可以抛出任何异常。 133 | 134 | 但是 `std::function` 就不行了。 135 | 136 | ## 3. 带有初始化的捕获 137 | 138 | 现在我们来讲讲更加具有建设性的更新。 139 | 140 | 你一定记得,在 Lambda 表达式中,你可以从外部范围中捕获变量。 141 | 142 | 编译器会拓展你的捕获语法并且在闭包类型中创建成员变量(非静态数据成员)。 143 | 144 | 现在在 C++14 中,你可以创建一个新的成员变量并且在捕获语句中初始化他们。 145 | 146 | 这样你就可以在 Lambda 内部访问那些变量了。 147 | 148 | 这叫做 **通过初始化器捕获** 或者你也可以用另一个名字 **广义 Lambda 捕获**。 149 | 150 | 看个简单的例子: 151 | 152 | > 代码 3-4 [通过初始化器捕获](https://wandbox.org/permlink/461XKCYNsQSKQeKO) 153 | 154 | ```cpp 155 | #include 156 | 157 | int main() { 158 | int x = 30; 159 | int y = 12; 160 | const auto foo = [z = x + y]() { 161 | std::cout << z << '\n'; 162 | }; 163 | 164 | x = 0; 165 | y = 0; 166 | foo(); 167 | } 168 | ``` 169 | 170 | 输出为 171 | 172 | ```plaintext 173 | 42 174 | ``` 175 | 176 | 在这个例子中,编译会生成一个新的成员变量并且将其初始化为 `x + y`。 177 | 178 | 这个新变量的类型会被自动推断出来,即便你在变量前加上了 `auto` 关键字: 179 | 180 | ```cpp 181 | auto z = x + y 182 | ``` 183 | 184 | 总之,前面示例中的 Lambda 会被解析为以下(简化的)仿函数: 185 | 186 | ```cpp 187 | struct _unnamedLambda { 188 | void operator()() const{ 189 | std::cout << z << '\n'; 190 | } 191 | 192 | int z; 193 | } someInstance; 194 | ``` 195 | 196 | 当 Lambda 的表达式定义完成时,`z` 将会被直接初始化 `x + y`。 197 | 198 | 上面这句的含义就是:新变量在你定义 Lambda 的地方初始化,而不是你调用它的地方。 199 | 200 | 这就是为什么如果你在创建 Lambda 后修改 `x` 或者 `y` 变量,变量 `z` 的值不会改变。 201 | 202 | 在示例中,你可以看到在定义 Lambda 之后,我立即更改了 `x` 和 `y` 的值。 203 | 204 | 然而,输出仍将是 42,因为 `z` 在这之前就已经被初始化。 205 | 206 | 当然,通过初始化器创建变量也可以是灵活的,不妨看看下面这个例子:创建一个外部范围的引用变量。 207 | 208 | > 代码 3-5 [通过初始化器进行引用捕获](https://wandbox.org/permlink/TVb2allLLdRQ1aPe) 209 | 210 | ```cpp 211 | #include 212 | 213 | int main() { 214 | int x = 30; 215 | const auto foo = [&z = x]() { 216 | std::cout << z << '\n'; 217 | }; 218 | foo(); 219 | x = 0; 220 | foo(); 221 | } 222 | ``` 223 | 224 | 这次,变量 `z` 是引用自变量 `x`,当然你也可以写成这样 `auto & z = x`。 225 | 226 | 如果运行这段代码,你应该可以看到,第一行会输出 30,但是第二行会输出 `0`。 227 | 228 | 这是因为我们进行了一个引用捕获,当你修改了引用内容时,对象 `z` 自然也会随之变化。 229 | 230 | ### 限制 231 | 232 | 需要注意,在使用初始化器捕获时,有一些限制: 233 | 234 | 一个是,当你通过初始化器进行引用捕获时,她不可能写入一个右值引用 `&&`。这是因为如下的代码目前是非法的: 235 | 236 | ```cpp 237 | [&& z = x] //非法语法 238 | ``` 239 | 240 | 另一个该特性的限制是,它不允许传入参数包。在条款 [\[expr.prim.lambda\]](https://timsong-cpp.github.io/cppwp/n4140/expr.prim.lambda#24) 的 24 节可以阅读到如下内容: 241 | 242 | 带有省略号的简单捕获是包扩展([\[temp.variadic\]](https://timsong-cpp.github.io/cppwp/n4140/temp.variadic)),但是 init-capture 带有省略号是格式错误。 243 | 244 | 简而言之,在 C++14 中,你并不能这样写代码: 245 | 246 | ```cpp 247 | template < class.. Args > 248 | auto captureTest(Args... args) { 249 | return lambda = [...capturedArgs = std::move(args)](){}; 250 | ... 251 | ``` 252 | 253 | 但是,这个语法,在 C++20 中是支持的,如果想提前了解,可以参考 [这个]()。 254 | 255 | ### 对现有问题的改进 256 | 257 | 总而言之,这个新的 C++14 特性可以解决一些问题,例如 仅可移动类型 或 允许一些额外的优化。 258 | 259 | #### Move 移动 260 | 261 | 在 C++11 中,你无法通过值捕获的方式捕获一个唯一指针(`unique_pointer`),只能进行引用捕获。但是现在在 C++14 中,我们可以移动一个对象到闭包类型的成员中: 262 | 263 | > 代码 3-6 [捕获一个仅可移动类型](https://wandbox.org/permlink/n65fzPHrNnyDqbIK) 264 | 265 | ```cpp 266 | #include 267 | #include 268 | 269 | int main() { 270 | std::unique_ptr p(new int{10}); 271 | const auto bar = [ptr = std::move(p)] { 272 | std::cout << "pointer in lambda: " << ptr.get() << '\n'; 273 | }; 274 | std::cout << "pointer in main(): " << p.get() << '\n'; 275 | bar(); 276 | } 277 | ``` 278 | 279 | 输出: 280 | 281 | ```plaintext 282 | pointer in main(): 0 283 | pointer in lambda: 0x1413c20 284 | ``` 285 | 286 | 有了捕获初始化器,你就可以移动一个指针的所有权到 Lambda 中。如你所见,在上面这个例子中,唯一指针在闭包对象被创建后立即被设为了 `nullptr`。 287 | 288 | 但是当你调用这个 Lambda 时,你会看见一个合法的内存地址。 289 | 290 | #### `std::function` 中的陷阱 291 | 292 | 在 lambda 中拥有一个仅可移动的捕获变量会让闭包对象变得不能被拷贝。 293 | 294 | 当你想在 `std::function` 中存储一个 Lambda,而这个 Lambda 接受仅可拷贝的可调用对象的时候,就会出现问题。 295 | 296 | 我们在 C++ Insights 上观察一下之前的一个例子([在线预览](https://cppinsights.io/s/5d11eb8f)),你会发现 `std::unique_ptr` 是一个闭包类型的成员变量。 297 | 298 | 但是,拥有一个仅可移动的成员会阻止编译器创建一个默认拷贝构造的。 299 | 300 | 简而言之,这段代码无法编译: 301 | 302 | > 代码 3-7 `std::function` 和 `std::move` 303 | 304 | ```cpp 305 | std::unique_ptr p(new int{10}); 306 | std::function fn = [ptr = std::move(p)](){}; //不可编译 307 | ``` 308 | 309 | 如果您想要完整的细节,您还可以查看草案([P0288]())中的 any\_invokable,这是 `std::function` 未来可能的改进,并且还会处理仅可移动类型。 310 | 311 | #### 优化 Optimisation 312 | 313 | 有一个将捕获初始化器作为潜在的性能优化的点子:我们可以在初始化器中计算一次,而不是每次调用 Lambda 时都计算某个值: 314 | 315 | > 代码 3-8 [给 Lambda 创建一个 `string`](https://wandbox.org/permlink/GWcJNoUsBFnscOp3) 316 | 317 | ```cpp 318 | #include 319 | #include 320 | #include 321 | #include 322 | 323 | int main() { 324 | using namespace std::string_literals; 325 | const std::vector vs = {"apple", "orange", "foobar", "lemon"}; 326 | const auto prefix = "foo"s; 327 | auto result = std::find_if(vs.begin(), vs.end(), [&prefix](const std::string& s) { 328 | return s == prefix + "bar"s; 329 | }); 330 | if (result != vs.end()) 331 | std::cout << prefix << "-something found!\n"; 332 | result = std::find_if(vs.begin(), vs.end(), [savedString = prefix + "bar"s](const std::string& s) { 333 | return s == savedString; 334 | }); 335 | if (result != vs.end()) 336 | std::cout << prefix << "-something found!\n"; 337 | } 338 | ``` 339 | 340 | 上面的代码对 `std::find_if` 调用了两次。在第一个场景中,我们捕获 `prefix` 并将输入值与 `prefix + "bar"s` 进行比较。 341 | 342 | 每次调用 Lambda 时,都必须创建并计算一个临时值来存储这些字符串的总和。 343 | 344 | 第二次调用 `find_if` 优化:我们创建了一个捕获的变量 `savedString` 来计算字符串的总和。 345 | 346 | 然后,我们可以安全地在 Lambda 体中引用它。 347 | 348 | 字符串的总和只会运行一次,而不是每次调用 lambda 时都会运行。 349 | 350 | 该示例还使用了 `std::string_literals`,这就是为什么我们可以编写代表 `std::string` 对象的 `"foo"s`。 351 | 352 | #### 捕获成员变量 353 | 354 | 初始化器也被用来捕获成员变量。我们可以捕获一个成员变量的拷贝并且不用担心悬空引用。 355 | 356 | 看个例子吧: 357 | 358 | > 代码 3-9 [捕获一个成员变量](https://wandbox.org/permlink/E65tipdkDj2nrdF5) 359 | 360 | ```cpp 361 | #include 362 | #include 363 | 364 | struct Baz { 365 | auto foo() const { 366 | return [s = s] { 367 | std::cout << s << std::endl; 368 | }; 369 | } 370 | 371 | std::string s; 372 | }; 373 | 374 | int main() { 375 | const auto f1 = Baz{"abc"}.foo(); 376 | const auto f2 = Baz{"xyz"}.foo(); 377 | f1(); 378 | f2(); 379 | } 380 | ``` 381 | 382 | 在 `foo()` 中我们通过拷贝的方式将成员变量拷贝进了闭包类型中。 383 | 384 | 此外,我们使用 `auto` 来进行成员函数 `foo()` 返回类型的推断。 385 | 386 | 当然,在 C++11 中,你也可以使用 `std::function`,详见 [捕获成员变量和 `this` 指针](../Chapter2/README.md#捕获类成员和-`this`-指针)。 387 | 388 | 在这里我们在 lambda 中使用了一个很“奇怪”的语法 `[ s = s ]`,这段代码能够工作的原因是捕获到的变量是在闭包类型内部的,而非外部。所以这里就没有歧义冲突了。 389 | 390 | ## 4. 泛型 Lambda 391 | 392 | 这是 C++14 中有关 Lambda 的最大的更新! 393 | 394 | Lambda 的早期规范允许我们创建匿名函数对象并将它们传递给标准库中的各种泛型算法。 395 | 396 | 然而,闭包本身并不是“泛型”的。例如,您不能将模板参数指定为 Lambda 的参数。 397 | 398 | 当然,在 C++14 中,标准引入了 **泛型 Lambda** 现在我们可以这样写: 399 | 400 | ```cpp 401 | const auto foo = [](auto x, int y) { 402 | std::cout << x << ", " << y << '\n'; 403 | }; 404 | 405 | foo(10, 1); 406 | foo(10.1234, 2); 407 | foo("hello world", 3); 408 | ``` 409 | 410 | 注意 Lambda 的参数 `auto x`,它等同于在闭包类型中使用一个模板声明: 411 | 412 | ```cpp 413 | struct { 414 | template < typename T > 415 | void operator ()(T x, int y) const { 416 | std::cout << x << ", " << y << '\n'; 417 | } 418 | } someInstance 419 | ``` 420 | 421 | 当然,当有多个 `auto` 参数时,代码将被扩展为多个模板参数: 422 | 423 | ```cpp 424 | const auto fooDouble =[](auto x, auto y) { /*...*/}; 425 | ``` 426 | 427 | 扩展为: 428 | 429 | ```cpp 430 | struct{ 431 | template< typename T, typename U> 432 | void operator()(T x, U y) const{ /*...*/} 433 | } someOtherInstance; 434 | ``` 435 | 436 | ### 可变泛型参数 437 | 438 | 但是这并不是全部,如果你需要更更多的函数参数类型,你可以自己进行可变性改造。 439 | 440 | 看这个栗子: 441 | 442 | > 代码 3-10 [用于求和的可变泛型 Lambda](https://wandbox.org/permlink/EVw677hLJwKpSpPg) 443 | 444 | ```cpp 445 | #include 446 | 447 | template 448 | auto sum(T x) { 449 | return x; 450 | } 451 | 452 | template 453 | auto sum(T1 s, T... ts) { 454 | return s + sum(ts...); 455 | } 456 | 457 | int main() { 458 | const auto sumLambda = [](auto... args) { 459 | std::cout << "sum of: " << sizeof...(args) << " numbers\n"; 460 | return sum(args...); 461 | }; 462 | std::cout << sumLambda(1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8, 9.9); 463 | } 464 | ``` 465 | 466 | 这段泛型 Lambda 代码中使用了 `auto ...` 来代表一个可变长参数包。理论上,它将在调用操作符中被展开为: 467 | 468 | ```cpp 469 | struct __anoymousLambda{ 470 | template < typename ... T > 471 | void operator()(T... args) const {/*...*/} 472 | }; 473 | ``` 474 | 475 | 在 C++17 中,我们有了新的选择 [折叠表达式](),它可以改进泛型可变参数 Lambdas,而在 C++20 中,我们将获得对模板参数的更多控制。 476 | 477 | 有关更多信息,请参阅 C++17 对可变参数泛型 Lambdas 的更新以及 C++20 中关于 [模板 Lambda]() 的信息 478 | 479 | ### 使用泛型 Lambda 进行完美转发 480 | 481 | 使用泛型 Lambda 表达式,其实并不限定在只使用 `auto x`,您可以像其他 `auto` 变量一样添加任何限定符,如 `auto&`、`const auto&` 或 `auto&&`。 482 | 483 | 有一个十分便利的点是,你可以指定 `auto&& x` 使其成为转发(泛型)引用。这使您可以完美地转发输入参数: 484 | 485 | > 代码 3-11 [泛型 Lambda 进行完美转发](https://wandbox.org/permlink/kA2GNHFiLOGDu9d9) 486 | 487 | ```cpp 488 | #include 489 | #include 490 | 491 | void foo(const std::string&) { 492 | std::cout << "foo(const string&)\n"; 493 | } 494 | 495 | void foo(std::string&&) { 496 | std::cout << "foo(string&&)\n"; 497 | } 498 | 499 | int main() { 500 | const auto callFoo = [](auto&& str) { 501 | std::cout << "Calling foo() on: " << str << '\n'; 502 | foo(std::forward(str)); 503 | }; 504 | const std::string str = "Hello World"; 505 | callFoo(str); 506 | callFoo("Hello World Ref Ref"); 507 | } 508 | ``` 509 | 510 | 输出 511 | 512 | ```plaintext 513 | Calling foo() on: Hello World 514 | foo(const string&) 515 | Calling foo() on: Hello World Ref Ref 516 | foo(string&&) 517 | ``` 518 | 519 | 示例代码定义了两个函数重载 `foo` 用于对 `std::string` 的 `const` 引用,另一个用于对 `std::string` 的右值引用。 520 | 521 | `callFoolambda` 使用泛型参数作为泛型引用([引用资料 6](https://isocpp.org/blog/2014/09/noexcept-optimization))。 522 | 523 | 如果您想将此 Lambda 重写为常规函数模板,它可能如下所示: 524 | 525 | ```cpp 526 | template 527 | void callFooFunc(T&& str) { 528 | std::cout << "Calling foo() on: " << str << '\n'; 529 | foo(std::forward(str)); 530 | } 531 | ``` 532 | 533 | 如你所见,在泛型 Lambda 的加持下,在编写本地匿名函数时候,你现在有更多的选择了。 534 | 535 | 但是,这还不是全部。 536 | 537 | ### 减少一些隐蔽的类型纠正 538 | 539 | 泛型 Lambda 在发现类型推断有问题时,很有帮助。 540 | 541 | 来看个例子: 542 | 543 | > 代码 3-13 [对 `std::map` 的迭代器进行类型纠正](https://wandbox.org/permlink/pSbtIA2lgYa6r1bW) 544 | 545 | ```cpp 546 | #include 547 | #include 548 | #include 549 | #include 550 | 551 | int main() { 552 | const std::map numbers{{"one", 1}, {"two", 2}, {"three", 3}}; 553 | // each time entry is copied from pair! 554 | std::for_each(std::begin(numbers), std::end(numbers), [](const std::pair& entry) { 555 | std::cout << entry.first << " = " << entry.second << '\n'; 556 | }); 557 | } 558 | ``` 559 | 560 | 这段代码有问题吗?`entry` 的类型正确吗? 561 | 562 | 很明显,这里是有问题的。 563 | 564 | `std::map` 的类型应该是 `std::pair` 而不是 `const std::pair `。而在我们的代码中,会造成不必要的额外拷贝,在 `std::pair` 和 `const std::pair&`(其中 `const std::string` 对 `std::string` 的转换)之间。 565 | 566 | 修复一下代码,它本应该是这样的: 567 | 568 | ```cpp 569 | std::for_each(std::begin(numbers), std::end(numbers), 570 | [](const auto& entry) { 571 | std::cout << entry.first << " = " << entry.second << '\n'; 572 | }); 573 | ``` 574 | 575 | 现在模板参数推导将充分获得 `entry` 对象的正确类型,并且不会创建额外的副本。 576 | 577 | 而且代码也更加简洁且易读。 578 | 579 | 接下来我们看看另一段比较长的代码,打印了 `entry` 的内存地址: 580 | 581 | > 代码 3-14 [对 `std::map` 的迭代器进行类型纠正,完整版](https://wandbox.org/permlink/yvow5G122f7A2SxN) 582 | 583 | ```cpp 584 | #include 585 | #include 586 | #include 587 | #include 588 | 589 | int main() { 590 | const std::map numbers{{"one", 1}, {"two", 2}, {"three", 3}}; 591 | // print addresses: 592 | for (auto mit = numbers.cbegin(); mit != numbers.cend(); ++mit) 593 | std::cout << &mit->first << ", " << &mit->second << '\n'; 594 | 595 | // each time entry is copied from pair! 596 | std::for_each(std::begin(numbers), std::end(numbers), [](const std::pair& entry) { 597 | std::cout << &entry.first << ", " << &entry.second << ": " << entry.first << " = " << entry.second << '\n'; 598 | }); 599 | 600 | // this time entries are not copied, they have the same addresses 601 | std::for_each(std::begin(numbers), std::end(numbers), [](const auto& entry) { 602 | std::cout << &entry.first << ", " << &entry.second << ": " << entry.first << " = " << entry.second << '\n'; 603 | }); 604 | } 605 | ``` 606 | 607 | 可能的输出结果: 608 | 609 | ```plaintext 610 | 1 0x165dc40, 0x165dc60 611 | 2 0x165dce0, 0x165dd00 612 | 3 0x165dc90, 0x165dcb0 613 | 4 0x7ffe5ed29a20, 0x7ffe5ed29a40: one = 1 614 | 5 0x7ffe5ed29a20, 0x7ffe5ed29a40: three = 3 615 | 6 0x7ffe5ed29a20, 0x7ffe5ed29a40: two = 2 616 | 7 0x165dc40, 0x165dc60: one = 1 617 | 8 0x165dce0, 0x165dd00: three = 3 618 | 9 0x165dc90, 0x165dcb0: two = 2 619 | ``` 620 | 621 | 前三行输出了 `map` 的 key 和 value 的内存地址。第 4、5、6 行分别展示了在循环迭代中临时拷贝出来的内存值。最后三行则是使用 `const auto&` 的版本,很明显可以看出来,和前三行使用自身迭代的内容是一样的。 622 | 623 | 在所举的例子中,我们关注拷贝产生的 key 的额外副本,但重要的是要了解 `entry` 也被复制了。 624 | 625 | 当使用像 `int` 这样的“廉价”的复制类型时,这也许不是什么问题,但如果对象像字符串一样更大,那么就会产生很大的拷贝开销和性能损耗。 626 | 627 | > 在 C++20 中,开发者可以更好地控制 Lambda 的模板参数,因为 C++20 的新修订引入了模板 Lambda、概念和受约束的 `auto` 参数。 628 | 629 | ## 5. 使用 Lambda 代替 `std::bind1st` 和 `std::bind2nd` 630 | 631 | 在 C++98/03 章节,我提到并展示了一些辅助函数,像 `std::bind1st` 和 `std::bind2nd` 之类。然而,这些函数在 C++11 中逐渐废弃,在 C++17 中,这些函数已被完全移除。 632 | 633 | 像 `bind1st()`/`bind2nd()`/`mem_fun()` 等函数,都是在 C++98 时期被引入进标注库的,而现在这些函数已不再需要了,因为我们可以使用 Lambda 或者更现代化的 C++ 技巧来代替。 634 | 635 | 当然了,这些函数也没有获得对于完美转发、泛型模板、`decltype` 以及其他 C++11 特性的更新,所以,我建议不要在现代编程中使用这些已弃用的内容。 636 | 637 | 下面是已被废弃的函数列表: 638 | 639 | * unary\_function()/pointer\_to\_unary\_function() 640 | * binary\_function()/pointer\_to\_binary\_function() 641 | * bind1st()/binder1st 642 | * bind2nd()/binder2nd 643 | * ptr\_fun() 644 | * mem\_fun() 645 | * mem\_fun\_ref() 646 | 647 | 当然,仅仅是为了替换 `bind1st` 或者 `bind2nd` 的话,你可以使用 `std::bind`( C++11 引入)或者 `std::bind_front`( C++20 引入)。 648 | 649 | 考虑下,这些我们之前使用旧函数所编写的这些代码要如何修改: 650 | 651 | ```cpp 652 | const auto onePlus =std::bind1st(std::plus(), 1); 653 | const auto minusOne =std::bind2nd(std::minus(), 1); 654 | std::cout << onePlus(10) << ", " << minusOne(10) << '\n'; 655 | ``` 656 | 657 | 这个例子中,`onePlus` 是由 `std::plus` 组成的一个可调用对象,并且第一参数被调用修正。 658 | 659 | 换种说法,当你写下 `onePlus(n)` 的时候,它会被展开为 `std::plus(1, n)`。 660 | 661 | 类似地,`minusOne` 是由 `std::minus` 组成的一个可调用对象,并且第二参数被调用修正。 662 | 663 | `miniusOne(n)` 会被展开为 `std::minus(n, 1)`。 664 | 665 | 上面的语法可能会十分的麻烦,我们下面来看看如何用现代化 C++ 技术来优化他们。 666 | 667 | ### 使用现代 C++ 技术 668 | 669 | 我们首先用 `std::bind()` 来替换 `bind1st` 和 `bind2nd` 670 | 671 | > 代码 3-15 [用 `std::bind` 来代替](https://godbolt.org/z/bj9Txh) 672 | 673 | ```cpp 674 | #include 675 | #include 676 | #include 677 | 678 | int main() { 679 | using std::placeholders::_1; 680 | const auto onePlus = std::bind(std::plus(), _1, 1); 681 | const auto minusOne = std::bind(std::minus(), 1, _1); 682 | std::cout << onePlus(10) << ", " << minusOne(10) << '\n'; 683 | } 684 | ``` 685 | 686 | `std::bind` 会更加灵活,它支持多个参数,甚至你可以对参数重新排序。 687 | 688 | 在参数管理上,你需要使用 ***占位符 placeholders***。 689 | 690 | 上面的例子中,使用了 `_1` 来代表第一个参数需要被传入最终的函数对象中的未知。 691 | 692 | 虽然 `std::bind` 比起 C++98/03 中的辅助函数好用多了,但是它仍然不如 Lambda 使用起来自然和便捷。 693 | 694 | 我们来尝试写一下上面例子中对应的 Lambda 表达式: 695 | 696 | ```cpp 697 | auto lamOnePlus1 =[](int b) { return 1 + b; }; 698 | auto lamMinusOne1 =[](int b) { return b - 1; }; 699 | std::cout << lamOnePlus1(10) << ", " << lamMinusOne1(10) << '\n'; 700 | ``` 701 | 702 | 当然,在 C++14 中我们也可以用初始化器来进一步优化 Lambda,让 Lambda 更加灵活: 703 | 704 | ```cpp 705 | auto lamOnePlus1 =[a = 1](int b) { return a + b; }; 706 | auto lamMinusOne1 =[a = 1](int b) { return b - a; }; 707 | std::cout << lamOnePlus1(10) << ", " << lamMinusOne1(10) << '\n'; 708 | ``` 709 | 710 | 很显然,Lambda 版本更简洁,更易读。这一点将在后面更复杂的示例中更加凸显出来。 711 | 712 | ### 函数组合 713 | 714 | 最后一个例子,我们来看看这个,在表达式中嵌套使用函数组合: 715 | 716 | > 代码 3-16 [`std::bind` 中使用函数组合](https://godbolt.org/z/8N7ZBX) 717 | 718 | ```cpp 719 | #include 720 | #include 721 | #include 722 | 723 | int main() { 724 | using std::placeholders::_1; 725 | const std::vector v{1, 2, 3, 4, 5, 6, 7, 8, 9}; 726 | const auto val = std::count_if(v.begin(), v.end(), 727 | std::bind(std::logical_and(), 728 | std::bind(std::greater(), _1, 2), 729 | std::bind(std::less(), _1, 6) 730 | ) 731 | ); 732 | return val; 733 | } 734 | ``` 735 | 736 | 你能快速解读出来这段代码的工作逻辑嘛? 737 | 738 | 不论是否读懂了,这段代码都可以重新书写为更简洁和可读的版本: 739 | 740 | ```cpp 741 | std::vector v{1, 2, 3, 4, 5, 6, 7, 8, 9}; 742 | const auto more2less6 = std::count_if(v.begin(), v.end(), [](int x) { 743 | return x > 2 && x < 6; 744 | }); 745 | ``` 746 | 747 | 现在应该好懂多了? 748 | 749 | > 有一些关于 `std::bind` 和 Lambda 的第三方指导性意见:比如《Effective Modern C++》中的第 34 项条款,比如 Google Abseil Blog 中的 [Avoid std::bind](https://abseil.io/tips/108) 750 | 751 | ## 6. Lambda 提升(LIFTing with Lambda) 752 | 753 | 尽管标准库中提供的常用算法已经很方便的,但是仍然有一些情况不太好解决。 754 | 755 | 比如,向模板函数中传递有重载的函数作为可调用对象。 756 | 757 | > 代码 3-17 调用重载函数 758 | 759 | ```cpp 760 | #include 761 | #include 762 | // two overloads: 763 | void foo(int) {} 764 | void foo(float) {} 765 | int main() { 766 | const std::vector vi{1, 2, 3, 4, 5, 6, 7, 8, 9}; 767 | std::for_each(vi.begin(), vi.end(), foo); 768 | } 769 | ``` 770 | 771 | 这个例子里面 `foo` 分别有对于 `int` 和 `float` 的两个重载,并且作为可调用对象传递给了模板函数 `for_each`。遗憾的是,在 GCC9 中,编译会提示如下错误: 772 | 773 | ```plaintext 774 | error: no matching function for call to 775 | for_each(std::vector::iterator, std::vector::iterator, 776 | ) 777 | std::for_each(vi.begin(), vi.end(), foo); 778 | ^^^^^ 779 | ``` 780 | 781 | 这里出错的主要原因是,`foo` 作为一个模板参数,它需要重新被确定为一个确定的类型。但是 `foo` 本身又有两个重载,并且实际上数据可以同时被两个重载都接受,这是编译器所不能接受的。 782 | 783 | 但是,这里有个技巧就是,我们可以使用 Lambda 来代替重载的可调用对象。上面的代码即可修改为: 784 | 785 | ```cpp 786 | std::for_each(vi.begin(), vi.end(), [](auto x) { return foo(x); }); 787 | ``` 788 | 789 | 现在我们也可以用包装器(泛型 Lambda)来解决重载的问题,让调用时可以找到适当的重载对象。 790 | 791 | 当然,我们也可以使用完美转发来更加巧妙的规避掉重载的情况。 792 | 793 | ```cpp 794 | std::for_each(vi.begin(), vi.end(), [](auto &&x) { 795 | return foo(std::forward(x); 796 | }); 797 | ``` 798 | 799 | 下面是一个应用的例子: 800 | 801 | > 代码 3-18 [泛型 Lambda 和函数重载](https://wandbox.org/permlink/2t1M9lUnTT16LjnU) 802 | 803 | ```cpp 804 | #include 805 | #include 806 | #include 807 | 808 | void foo(int i) { 809 | std::cout << "int: " << i << "\n"; 810 | } 811 | 812 | void foo(float f) { 813 | std::cout << "float: " << f << "\n"; 814 | } 815 | 816 | int main() { 817 | const std::vector vi{1, 2, 3, 4, 5, 6, 7, 8, 9}; 818 | std::for_each(vi.begin(), vi.end(), [](auto&& x) { 819 | return foo(std::forward(x)); 820 | }); 821 | } 822 | ``` 823 | 824 | 但是,对于更高级或者更复杂的场景,这可能不是首选解决方案,因为我们没有严格遵守可变参数和异常规范。 825 | 826 | 如果需要一个更加泛型、或者更好的解决办法。那可能需要多写一些代码了: 827 | 828 | ```cpp 829 | #define LIFT(foo) \ 830 | [](auto&&... x) noexcept( \ 831 | noexcept(foo(std::forward(x)...))) -> decltype(foo(std::forward(x)...)) { \ 832 | return foo(std::forward(x)...); \ 833 | } 834 | ``` 835 | 836 | 看着有点懵?别急,我们来一点点解析这段代码的功能。 837 | 838 | * 返回 `foo(std::forward(x)...)` 839 | * 完美转发,这样我们才能完整传递输入参数到 `foo` 函数中,并且保留类型。 840 | * `noexcept(noexcept(foo(std::forward(x)...)))` 841 | * 使用 `noexcept` 操作符(被嵌套的那一个)检查 可调用对象 `foo` 的异常规范。 842 | * 依赖于异常的检查结果,最终会产生 `noexcept(true)` 或者 `noexcept(false)`。 843 | * `decltype(foo(std::forward(x)...))` 844 | * 推断包装 Lambda 的最终返回类型 845 | 846 | Lambda 提升(LIFT)通过宏定义的方式实现,不然每次需要使用提升的时候你都需要编写类似的代码,并将其传递给一个算法中。而使用宏定义,这是一种最简单的语法实现了。 847 | 848 | 有兴趣的话,可以看看使用 Lambda 提升后的 [最终代码](https://wandbox.org/permlink/r81jASiPPmYXTOmx)。 849 | 850 | ## 7. 递归 Lambda 851 | 852 | 如果你有一个常规函数,那么递归调用这函数十分容易的。比如,我们计算阶乘时候的递归函数应该是这样的: 853 | 854 | > 代码 3-19 [常规函数的递归调用](https://wandbox.org/permlink/BKwwFt2eW7Nd3gIV) 855 | 856 | ```cpp 857 | int factorial(int n) { 858 | return n > 1 ? n * factorial(n - 1) : 1; 859 | } 860 | 861 | int main() { 862 | return factorial(5); 863 | } 864 | ``` 865 | 866 | 我们来尝试用 Lambda 的方式进行递归: 867 | 868 | > 代码 3-20 [Lambda 递归的错误示例](https://wandbox.org/permlink/etbPCZDuFUUYfit0) 869 | 870 | ```cpp 871 | int main() { 872 | auto factorial = [](int n) { 873 | return n > 1 ? n * factorial(n - 1) : 1; 874 | }; 875 | return factorial(5); 876 | } 877 | ``` 878 | 879 | 这段代码不会编译成功,在 GCC 中会提示编译错误: 880 | 881 | ```plaintext 882 | error:use of 'factorial'before deduction of 'auto' 883 | ``` 884 | 885 | 由于我们无法在 Lambda 函数体内访问 `factorial` 本身,因为他还尚未被编译器完全识别出来。 886 | 887 | 我们深入一下,先将这段代码展开为一个简单的仿函数: 888 | 889 | ```cpp 890 | struct fact { 891 | int operator()(int n) const { 892 | return n > 1 ? n * factorial(n - 1) : 1; 893 | }; 894 | }; 895 | auto factorial = fact{}; 896 | ``` 897 | 898 | 这样就清晰很多了,因为在调用操作符 `()` 中,我们压根无法访问到仿函数类型。 899 | 900 | 如果我们要实现递归,那么这里有两个途径可以考虑下: 901 | 902 | * 使用 `std::function` 并且捕获它 903 | * 使用内部 Lambda 然后传递泛型参数 904 | 905 | ### 利用 `std::function` 906 | 907 | 将 Lambda 表达式赋值给一个 `std::function`,后续捕获该这个对象到 Lambda 函数体内,实现递归。 908 | 909 | > 代码 3-21 [使用 `std::function` 实现 Lambda 递归](https://wandbox.org/permlink/ogZxy9CvAvRBUfJL) 910 | 911 | ```cpp 912 | #include 913 | int main() { 914 | const std::function factorial = [&factorial](int n) { 915 | return n > 1 ? n * factorial(n - 1) : 1; 916 | }; 917 | return factorial(5); 918 | } 919 | ``` 920 | 921 | 这个示例里面,我们在 Lambda 函数体内调用捕获的 `std::function` 对象 `factorial`。 922 | 923 | 此时这个对象是完整定义的,所以编译器访问并调用对象就不存在问题了。 924 | 925 | 如果你想使用一个无状态的 Lambda,那么你甚至可以使用一个函数指针来代替 `std::function`,这样内存占用会更少。 926 | 927 | 但是,但是,下面这种方式会更好。 928 | 929 | ### 内部 Lambda 和泛型参数 930 | 931 | 来看看 C++14 中的用法: 932 | 933 | > 代码 3-22 [使用内部 Lambda 来实现 Lambda 递归](https://wandbox.org/permlink/B0ueQ9nbZmr8PQE3) 934 | 935 | ```cpp 936 | int main() { 937 | const auto factorial = [](int n) noexcept { 938 | const auto f_impl = [](int n, const auto &impl) noexcept -> int { 939 | return n > 1 ? n * impl(n - 1, impl) : 1; 940 | }; 941 | return f_impl(n, f_impl); 942 | }; 943 | return factorial(5); 944 | } 945 | ``` 946 | 947 | 这次我们创建了一个内部 Lambda(`f_impl`)来执行主逻辑。 948 | 949 | 同时,我们向它传递一个泛型参数 `const auto& impl`,这个参数是一个我们可以递归调用的可调用对象。 950 | 951 | 多亏了 C++14 中的泛型 Lambda,我们可以避免 `std::function` 的开销并依赖 `auto` 进行类型推导。 952 | 953 | ### 更多技巧 954 | 955 | 可以参阅下面两个链接来学习更多关于 lambda 递归的技巧: 956 | 957 | * [Recursive lambda functions in C++11](https://stackoverflow.com/questions/2067988/recursive-lambda-functions-in-c11) 958 | * [Recursive lambdas in C++(14) - Pedro Melendez](http://pedromelendez.com/blog/2015/07/16/recursive-lambdas-in-c14/) 959 | 960 | ### 使用递归 Lambda 是最好的选择吗? 961 | 962 | 在本节中,我们学到了一些有关 Lambda 表达式的技巧。 963 | 964 | 尽管如此,这些技巧实现起来的复杂性远远高于仅使用常规递归函数调用的简单解决方案。 965 | 966 | 这就是为什么在某些情况下递归 Lambda 不是最好和最直接的选择。 967 | 968 | 另一方面,复杂递归 Lambda 的优点是它的局部性和采用 `auto` 参数的能力。 969 | 970 | ## 8. 总结 971 | 972 | 在本章,C++14 为 Lambda 表达式带来了几个关键性的改进。 973 | 974 | 由于 C++14 可以在 Lambda 范围内声明新的变量,开发者可以在模板代码中更高效的使用 Lambda。 975 | 976 | 在下一章中,我们会移步 C++17,来看看更多的 Lambda 更新。 977 | -------------------------------------------------------------------------------- /Source/Chapter4/README.md: -------------------------------------------------------------------------------- 1 | # 四、Lambda in C++17 2 | 3 | C++17 为 Lambda 表达式添加了两个重要的增强特性: 4 | 5 | * `constexpr` Lambdas 6 | * `*this` 的捕获 7 | 8 | 新的 C++ 修订版更新了其类型系统,现在包含了关于 Lambda 表达式的异常规范。 9 | 10 | 你可以在 [N4659](https://timsong-cpp.github.io/cppwp/n4659/) 中的 [\[expr.prim.lambda\]](https://timsong-cpp.github.io/cppwp/n4659/expr.prim.lambda) 章节查阅标准规范中 Lambda 相关的内容。 11 | 12 | 在本章中,我们将重点关注以下内容: 13 | 14 | * 在 C++17 中如何提升 立即调用函数表达式(IIFE pattern) 15 | * 在 C++17 中如何提升 带有折叠表达式的可变泛型 Lambda(Vriadic Generic Lambdas with Fold Expressions) 16 | * 从多个 Lambda 派生 17 | * Lambda 和异步编程 18 | 19 | ## 1. Lambda 语法更新 20 | 21 | 在 C++17,有一些关于 Lamdba 表达式的改变: 22 | 23 | * 你可以在参数列表之后加上 `constexpr` 关键字 24 | * 动态异常规范在 C++11 中被弃用,在 C++17 中被移除,所以你应该使用 `noexcept` 25 | 26 | ```cpp 27 | [] () specifiers exception attr -> ret { /*code; */ } 28 | ^ ^ ^ ^ 29 | | | | | 30 | | | | optional: trailing return type 31 | | | | 32 | | | optional: mutable, constexpr, noexcept, attributes 33 | | | 34 | | parameter list (optional when no specifiers added) 35 | | 36 | lambda introducer with an optional capture list 37 | ``` 38 | 39 | 你可以在下一节中了解到更多的变更。 40 | 41 | ## 2. 类型系统中的异常规范 42 | 43 | 在我们了解关于 Lambda 的语法改进之前,我们需要引入一个 C++17 的通用语言特性。 44 | 45 | 函数的异常规范过去不属于函数类型的一部分,但是在 C++17 中被纳入其中,这意味着你可以有两种函数类型,一种有 `noexcept`,另一种没有。 46 | 47 | > 代码 4-1 [类型系统中的异常规范](https://wandbox.org/permlink/4tcaRPeMPUd8XbOd) 48 | 49 | ```cpp 50 | using TNoexceptVoidFunc = void (*)() noexcept; 51 | void SimpleNoexceptCall(TNoexceptVoidFunc f) { 52 | f(); 53 | } 54 | 55 | using TVoidFunc = void (*)(); 56 | void SimpleCall(TVoidFunc f) { 57 | f(); 58 | } 59 | 60 | void fNoexcept() noexcept {} 61 | void fRegular() {} 62 | 63 | int main() { 64 | SimpleNoexceptCall(fNoexcept); 65 | SimpleNoexceptCall([]() noexcept {}); 66 | // SimpleNoexceptCall(fRegular); // cannot convert 67 | // SimpleNoexceptCall([]() {}); // cannot convert 68 | 69 | SimpleCall(fNoexcept); // converts to regular function 70 | SimpleCall(fRegular); 71 | SimpleCall([]() noexcept {}); // converts 72 | SimpleCall([]() {}); 73 | } 74 | ``` 75 | 76 | 一个指向 `noexcept` 函数(常规函数、成员函数、Lambda 函数)的指针可以被转化成指向不带 `noexcept` 函数(与转换前对应的函数类型)的指针。 77 | 78 | 但是反过来是不行的。 79 | 80 | 其中一个原因是代码优化。 81 | 82 | 如果编译器能够确保函数不会抛出异常,那么它就有可能生成 [更快的代码](https://isocpp.org/blog/2014/09/noexcept-optimization)。 83 | 84 | 在标准库中,有很多地方会基于 `noexcept` 判断代码能够变得更高效,这也是 `std::vector` 内部进行元素移动时是否会抛出异常的判断机制。 85 | 86 | 下面是一个栗子 87 | 88 | > 代码 4-2 [使用 `type_traits` 判断可调用对象是否标记为了 `noexcept`](https://wandbox.org/permlink/4Zv5pFWVV1TmgQTl) 89 | 90 | ```cpp 91 | #include 92 | #include 93 | 94 | template 95 | void CallWith10(Callable&& fn) { 96 | if constexpr (std::is_nothrow_invocable_v) { 97 | std::cout << "Calling fn(10) with optimisation\n"; 98 | fn(10); 99 | } else { 100 | std::cout << "Calling fn(10) normally\n"; 101 | fn(10); 102 | } 103 | } 104 | 105 | int main() { 106 | int x{10}; 107 | 108 | const auto lam = [&x](int y) noexcept { 109 | x += y; 110 | }; 111 | 112 | CallWith10(lam); 113 | 114 | const auto lamEx = [&x](int y) { 115 | std::cout << "lamEx with x = " << x << '\n'; 116 | x += y; 117 | }; 118 | 119 | CallWith10(lamEx); 120 | } 121 | ``` 122 | 123 | 输出如下: 124 | 125 | ```plaintext 126 | Calling fn(10) with optimisation 127 | Calling fn(10) normally 128 | lamEx with x = 20 129 | ``` 130 | 131 | 上述代码使用 `std::is_nothrow_invocable_v` 去检查传入的可调用对象是否具有 `noexcept` 标记。 132 | 133 | 动态异常规范在 C++11 中 **被弃用**,在 C++17 中 **被删除**,只能使用 `noexcept` 关键字去声明一个不会抛出异常的函数。 134 | 135 | Question:如果在一个具有 `noexcept` 声明的函数中抛出异常,会发生什么? 136 | 137 | Answer:编译期会调用 `std::terminate`。 138 | 139 | ## 3.`constexpr` Lambda 表达式 140 | 141 | 从 C++11 开始,`constexpr` 关键字能够在编译期评估越来越多的代码。这不仅会影响到程序的性能,也让编译期的编码变得更加愉快和有力。 142 | 143 | 在 C++17,`constexpr` 能够被用于 Lambda 表达式,可以看一下规范 [\[expr.prim.lambda\]](https://timsong-cpp.github.io/cppwp/n4659/expr.prim.lambda#closure-4) 中的 #4: 144 | 145 | > 如果函数是声明中带有 `constexpr` 或者 Lambda 表达式的参数声明子句后跟 `constexpr`,那么这是一个 `constexpr` 函数。 146 | 147 | 换句话说,如果 Lambda 表达式遵循 `constexpr` 函数的规则,那么 Lambda 表达式对应的 `operator()` 函数被隐式定义为 `constexpr,在` C++17 `中,constexpr` 函数根据 [\[dcl.constexpr\]](https://timsong-cpp.github.io/cppwp/n4659/dcl.constexpr#3)#3 需要满足以下规则: 148 | 149 | > * 不是一个虚函数 150 | > * 返回类型是 literal type(可以在编译期计算的变量) 151 | > * 所有参数都是 literal type 152 | > * 其函数体应为 = delete, = default 或者是一个不包含以下语句的复合语句: 153 | > * an asm-definition 154 | > * a goto statement 155 | > * an identifier label 156 | > * try block 157 | > * a definition of a variable of non-literal type or of static or thread storage duration or for which no initialisation is performed 158 | 159 | 举个栗子: 160 | 161 | ```cpp 162 | constexpr auto Square =[](int n) { return n * n; }; // implicit constexpr 163 | static_assert(Square(2) == 4); 164 | ``` 165 | 166 | 由于 `Square` 函数体非常简单并且它没有违反 `constexpr` 所需的相关规则,所以它被隐式声明为 `constexpr` 并且我们可以使用 `static_assert` 在编译期调用它。 167 | 168 | ### 用例 169 | 170 | 有没有更实用的代码例子? 171 | 172 | 我们先实现一个常用的累加算法: 173 | 174 | > 代码 4-3 [简单的累加](https://wandbox.org/permlink/E90ctLei3jEHIk9d) 175 | 176 | ```cpp 177 | #include 178 | template 179 | constexpr T SimpleAccumulate(Range &&range, Func func, T init) { 180 | for (auto &&elem : range) { 181 | init += func(elem); 182 | } 183 | return init; 184 | } 185 | 186 | int main() { 187 | constexpr std::array arr{1, 2, 3}; 188 | constexpr auto sum = SimpleAccumulate( 189 | arr, 190 | [](auto i) { 191 | return i * i; 192 | }, 193 | 0); 194 | 195 | static_assert(sum == 14); 196 | } 197 | ``` 198 | 199 | 该代码在将 Lamdba 函数传递给 `SimpleAccumulate` 时,虽然没有显示声明 `constexpr`,但是编译器会发现这个 Lamdba 函数被一个 `constexpr` 函数调用了,并且该 Lamdba 函数体只包含简单的语句,符合成为 `constexpr` Lamdba 的条件,所以不会报错。 200 | 201 | 并且这一过程同样适用于 `SimpleAccumulate` 中调用到的 `std::array`,`std::begin`,`std::end`。 202 | 203 | 所以 `SimpleAccumulate` 函数可能会运行在编译期。 204 | 205 | 另外一个例子是使用了递归的 Lamdba: 206 | 207 | > 代码 4-4 [递归的 `constexpr` Lamdba](https://wandbox.org/permlink/YGIJXUw3ERxnta6s) 208 | 209 | ```cpp 210 | int main() { 211 | constexpr auto factorial = [](int n) { 212 | constexpr auto fact_impl = [](int n, const auto &impl) -> int { 213 | return n > 1 ? n * impl(n - 1, impl) : 1; 214 | }; 215 | return fact_impl(n, fact_impl); 216 | }; 217 | 218 | static_assert(factorial(5) == 120); 219 | } 220 | ``` 221 | 222 | 在这个例子中,我们将 `factorial` 声明为 `constexpr`,这将会允许使用编译期进行检查的 `static_assert`。 223 | 224 | ### 捕获变量 225 | 226 | 你可以捕获变量(需要保证捕获后仍然是个常量表达式): 227 | 228 | > 代码 4-5 [捕获常量](https://wandbox.org/permlink/Uy9MoYl0OnqZgUv2) 229 | 230 | ```cpp 231 | constexpr int add(int const &t, int const &u) { 232 | return t + u; 233 | } 234 | 235 | int main() { 236 | constexpr int x = 0; 237 | constexpr auto lam = [x](int n) { 238 | return add(x, n); 239 | }; 240 | 241 | static_assert(lam(10) == 10); 242 | } 243 | ``` 244 | 245 | 然而,有趣的事情是,代码如果这么写的话: 246 | 247 | ```cpp 248 | constexpr int x = 0; 249 | constexpr auto lam = [x](int n) { return n + x }; 250 | ``` 251 | 252 | 你并不需要去捕获 `x`。 253 | 254 | 在 Clang 中,我们甚至会得到如下的 warning: 255 | 256 | ```plaintext 257 | warning: lambda capture 'x' is not required to be captured for this use 258 | ``` 259 | 260 | 同样的,如果我们将 `add` 函数改为值传递的话,也会产生同样的效果: 261 | 262 | ```cpp 263 | constexpr int add(int t, int u) { 264 | return t + u; 265 | } 266 | ``` 267 | 268 | 这是因为如果我们依赖常量表达式,编译器可以优化变量,特别是对于在编译期就可以知道值的内置类型。 269 | 270 | 下面是一些来自 [CppReference](https://en.cppreference.com/w/cpp/language/lambda) 的描述: 271 | 272 | > 一个 Lamdba 表达式如果想要不经过捕获读取一个变量的值,当且仅当该变量: 273 | > 274 | > * 是一个 const non-volatile integral 或者 enumeration type 并且被 constant expression 初始化 275 | > * 是 `constexpr` 并且没有 `mutable` 的变量 276 | 277 | 如果想要获得更多关于此的信息,你可以阅读这部分的标准 [\[basic.def.odr #4\]](https://eel.is/c++draft/basic.def.odr#4)。 278 | 279 | 在第一个 `add()` 的例子中,接收变量的时候使用了引用传递,我们强制编译器创建一个闭包成员,然后将其绑定到引用。 280 | 281 | 然后让 `add()` 函数返回参数的地址,然后它们进行比较,像是这样: 282 | 283 | ```cpp 284 | int const *address(int const &x) { 285 | return &x; 286 | } 287 | 288 | auto f = [x] { 289 | auto *p = address(x); 290 | return p == &x; // these need to be true 291 | }; 292 | ``` 293 | 294 | 因此编译器需要在闭包中存储 `x` 的拷贝,也就是说需要捕获它,这个捕获操作并不能被优化掉。 295 | 296 | ### `constexpr` 总结 297 | 298 | 简而言之: 299 | 300 | `constexpr` 允许你进行模板编程并且可能使用更短的代码。 301 | 302 | > 为将来做准备: 303 | > 在 C++20 中,我们将会拥有许多 `constexpr` 标准的算法和容器,比如 `std::vector` 和 `std::string`,所以 `constexpr` Lamdba 在这种情况下会非常便利。 304 | > 届时,运行时的代码和编译期运行的代码将会非常相似。 305 | 306 | 现在让我们现在了解自 C++17 引入的第二个重要的特性。 307 | 308 | ## 4. 捕获 `*this` 309 | 310 | 还记得我们之前是如何 [捕获类的成员变量](../Chapter2/README.md#捕获类成员和-this-指针) 的吗? 311 | 312 | 默认情况下,我们捕获 `this`(作为一个指针),并且当临时创建的对象的生命周期短于 Lamdba 函数的生命周期时,将会出现错误。 313 | 314 | 在 C++17 当中,我们有另外一种方式,我们可以捕获 `this` 的拷贝 `*this`; 315 | 316 | > 代码 4-6 [捕获 `*this`](https://wandbox.org/permlink/SPWD9gMPEiWRlZgp) 317 | 318 | ```cpp 319 | #include 320 | 321 | struct Baz { 322 | auto foo() { 323 | return [*this] { 324 | std::cout << s << std::endl; 325 | }; 326 | } 327 | std::string s; 328 | }; 329 | 330 | int main() { 331 | const auto f1 = Baz{"xyz"}.foo(); 332 | const auto f2 = Baz{"abc"}.foo(); 333 | f1(); 334 | f2(); 335 | } 336 | ``` 337 | 338 | 在这个例子中,我们可以通过 `[*this]` 来捕获一个对象的临时拷贝,该拷贝存在于闭包内并且不会在之后调用该 Lamdba 时产生 UB。 339 | 340 | 需要注意的是: 341 | 342 | * 在 C++17 中,如果你在类的成员函数当中使用 `[=]`,那么 `this` 将会被隐式捕获! 343 | * 你可以查看 C++20 相关的章节知晓这将会被增强和弃用! 344 | * 可以查看 [P0806](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0806r2.html) 获取更多资料。 345 | 346 | ### 一些指导性意见 347 | 348 | 所以我们应该捕获 `[this]` 还是 `[*this]` 呢?以及,这为什么那么重要? 349 | 350 | 在大多数情况下,当你在类的范围内里面使用 Lamdba 时,使用 `[this]` 或者 `[&]` 是很好的方式,当你的对象很大的时候不会产生额外的拷贝从而影响性能。 351 | 352 | 当你的 Lamdba 表达式的生命周期可能比对象的生命周期长的时候,你应当使用 `[*this]`。 353 | 354 | 这对于异步或者并行执行中的数据竞争可能至关重要。 355 | 356 | 此外,在异步/多线程执行模式下,Lamdba 表达式的生命周期可能比对象的生命周期更长,因此捕获的 `this` 指针可能会失效。 357 | 358 | ## 5. IIFE 更新 359 | 360 | 在 C++11,引入了 [IIFE - 立即调用函数表达式](../Chapter2/README.md#7-IIFE---立即调用函数表达式),在 C++17,有一些关于 IIFE 的更新。 361 | 362 | 在使用 IIFE 过程中会遇到的一个问题时,IIFE 式的代码不易阅读。因为调用操作符 `()` 很容易被人忽略,下面是一个 IIFE 的例子: 363 | 364 | ```cpp 365 | const auto var = [&] { 366 | if (TheFirstCondition()) 367 | return one_value; 368 | if (TheSecondCindition()) 369 | return second_val; 370 | return default_value; 371 | }(); // call it! 372 | ``` 373 | 374 | 在 C++11 章节,我们甚至讨论了使用 `const auto var` 也会有一些误导。 375 | 376 | 这是因为开发人员可能已经习惯了 `var` 可能是一个闭包对象而不是函数调用结果。 377 | 378 | 在 C++17 有一个更方便的模板函数 `std::invoke()` 可以使 IIFE 更加清晰。 379 | 380 | ```cpp 381 | const auto var = std::invoke([&] { 382 | if (TheFirstCondition()) 383 | return one_value; 384 | if (TheSecondCindition()) 385 | return second_val; 386 | return default_value; 387 | }); 388 | ``` 389 | 390 | 如你所见,不再需要在末尾写上 `()`,而是更清晰的进行调用。 391 | 392 | Note:`std::invoke()` 位于 `` 头文件中。 393 | 394 | ## 6. 可变泛型 Lambda 的更新 395 | 396 | 在 C++14 章节,我们了解到在泛型 Lamdba 中可以使用 [泛型参数列表](../Chapter3/README.md#可变泛型参数)。 397 | 398 | 感谢 C++17 带来的折叠表达式能够让我们写出更加紧凑的代码。 399 | 400 | > 代码 4-7 [使用折叠表达式实现的求和函数](https://wandbox.org/permlink/2y6y2ZAgSftBjG0S) 401 | 402 | ```cpp 403 | #include 404 | int main() { 405 | const auto sumLambda = [](auto... args) { 406 | std::cout << "sum of: " << sizeof...(args) << " numbers\n"; 407 | return (args + ... + 0); 408 | }; 409 | std::cout << sumLambda(1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8, 9.9); 410 | } 411 | ``` 412 | 413 | 如果你将上述代码与之前才 C++14 章节中求和函数的例子进行对比,你会发现这个例子中不再需要递归。 414 | 415 | 当我们需要书写包含可变参数的表达式时,使用折叠表达式会相对简单和直观。 416 | 417 | 下面是另外一个例子,它能够输出多个参数。 418 | 419 | > 代码 4-8 [使用折叠表达式实现简单的多参数输出](https://wandbox.org/permlink/ABqmzBYbWhLIVTr6) 420 | 421 | ```cpp 422 | #include 423 | int main() { 424 | const auto printer = [](auto... args) { 425 | (std::cout << ... << args) << '\n'; 426 | }; 427 | printer(1, 2, 3, "hello", 10.5f); 428 | } 429 | ``` 430 | 431 | 运行该代码后,会输出所有的参数并且不包含分隔符: 432 | 433 | ```plaintext 434 | 123hello10.5 435 | ``` 436 | 437 | 为了解决这个问题,我们将介绍一个小技巧,一并折叠逗号分隔符: 438 | 439 | > 代码 4-9 [使用折叠表达式实现带分隔符的多参数输出](https://wandbox.org/permlink/BPcBo49NQoj2RBBF) 440 | 441 | ```cpp 442 | #include 443 | int main() { 444 | const auto printer = [](auto... args) { 445 | const auto printElem = [](auto elem) { 446 | std::cout << elem << ", "; 447 | }; 448 | (printElem(args), ...); 449 | std::cout << '\n'; 450 | }; 451 | printer(1, 2, 3, "hello", 10.5f); 452 | } 453 | ``` 454 | 455 | 我们将得到如下输出: 456 | 457 | ```plaintext 458 | 1, 2, 3, hello, 10.5, 459 | ``` 460 | 461 | 代码其实可以更短: 462 | 463 | ```cpp 464 | const auto printer = [](auto... args) { 465 | ((std::cout << args << ", "), ...); 466 | std::cout << '\n'; 467 | }; 468 | ``` 469 | 470 | 如果我们不想输出末尾逗号的话,我们可以将代码改成这样: 471 | 472 | > 代码 4-10 [没有尾逗号的带分隔符的多参数输出](https://wandbox.org/permlink/qvuIL1vXdHff6QAL) 473 | 474 | ```cpp 475 | #include 476 | int main() { 477 | const auto printer = [](auto first, auto... args) { 478 | std::cout << first; 479 | ((std::cout << ", " << args), ...); 480 | std::cout << '\n'; 481 | }; 482 | printer(1, 2, 3, "hello", 10.5f); 483 | } 484 | ``` 485 | 486 | 这一次我们需要使用通用模板参数来输出第一个元素。 487 | 488 | 然后为其余元素使用可变参数列表,并且在输出元素前输出一个逗号分隔符。 489 | 490 | 代码输出如下: 491 | 492 | ```plaintext 493 | 1, 2, 3, hello, 10.5 494 | ``` 495 | 496 | ## 7. 从多个 Lambda 派生 497 | 498 | 在 C++11 章节,我们了解了从 Lamdba 表达式进行派生,虽然这很有趣,但是使用场景很有限。 499 | 500 | 主要的问题是在 C++11 中只支持特定数量的 Lambda,那么例子使用了一个或两个基类,但是如何能够使用可变数量的基类,即可变数量的 Lamdba 表达式。 501 | 502 | 在 C++17 我们有了相对简单的模式去实现: 503 | 504 | ```cpp 505 | template 506 | struct overloaded : Ts... { 507 | using Ts::operator()...; 508 | }; 509 | template 510 | overloaded(Ts...) -> overloaded; 511 | ``` 512 | 513 | 如你所见,我们需要使用可变参数模板,因为它允许我们使用任意数量的基类。 514 | 515 | 下面是一个例子: 516 | 517 | > 代码 4-11 [重载模式](https://wandbox.org/permlink/NnSVvxmSakroK7OH) 518 | 519 | ```cpp 520 | #include 521 | template 522 | struct overloaded : Ts... { 523 | using Ts::operator()...; 524 | }; 525 | 526 | template 527 | overloaded(Ts...) -> overloaded; 528 | 529 | int main() { 530 | const auto test = overloaded{[](const int &i) { 531 | std::cout << "int: " << i << '\n'; 532 | }, 533 | [](const float &f) { 534 | std::cout << "float: " << f << '\n'; 535 | }, 536 | [](const std::string &s) { 537 | std::cout << "string: " << s << '\n'; 538 | }}; 539 | test("10.0f"); 540 | } 541 | ``` 542 | 543 | 在上述的例子中,我们创建了一个由三个 Lambda 组成的 Lamdba 表达式。 544 | 545 | 之后我们可以带上参数调用该 Lamdba 表达式,将会通过传入的参数类型调用所需的函数。 546 | 547 | 现在让我们仔细看看这个模式核心的两行代码。 548 | 549 | 这两行代码受益于自 C++17 以来可用的三个特性: 550 | 551 | * using 声明的包扩展 - 用更简单且紧凑的代码实现可变模板。 552 | * 自定义模板参数推导规则 - 允许将 Lamdba 列表转换为重载类的基类列表。(在 C++20 中不需要这么做)。 553 | * 聚合初始化的扩展 - 在 C++17 之前,不能合并从其它类型派生的初始化类型。 554 | 555 | 在 C++11 章节中,我们已经使用了 using declaration。 556 | 557 | 这个特性对于使用同一个作用域内的仿函数重载带来很大帮助。 558 | 559 | 在 C++17 我们获得了支持可变参数模板的语法,这在先前的版本中是没有的。 560 | 561 | 现在让我们试着去理解剩下的两个特性: 562 | 563 | ### 自定义模板参数推导规则 564 | 565 | 我们从 Lambda 派生,并且将它们的 `operator()` 暴露出来,上一节看到的那样。 566 | 567 | 那么我们如何创建这种重载类型的变量呢? 568 | 569 | 像你知道的那样,我们无法预先知道某一个 Lambda 的类型,因为编译器会为每一个 Lambda 生成一个唯一的类型名称。例如,我们不能写下如下的代码: 570 | 571 | ```cpp 572 | overloadmyOverload { ... } // ??? 573 | // what is LambdaType1 and LambdaType2 ?? 574 | ``` 575 | 576 | 唯一的方式是使用一些 `make` 函数(因为模板参数推导适用于函数模板),像下面这样: 577 | 578 | ```cpp 579 | template 580 | constexpr auto make_overloader(T&&... t) { 581 | return overloaded{std::forward(t)...}; 582 | } 583 | ``` 584 | 585 | 如果使用 C++17 中引入的模板参数推导规则,我们可以简化常见模板类型的创建,而不需要像上面那样需要使用一个类似于 `make_overloader` 的函数。 586 | 587 | 举个例子,对于一个简单的类型,我们可以写下如下代码: 588 | 589 | ```cpp 590 | std::pair strDouble { std::string{"Hello"}, 10.0}; 591 | // strDouble is std::pair 592 | ``` 593 | 594 | 有一个 `option` 能够自定义推导规则,并且在标准库中大量的使用了它们,比如 `std::array`: 595 | 596 | ```cpp 597 | template 598 | array(T, U...) -> array; 599 | ``` 600 | 601 | 上述的写法允许我们写下如下的代码: 602 | 603 | ```cpp 604 | array test{1, 2, 3, 4, 5}; 605 | // test is std::array 606 | ``` 607 | 608 | 对于重载模式,我们可以使用如下的自定义推导规则: 609 | 610 | ```cpp 611 | templateoverloaded(Ts...) ->overloaded; 612 | ``` 613 | 614 | 现在,我们可以使用两个 Lamdba 初始化一个 Lamdba 表达式: 615 | 616 | ```cpp 617 | overloaded myOverload { [](int) { }, [](double) { } }; 618 | ``` 619 | 620 | 上述的 Lamdba 表达式中的模板参数将被正确推导,因为在这个例子中,编译器知道这两个 Lamdba 表达式参数的类型,所以可以解析出继承自这两个参数的 Lamdba 表达式的类型。 621 | 622 | 你可以在 [C++20 章节]() 中看到新的标准,类模板参数推导将被提升,对于重载模式,将不再需要写自定义的推导规则。 623 | 624 | 现在让我们进入最后一个小节 - 聚合初始化 625 | 626 | ### 聚合初始化的扩展 627 | 628 | 这个功能相对简单:我们可以聚合初始化一个从其它类型派生的类型。 629 | 630 | 来自这个标准 [\[dcl.init.aggr\]](https://timsong-cpp.github.io/cppwp/n4659/dcl.init.aggr): 631 | 632 | > An aggregate is an array or a class with: 633 | > 634 | > * no user-provided, explicit, or inherited constructors 635 | > * no private or protected non-static data members 636 | > * no virtual functions, and 637 | > * no virtual, private, or protected base classes 638 | 639 | 如下这个例子(例子来自于标准草案): 640 | 641 | ```cpp 642 | struct base1 { 643 | int b1, b2 = 32; 644 | }; 645 | 646 | struct base2 { 647 | base2() { 648 | b3 = 64; 649 | } 650 | int b3; 651 | }; 652 | struct derived : base1, base2 { 653 | int d; 654 | }; 655 | 656 | derived d1{{1, 2}, {}, 4}; 657 | derived d2{{}, {}, 4}; 658 | ``` 659 | 660 | 该代码中: 661 | 662 | 对于 d1: 663 | 664 | * `d1.b1` 初始化为 1 665 | * `d1.b2` 初始化为 2 666 | * `d1.b3` 初始化为 64 667 | * `d1.d` 初始化为 4 668 | 669 | 对于 d2: 670 | 671 | * `d2.b1` 初始化为 0 672 | * `d2.b2` 初始化为 32 673 | * `d2.b3` 初始化为 64 674 | * `d2.d` 初始化为 4 675 | 676 | 在我们的例子中,聚合初始化有更显著的影响。因为对于重载类,没有聚合初始化,我们必须实现如下的构造函数: 677 | 678 | ```cpp 679 | struct overloaded : Fs... { 680 | template 681 | overloaded(Ts&&... ts) : Fs{std::forward(ts)}... {} 682 | // ... 683 | } 684 | ``` 685 | 686 | 这将会需要写很多代码,而且可能没有涵盖所有的情况,比如 `noexcept。` 687 | 688 | 通过聚合初始化,我们「直接」从基类列表中调用 Lambda 的构造函数,因此无需编写向其显示转发参数的代码。 689 | 690 | 至此为止,我们介绍了很多,那么有没有什么有用的重载模式的例子? 691 | 692 | 现在看来似乎 `std::variant` 更为方便。 693 | 694 | ### `std::variant` 和 `std::visit` 的例子 695 | 696 | 我们可以使用继承和重载模式来做一些更实用的事情。 697 | 698 | 先看一个 `std::variant` 和 `std::visit` 的例子 699 | 700 | > 代码 4-12 [使用 `variant` 和 `visit` 实现重载模式](https://wandbox.org/permlink/TWx5fV2D0bwCWnxO) 701 | 702 | ```cpp 703 | #include 704 | #include 705 | 706 | template 707 | struct overloaded : Ts... { 708 | using Ts::operator()...; 709 | }; 710 | template 711 | overloaded(Ts...) -> overloaded; 712 | 713 | int main() { 714 | const auto PrintVisitor = [](const auto& t) { 715 | std::cout << t << "\n"; 716 | }; 717 | 718 | std::variant intFloatString{"Hello"}; 719 | std::visit(PrintVisitor, intFloatString); 720 | std::visit(overloaded{[](int& i) { 721 | i *= 2; 722 | }, 723 | [](float& f) { 724 | f *= 2.0f; 725 | }, 726 | [](std::string& s) { 727 | s = s + s; 728 | }}, 729 | intFloatString); 730 | std::visit(PrintVisitor, intFloatString); 731 | } 732 | ``` 733 | 734 | 在上述的代码中: 735 | 736 | * 我们创建了一个支持 整型、浮点型和字符串的 `variant` 变量。 737 | * 然后通过三个重载函数调整了 `intFloatString` 的值。 738 | * 最后再通过 `PrintVisitor` 将其输出出来。 739 | * 由于范型 Lamdba `的支持,PrintVisitor` 函数只需要写一个,它支持所有实现了 `<<` 操作符的对象。 740 | 741 | 其中,我们有一个 `std::visit` 的调用,它创建了一个 `visitor`,重载了三种类型,三个函数都是将当前值赋值一份,只是类型不同。 742 | 743 | ## 8. 使用 Lambda 进行并发编程 744 | 745 | 如果在同一个线程中调用 Lamdba 是比较容易的情形。 746 | 747 | 但是如果你想在一个单独的线程中调用 Lamdba 的话,应该怎么做? 748 | 749 | 可能会遇到什么问题? 750 | 751 | 让我们在本节中展开说说。 752 | 753 | > 本节不是关于如何用 C++ 编写并发代码的教程,旨在展示您在异步代码中使用 lambda 可能会遇到的问题。 754 | > 有关 C++ 中的并发问题,您可以参考单独的书籍,例如 Rainer Grimm 的《[Concurrency with Modern C++](https://leanpub.com/concurrencywithmodernc)》或者 Anthony Williams 的《[C++ Concurrency in Action](https://www.amazon.com/C-Concurrency-Action-Anthony-Williams/dp/1617294691/ref=as_li_ss_tl?dchild=1\&keywords=concurrency+C+++williams\&qid=1594551073\&sr=8-1\&linkCode=sl1\&tag=bfilipek-20\&linkId=20fd1d68c301de792ad0b0e7a30c661a\&language=en_US)》。 755 | 756 | ### Lambda 和 `std::thread` 757 | 758 | 让我们先看一下自从 C++11 就开始支持的 `std::thread`。 759 | 760 | 您可能已经知道 `std::thread` 在其构造函数中接受一个可调用对象。 761 | 762 | 可调用对象可能是一个普通的函数指针、仿函数或者 Lamdba 表达式。 763 | 764 | 一个简单的例子: 765 | 766 | > 代码 4-13 [将 Lamdba 传递给 `thread`](https://wandbox.org/permlink/iJB6ldTDQqBVrXBC) 767 | 768 | ```cpp 769 | #include 770 | #include // for std::iota 771 | #include 772 | #include 773 | 774 | int main() { 775 | const auto printThreadID = [](const char* str) { 776 | std::cout << str << ": " << std::this_thread::get_id() << " thread id\n"; 777 | }; 778 | std::vector numbers(100); 779 | std::thread iotaThread( 780 | [&numbers, &printThreadID](int startArg) { 781 | std::iota(numbers.begin(), numbers.end(), startArg); 782 | printThreadID("iota in"); 783 | }, 784 | 10); 785 | iotaThread.join(); 786 | printThreadID("printing numbers in"); 787 | for (const auto& num : numbers) std::cout << num << ", "; 788 | } 789 | ``` 790 | 791 | 在上述的例子中,我们使用 Lamdba 表达式创建了一个线程。`std::thread` 类拥有非常灵活的构造函数,所以我们甚至能够在 Lamdba 中传入一个参数,在上述代码中,我们将 `10` 作为 `startArg` 传给了 Lamdba。 792 | 793 | 上述代码很简单,因为我们通过 `join` 控制了线程的执行,保证我们在输出 `numbers` 之前,`numbers` 里的数据一定会准备好。 794 | 795 | 关键的是,虽然 Lamdba 使得创建线程变得更加容易和方便,但是它仍然是异步执行的。 796 | 797 | 闭包并不会改变其异步执行的特性,闭包同样会受到所有竞争条件和阻塞的影响。 798 | 799 | 我们可以看一下下面的例子: 800 | 801 | > 代码 4-14 [通过很多线程更改共享变量](https://wandbox.org/permlink/ZXL9k3jsl3KvrLVb) 802 | 803 | ```cpp 804 | #include 805 | #include 806 | #include 807 | 808 | int main() { 809 | int counter = 0; 810 | const auto maxThreads = std::thread::hardware_concurrency(); 811 | std::vector threads; 812 | threads.reserve(maxThreads); 813 | for (size_t tCounter = 0; tCounter < maxThreads; ++tCounter) { 814 | threads.push_back(std::thread([&counter]() noexcept { 815 | for (int i = 0; i < 1000; ++i) { 816 | ++counter; 817 | --counter; 818 | ++counter; 819 | --counter; 820 | } 821 | })); 822 | } 823 | for (auto& thread : threads) { 824 | thread.join(); 825 | } 826 | std::cout << counter << std::endl; 827 | } 828 | ``` 829 | 830 | `std::thread::hardware_concurrency()` 是一个静态成员函数。它会返回支持的线程数量。 831 | 832 | 通常它是给定机器上的硬件线程数,据 Coliru 说,在 Wandbox 上通常是 `3`。 833 | 834 | 在这个例子中,我们创建了若干个线程,每个线程都对 `counter` 有一些运算。`counter` 变量被所有线程共享。 835 | 836 | 在 C++20 中,你可以使用 `std::jthread`,它能够在析构的时候进行 `join` 并且能够接收停止标记的线程。 837 | 838 | 这种新的线程对象能够允许用户对线程执行进行更多的控制。 839 | 840 | 虽然您可能希望的最终结果是 `0`,但是结果是未定义的。 841 | 842 | 当一个线程正在读该变量的时候,可能正在有另外一个变量在并发写,导致最终的结果是未定义的。 843 | 844 | 为了解决这个问题,与常规线程场景一样,我们应该使用某种同步机制。 845 | 846 | 比如上面那个例子,我们可以使用较为易用的原子变量。 847 | 848 | > 代码 4-15 [使用原子变量](https://wandbox.org/permlink/8Te9dhfQ4r9jvOXf) 849 | 850 | ```cpp 851 | #include 852 | #include 853 | #include 854 | #include 855 | 856 | int main() { 857 | std::atomic counter = 0; 858 | const auto maxThreads = std::thread::hardware_concurrency(); 859 | std::vector threads; 860 | threads.reserve(maxThreads); 861 | 862 | for (size_t tCounter = 0; tCounter < maxThreads; ++tCounter) { 863 | threads.push_back(std::thread([&counter]() noexcept { 864 | for (int i = 0; i < 1000; ++i) { 865 | counter.fetch_add(1); 866 | counter.fetch_sub(1); 867 | counter.fetch_add(1); 868 | counter.fetch_sub(1); 869 | } 870 | })); 871 | } 872 | 873 | for (auto& thread : threads) { 874 | thread.join(); 875 | } 876 | std::cout << counter.load() << std::endl; 877 | } 878 | ``` 879 | 880 | 上面的代码会按我们的预期进行执行,因为增加和减少操作现在是原子的。 881 | 882 | 这意味着当 counter 改变的时候,其它线程不能中断这个操作。 883 | 884 | 「同步」使得代码更加安全,但是需要以性能作为牺牲。 885 | 886 | 然后这也是一个需要值得出一本书来长久讨论的主题。 887 | 888 | 解决同步问题的另外一个选择是在计算的每个线程中都有一个局部变量。 889 | 890 | 然后在线程结束之前,我们可以去锁定并且更新全局变量。 891 | 892 | 值得补充的一点是,将变量定义为 `volatile` 并不能提供正确的同步机制,并且在 C++20 中 `volatile` 在许多地方被弃用。 893 | 894 | 正如我们所见,使用 Lambda 表达式创建线程非常方便。 895 | 896 | 它可以与线程声明在一起,并且可以做任何你在常规函数和仿函数中能够做的事情。 897 | 898 | 现在让我们来尝试一下在 C++ 中新引入的另外一个科技。 899 | 900 | ### Lambda 和 `std::async` 901 | 902 | 您可以使用多线程的第二种方法是通过 `std::async`。 903 | 904 | 我们在 C++11 中通常将这个功能与线程一起使用。 905 | 906 | 这是一个高级 API,允许您延迟或完全异步地调用和计算。 907 | 908 | 现在让我们将 `iota` 的例子使用 `std::async` 来实现: 909 | 910 | > 代码 4-16 [使用 std::async 异步调用代码](https://wandbox.org/permlink/474G5hs81EZfiFNR) 911 | 912 | ```cpp 913 | #include // for async and future 914 | #include 915 | #include // for std::iota 916 | #include 917 | #include 918 | 919 | int main() { 920 | const auto printThreadID = [](const char* str) { 921 | std::cout << str << ": " << std::this_thread::get_id() << " thread id\n"; 922 | }; 923 | 924 | std::vector numbers(100); 925 | 926 | std::future iotaFuture = std::async(std::launch::async, [&numbers, startArg = 10, &printThreadID]() { 927 | std::iota(numbers.begin(), numbers.end(), startArg); 928 | printThreadID("iota in"); 929 | }); 930 | 931 | iotaFuture.get(); // make sure we get the results... 932 | printThreadID("printing numbers in"); 933 | for (const auto& num : numbers) std::cout << num << ", "; 934 | } 935 | ``` 936 | 937 | 这一次,我们没有使用线程,而是依赖了 `std::future` 机制来实现。 938 | 939 | 这是一个处理同步并保证调用结果在我们通过 `.get()` 请求时可用的对象。 940 | 941 | 在这个例子中,我们通过 `std::async` 调度 Lambda 的执行,然后通过调用 `.get()` 来等待这些被调度的任务执行完毕。 942 | 943 | 然后,上面的代码实现不够优雅。 944 | 945 | 因为我们使用了 `future` 并且使用引用捕获了 `numbers`。 946 | 947 | 更好的解耦方式应该是使用 `std::future>`,然后通过 `future` 的 `.get()` 机制来传递结果。 948 | 949 | 像是下述代码写的一样: 950 | 951 | ```cpp 952 | std::future> iotaFuture = std::async(std::launch::async, [starArg = 10]() { 953 | std::vector numbers(100); 954 | std::iota(numbers.begin(), numbers.end(), startArg); 955 | std::cout << "calling from: " << std::this_thread::get_id() << " thread id\n"; 956 | return numbers; 957 | }); 958 | auto vec = iotaFuture.get(); // make sure we get the results...// ... 959 | ``` 960 | 961 | 长久以来,`std::async/std::future` 似乎获得了褒贬不一的评价。 962 | 963 | 看起来可能是实现的太粗鲁了。 964 | 965 | 它适用于相对简单的情况,在一些复杂的情况下可能没那么有效,例如: 966 | 967 | * continuation 968 | * task merging 969 | * no cancellation/joining 970 | * it’s not a regular type 971 | * and a few other issues 972 | 973 | 如果你想了解更多,那么你可以阅读以下资料: 974 | 975 | * [There is a Better Future - Felix Petriconi - code::dive 2018](https://www.youtube.com/watch?v=WZdKFlH7qxo\&ab_channel=code%3A%3Adiveconference) 976 | * [code::dive 2016 conference–Sean Parent–Better Code: Concurrency](https://www.youtube.com/watch?v=QIHy8pXbneI) 977 | * [Core C++ 2019 :: Avi Kivity :: Building efficient I/O intensive applications with Seastar](https://www.youtube.com/watch?v=p8d28t4qCTY) 978 | 979 | ### Lambda 和 C++17 的并行算法 980 | 981 | 在讨论了 C++11 的线程支持后,我们可以转向更新的标准:C++17。 982 | 983 | 这次有一个超级好用的技巧,允许您并行化标准库中的大多数算法。 984 | 985 | 您所要做的就是在算法中指定第一个参数,例如: 986 | 987 | ```cpp 988 | auto myVec = GenerateVector(); 989 | std::sort(std::execution::par, myVec.begin(), myVec.end()); 990 | ``` 991 | 992 | 值得注意的是我们指定了第一个参数 `std::execution::par`。 993 | 994 | 它将为排序算法开启并发执行的特性。 995 | 996 | 我们还有其它的特性: 997 | 998 | | 特性名 | 描述 | 999 | | --------------------------- | ---------------------------------------------------------------------------------------- | 1000 | | sequenced\_policy | 这是一种执行策略类型,用作消除并行算法重载的歧义并指示并行算法的执行不能并行化。| 1001 | | parallel\_policy | 这是一种执行策略类型,用作消除并行算法重载的歧义并指示并行算法的执行可以并行化。| 1002 | | parallel\_unsequenced\_policy | 这是一种执行策略类型,用作消除并行算法重载的歧义并指示并行算法的执行可以并行化和向量化。| 1003 | 1004 | 对于每一种特性来说,我们预先定义了全局对象,你可以将它传递给特定的算法: 1005 | 1006 | * `std::execution::par` 1007 | * `std::execution::seq` 1008 | * `std::execution::par_unseq` 1009 | 1010 | 执行特性的声明和其对应的全局对象位于 `` 头文件中。 1011 | 1012 | 在 C++20 中还有另外一种执行策略:`unsequenced_policy` 以及其对应的全局对象 `std::execution::unseq`。 1013 | 1014 | 它用于在单线程上启用向量化执行。 1015 | 1016 | 虽然我们可以轻松的启用并行排序,但是我们也很有可能写出如下糟糕的代码: 1017 | 1018 | > 代码 4-17 [向 vector 中拷贝的危险行为](https://wandbox.org/permlink/pK63LEwKymZJC6Ne) 1019 | 1020 | ```cpp 1021 | #include 1022 | #include 1023 | #include 1024 | #include 1025 | 1026 | int main() { 1027 | std::vector vec(1000); 1028 | std::iota(vec.begin(), vec.end(), 0); 1029 | std::vector output; 1030 | std::for_each(std::execution::par, vec.begin(), vec.end(), [&output](int& elem) { 1031 | if (elem % 2 == 0) { 1032 | output.push_back(elem); 1033 | } 1034 | }); 1035 | for (const auto& elem : output) std::cout << elem << ", "; 1036 | } 1037 | ``` 1038 | 1039 | 上述代码不包含任何的第三方库,但是需要支持并行算法的编译器。 1040 | 1041 | 这在 MSVC(始于 VS 2017)中是可能可以运行的,但是不适合于任何在线编译器,你可以将该代码拷贝到 Visual Studio 上运行。 1042 | 1043 | 译者注:现在可以在 Wandbox 上跑了。 1044 | 1045 | 你看到这里的问题所在了吗? 1046 | 1047 | 通过将 Lamdba 传递给 `std::for_each`,我们需要记住代码不会运行在单线程中。 1048 | 1049 | 这里可能会使用多线程,例如:线程池的解决方案。 1050 | 1051 | 这就是为什么访问共享输出变量不是一个好主意。 1052 | 1053 | 它不仅可能会以错误的顺序插入元素,而且如果多个线程同时尝试更改变量,它甚至会崩溃。 1054 | 1055 | 我们可以通过在每次调用 `push_back` 之前使用互斥锁并锁定它来解决同步问题。 1056 | 1057 | 但是上述的代码仍然高效吗? 1058 | 1059 | 如果过滤的条件简单且执行速度较快,那么上述代码的性能甚至会低于其对应的串行版本的代码。 1060 | 1061 | 如果没有实际运行过,您不知道 `output` 中元素的顺序。 1062 | 1063 | 这一节展示了基本的并行算法,如果你想了解的更多,可以阅读以下文章: 1064 | 1065 | * [The Amazing Performance of C++17 Parallel Algorithms, is it Possible?](https://www.cppstories.com/2018/11/parallel-alg-perf/) 1066 | 1067 | ### Lambda 和异步 - 总结 1068 | 1069 | 当你想启动一个线程、通过 `std::async` 或者调用并行算法的时候,使用 Lamdba 表达式会非常方便。 1070 | 1071 | 但是必须要记住的一点是,闭包对象在并发性方面并没有特殊性,所有的挑战和困难也都是基于此。 1072 | 1073 | ## 9. 总结 1074 | 1075 | 在本章节中,您已经看到了 C++17 加入了 C++ 中的两个基本元素,`constexpr` 和 Lamdba。 1076 | 1077 | 现在你可以配合 `constexpr` 使用 Lamdba 表达式了。 1078 | 1079 | 这是改进语言中元编程支持的必要步骤。 1080 | 1081 | 我们将在 C++20 的章节中看到更多关于此的内容。 1082 | 1083 | 更重要的是,C++17 标准也解决了捕获的问题,从 C++17 开始,您可以通过 `[*this]` 对 `this` 进行值捕获,从而使代码更加安全。 1084 | 1085 | 我们还查看了 Lamdba 相关的一些例子:IIFE 技术、折叠表达式和可变参数泛型 Lamdba,从多个 Lamdba 进行派生已经异步代码的执行。 1086 | 1087 | 由于在 C++17 中支持的各种功能,我们现在有更好的语法和更直接的方法来编写更高效的代码。 1088 | -------------------------------------------------------------------------------- /Source/Chapter2/README.md: -------------------------------------------------------------------------------- 1 | # 二、Lambda in C++11 2 | 3 | 这真是激动人心的时刻。C++ 委员会听取了开发者们的声音,从 C++11 开始,我们终于拥有了 Lambda 表达式。 4 | 5 | Lambda 很快就成为了现代 C++ 最广为认可和使用的特性。 6 | 7 | 你可以阅读 [N3337](https://timsong-cpp.github.io/cppwp/n3337/) 草案——C++11 的最终草案——中 [\[expr.prim.lambda\]](https://timsong-cpp.github.io/cppwp/n3337/expr.prim.lambda) 章节中的 Lambda 规范。 8 | 9 | 我认为委员会把 Lambda 加入进来是一个明智的做法,对于 C++ 这个语言本身而言。 10 | 11 | 他们引进了一种新的语法,而编译器会去将其展开为一个未命名的“隐藏”方函数对象。引入 Lambda,对于一种真正的强类型语言,有很多优点(当然也有缺点),同时这种特性也更容易去推断代码的意图。 12 | 13 | 在本章节,你可以学习到: 14 | 15 | * Lambda 的基础语法 16 | * 如何捕获变量 17 | * 如何捕获成员变量 18 | * Lambda 的返回类型 19 | * 什么是闭包对象 20 | * Lambda 如何转换为一个函数指针以及用 C 风格的 API 来调用 21 | * IIFE 是什么 22 | * 如何从 Lambda 表达式继承以及它为什么有用 23 | 24 | ## 1. Lambda 表达式的语法 25 | 26 | 下面就是 Lambda 语法的「公式」和说明: 27 | 28 | ```plaintext 29 | [] () specifiers exception attr -> ret { /*code; */ } 30 | ^ ^ ^ ^ 31 | | | | | 32 | | | | 可选: 尾部返回类型 33 | | | | 34 | | | 可选: 可变、异常说明或者 noexcept 、属性 35 | | | 36 | | 参数列表 (当不添加说明符时可选) 37 | | 38 | Lambda 引入器以及捕获列表(可选) 39 | ``` 40 | 41 | 在我们开始学习 Lambda 之前,需要从 C++ 标准中引入一些核心定义: 42 | 43 | * 闭包对象在 [\[expr.prim.lambda#2\]](https://timsong-cpp.github.io/cppwp/n3337/expr.prim.lambda#2) 中,有如下定义: 44 | 45 | > 对 lambda 表达式进行求值会产生一个 `prvalue` 类型的临时值。这个临时对象叫做 **闭包对象**。 46 | 47 | * 闭包类型在 [\[expr.prim.lambda#3\]](https://timsong-cpp.github.io/cppwp/n3337/expr.prim.lambda#3) 中,有如下定义: 48 | 49 | > lambda 表达式的类型(也是闭包对象的类型)是唯一未命名的“非联合”类型——称为 **闭包类型** 50 | 51 | ### Lambda 表达式的一些例子 52 | 53 | ```cpp 54 | // 1. 一个最简单的lambda 55 | []{}; 56 | ``` 57 | 58 | 在第一个例子中,你可以看见一个“最小巧”的 Lambda 表达式。 59 | 60 | 它仅需要 `[]` 和一个空的函数体 `{}`。参数列表 `()` 是可选的,所以在本例中不需要。 61 | 62 | ```cpp 63 | // 2. 带有两个参数的lambda 64 | [](float f, int a){ return a * f;}; 65 | [](int a, int b){ return a < b }; 66 | ``` 67 | 68 | 在第二个例子中,你可以看到参数在 `()` 部分被传入进去,就和常规函数一样。返回类型是不需要的,因为编译器会自动推断它。 69 | 70 | ```cpp 71 | // 3. 带有尾返回类型的lambda 72 | [](MyClass t) -> int { auto a = t.compute(); print(a); return a; }; 73 | ``` 74 | 75 | 在第三个例子中,我们显示地定义了返回类型。 76 | 77 | 从 C++11 开始,这个尾部返回类型其实和常规函数的声明方式是一样的。 78 | 79 | ```cpp 80 | // 4. 带有额外描述符的lambda 81 | [x](int a, int b) mutable{ ++x; return a < b; }; 82 | [](float param) noexcept{ return param * param; }; 83 | [x](int a, int b) mutable noexcept{ ++x; return a < b; }; 84 | ``` 85 | 86 | 第四个例子展示了在 lambda 表达式的函数体前,你可以添加额外的描述符。 87 | 88 | 如上代码,我们使用 `mutable`(这样我们就可以改变捕获的变量)也可以是 `noexcept`。 89 | 90 | 第三个 lambda 表达式同时使用了 `mutable` 和 `noexcept`,请注意顺序(当书写为 `noexcept mutable` 时,无法编译通过)。 91 | 92 | 虽然()部分是可选的,但是如果你想要应用 `mutable` 或者 `noexcept`,那么 `()` 则必须在表达式书写。 93 | 94 | ```cpp 95 | // 5. 可选()的lambda 96 | [x] { std::cout << x; }; // 正确,无需() 97 | [x] mutable { ++x; }; // 编译失败 98 | [x]() mutable { ++x; }; // 正确,mutable前需要() 99 | [] noexcept {}; // 编译失败 100 | []() noexcept {}; // 正确 101 | ``` 102 | 103 | 同样的模式也可以在其他描述符中被应用在 lambda 中,像 C++17 的 `constexpr` 和 C++20 中的 `consteval`。 104 | 105 | #### 属性 106 | 107 | Lambda 语法也允许使用以下形式引入的属性:`[[attr_name]]`。 108 | 109 | 然而,如果你试图在 lambda 应用一个属性,那么这个属性是被应用在调用操作符的类型上,而不是操作符本身。 110 | 111 | 这就是为什么现在(甚至在 C++20)中都没有对 lambda 真正有意义的属性存在。 112 | 113 | 大多数编译器甚至会报错。如果我们使用 C++17 的属性并尝试应用在 Lambda 表达式中: 114 | 115 | ```cpp 116 | auto myLambda = [](int a)[[nodiscard]]{ return a * a; }; 117 | ``` 118 | 119 | 使用 [Clang](https://wandbox.org/permlink/3zfzL1NNpPXXgLOx) 编译,就会产生如下的编译错误: 120 | 121 | ```plaintext 122 | error: 'nodiscard' attribute cannot be applied to types 123 | ``` 124 | 125 | ### Lambda 在编译器的展开 126 | 127 | 总结一下,这儿有一个基础的代码用例来展示下编写 Lambda 表达式并应用在 `std::for_each` 中去。 128 | 129 | 作为对比,我们也编写了一个相应功能的仿函数类型: 130 | 131 | > 代码 2-1 [Lambda 表达式和对应的仿函数](https://wandbox.org/permlink/XXR02LXYAngHF5dt) 132 | 133 | ```cpp 134 | #include 135 | #include 136 | #include 137 | 138 | int main() { 139 | struct { 140 | void operator()(int x) const { 141 | std::cout << x << '\n'; 142 | } 143 | } someInstance; 144 | 145 | const std::vector v{1, 2, 3}; 146 | std::for_each(v.cbegin(), v.cend(), someInstance); 147 | std::for_each(v.cbegin(), v.cend(), [](int x) { 148 | std::cout << x << '\n'; 149 | }); 150 | } 151 | ``` 152 | 153 | 对于这个例子,编译器会将 Lambda 表达式 154 | 155 | ```cpp 156 | [](int x) { std::cout << x << 'n'; }; 157 | ``` 158 | 159 | 转化为一个简化格式的匿名仿函数: 160 | 161 | ```cpp 162 | struct { 163 | void operator()(int x) const { 164 | std::cout << x << '\n'; 165 | } 166 | } someInstance; 167 | ``` 168 | 169 | 这种转换或者“展开”的过程,可以在 [C++ Insights](https://cppinsights.io/) 上查看,这是一个可以查看合法 C++ 代码转化为编译器源码视图的在线工具,包括 Lambda 达式的展开以及模板初始化的过程。 170 | 171 | 下一节中,我们会深入研究下 Lambda 表达式的各个部分。 172 | 173 | ## 2. Lambda 表达式的类型 174 | 175 | 由于编译器会生成给每个 Lambda(闭包类型)生成一个唯一名称,所以没有办法预先“拼写”出它的类型。 176 | 177 | 这就是为什么你需要使用 `auto` 或者 `decltype` 关键字来推断类型了。 178 | 179 | ```cpp 180 | auto myLambda = [](int a) -> double { return 2.0 * a; }; 181 | ``` 182 | 183 | 当然,下面这两 lambda 也是一样的。 184 | 185 | ```cpp 186 | auto firstLam = [](int x) { return x * 2; }; 187 | auto secondLam = [](int x) { return x * 2; }; 188 | ``` 189 | 190 | 这俩 Lambda 拥有完全一样的代码,但是他们的类型是不同的。 191 | 192 | 编译器会推断为两个 Lambda 表达式推断出各自独立的未命名类型。 193 | 194 | 我们可以用下面的代码来证明这个性质: 195 | 196 | > 代码 2-2 [同样的代码,不同的类型](https://wandbox.org/permlink/dimC66ghOFL3GF3q) 197 | 198 | ```cpp 199 | #include 200 | 201 | int main() { 202 | const auto oneLam = [](int x) noexcept { 203 | return x * 2; 204 | }; 205 | const auto twoLam = [](int x) noexcept { 206 | return x * 2; 207 | }; 208 | static_assert(!std::is_same::value, "must be different!"); 209 | } 210 | ``` 211 | 212 | 这个例子可以用来验证两个 Lambda(`oneLam` 和 `twoLam`)的闭包类型是否一致。 213 | 214 | > 在 C++17,我们可以使用不带消息的 `static_assert` 和推断类型特征的变量模板辅助函数 `is_same_v`: 215 | 216 | ```cpp 217 | static_assert(std::is_same_v); 218 | ``` 219 | 220 | 但是,尽管你不知道确切的类型名,你可以将 Lambda 的签名存储在 `std::function` 中使用。 221 | 222 | 通常来说,如果定义为 `auto` 的 Lambda 无法解决的,可以通过定义为 `std::function` 类型来解决。 223 | 224 | 举个例子,之前的 Lambda 有一个 `double(int)` 的签名(参数为 `int` 返回 `double`)。 225 | 226 | 我们可以通过以下方式创建一个 `std::function` 对象: 227 | 228 | ```cpp 229 | std::function myFunc = [](int a) -> double { return 2.0 * a; }; 230 | ``` 231 | 232 | `std::function` 是一个“笨重”的对象,因为他需要操控全部的可调用对象。 233 | 234 | 为了实现这一点,他需要一套先进的内核机制,比如 [类型双关(Type punning)](https://zh.wikipedia.org/wiki/%E7%B1%BB%E5%9E%8B%E5%8F%8C%E5%85%B3) 或者甚至动态内存分配。 235 | 236 | 来试试下面这个例子: 237 | 238 | > 代码 2-3 [`std::function` 和 `auto` 类型推断](https://wandbox.org/permlink/bTJHEc4uWAMTteyN) 239 | 240 | ```cpp 241 | #include 242 | #include 243 | 244 | int main() { 245 | const auto myLambda = [](int a) noexcept -> double { 246 | return 2.0 * a; 247 | }; 248 | 249 | const std::function myFunc = [](int a) noexcept -> double { 250 | return 2 .0 * a; 251 | }; 252 | 253 | std::cout << "sizeof(myLambda) is " << sizeof(myLambda) << '\n'; 254 | std::cout << "sizeof(myFunc) is " << sizeof(myFunc) << '\n'; 255 | 256 | return myLambda(10) == myFunc(10); 257 | } 258 | ``` 259 | 260 | 用 GCC 编译并运行,将会输出: 261 | 262 | ```plaintext 263 | sizeof(myLambda) is 1 264 | sizeof(myFunc) is 32 265 | ``` 266 | 267 | 因为 `myLambda` 仅仅是一个无状态 Lambda,所以它也是一个没有任何数据成员字段的空类,这也就是为什么它的大小只有 1 字节的原因。 268 | 269 | 而 `std::function` 版本则占用了 32 字节。 270 | 271 | 所以,一目了然,这就是为什么你应该尽可能使用 `auto` 类型推断来获取占用内存更少的闭包对象了。 272 | 273 | 当然,我们也不得不去深入讨论 `std::function` 的使用,因为它不支持只能移动(moveable-only)的闭包对象。 274 | 275 | 我们会在 C++14 章可移动类型一节 来详细介绍这部分内容。 276 | 277 | ### 构造,还是拷贝? 278 | 279 | 在 [\[expr.prim.lambda#19\]](https://timsong-cpp.github.io/cppwp/n3337/expr.prim.lambda#19) 中有一个规则: 280 | 281 | > Lambda 表达式产生的闭包对象是 **删除** 了 *默认构造函数* 和 *拷贝赋值运算符* 的。 282 | > 但是它包含隐式声明的 **拷贝构造函数** 以及 **移动构造函数**。 283 | 284 | 由于这个规则的存在,所以你无法这样编写代码: 285 | 286 | ```cpp 287 | auto foo = [&x, &y]() { ++x; ++y; }; 288 | decltype(foo) fooCopy 289 | ``` 290 | 291 | GCC 会提示如下错误: 292 | 293 | ```plaintext 294 | error: use of deleted function'main()::::()' 295 | decltype(foo) fooCopy; 296 | ^~~~~~~ 297 | note:a lambda closure type has a deleted default constructor 298 | ``` 299 | 300 | 但是,你可以拷贝 Lambda: 301 | 302 | > 代码 2-4 [拷贝 Lambda](https://wandbox.org/permlink/F50TYeCwooAWaruV) 303 | 304 | ```cpp 305 | #include 306 | 307 | int main() { 308 | const auto firstLam = [](int x) noexcept { 309 | return x * 2; 310 | }; 311 | 312 | const auto secondLam = firstLam; 313 | static_assert(std::is_same::value, "must be the same"); 314 | } 315 | ``` 316 | 317 | 如果拷贝了一个 Lambda(实际上发生的是拷贝构造),它的状态也会被拷贝过来。 318 | 319 | 这一点对于捕获对象来说很重要。 320 | 321 | 因为,一个闭包类型会存储捕获的对象作为其成员字段。 322 | 323 | 所以,当进行 Lambda 拷贝时,会拷贝那些数据成员字段。 324 | 325 | > 在 C++20 中,无状态 Lambda 会拥有默认的构造器和拷贝赋值。 326 | 327 | ## 3. 调用操作符 328 | 329 | 我们传入 Lambda 中的参数部分,会被“转译”为相应闭包类型的调用操作符的参数。 330 | 331 | 默认情况下,在 C++11 中,他会被“转译”为一个常量内联成员函数。 332 | 333 | 例如 334 | 335 | ```cpp 336 | auto lam = [](double param) { /*do something*/ }; 337 | ``` 338 | 339 | 将被编译器展开为: 340 | 341 | ```cpp 342 | struct __anonymousLambda { 343 | inline void operator()(double param) const { /*do something*/ } 344 | }; 345 | ``` 346 | 347 | ### 重载 348 | 349 | 有一件事情值得提一下,那就是当你定义了一个 lambda 时,你不能创建它的任何重载形式来传入不同的参数。 350 | 351 | ```cpp 352 | // 无法编译 353 | auto lam = [](double param) { /* do something */ }; 354 | auto lam = [](int param) { /* do something */ }; 355 | ``` 356 | 357 | 上面的代码将无法通过编译,因为编译器会将他们“转译”为一个仿函数,当然这就意味着无法重新定义一个相同的变量。 358 | 359 | 但是,你可以在一个仿函数中定义两个调用操作符的重载形式,这是允许的: 360 | 361 | ```cpp 362 | struct MyFunctor { 363 | inline void operator()(double param) { /* do something */ }; 364 | inline void operator()(int param) { /* do something */ }; 365 | }; 366 | ``` 367 | 368 | `MyFunctor` 现在就可以同时接受 `double` 和 `int` 参数了。 369 | 370 | 如果你想在 Lambda 中实现相似的效果,那么你可以看看这部分内容 [Lambda 继承]() 371 | 372 | ### 其他修饰符 373 | 374 | 我们在 Lambda 语法 一节中简略介绍过这部分主题,但是你并不会被闭包类型调用操作符的默认声明所限制到。 375 | 376 | 在 C++11 中,你可以添加 `mutalbe` 或者异常描述符。 377 | 378 | > 如果可能的话,本书会使用长例子来用 `const` 标记闭包对象并且使 Lambda 为 `noexcept`。 379 | 380 | 你可以通过在参数声明后面那部分指定 `mutable` 或者 `noexcept` 来使用这些关键字。 381 | 382 | ```cpp 383 | auto myLambda = [](int a) mutable noexcept { /* do something */ }; 384 | ``` 385 | 386 | 编译器会展开为: 387 | 388 | ```cpp 389 | struct __anonymousLambda { 390 | inline void operator()(int a) noexcept{ /* do something */ } 391 | }; 392 | ``` 393 | 394 | 请注意,`const` 关键字此时会消失,并且调用操作符可以修改 Lambda 的成员变量了。 395 | 396 | 但是,成员变量呢?我们要如何在 Lambda 中声明成员变量? 397 | 398 | 请看下一个章节——关于「捕获」变量。 399 | 400 | ## 4. 捕获 401 | 402 | **捕获子句**-`[]` 操作符绝不仅仅只是 Lambda 的引入符号,同时它还兼顾捕获变量的列表的职能。 403 | 404 | 通过从 Lambda 表达式外部捕获变量,你可以在闭包类型中创建成员变量(非静态成员),然后,在 Lambda 函数体中,你就可以使用它了。 405 | 406 | 我们可以弄一个类似于 C++98/03 章节中 `PrintFunctor` 的内容,在这个类中,我们添加成员变量 `std::string strText` 并让他在构造函数中被初始化。 407 | 408 | 拥有一个成员变量可以让我们存储可调用对象的一些状态了。 409 | 410 | 一些有关捕获器的语法: 411 | 412 | * `[&]` - 引用捕获,自动捕获声明在捕获范围内的生命周期尚未结束的变量。 413 | * `[=]` - 值捕获(创建拷贝),自动捕获声明在捕获范围内的生命周期尚未结束的变量。 414 | * `[x, &y]` - `x` 为值捕获,`y` 为显式引用捕获。 415 | * `[args...]` - 捕获一个模板参数包,全部都是值捕获 416 | * `[&args...]` - 捕获一个模板参数包,全部都是引用捕获 417 | 418 | 一些例子: 419 | 420 | ```cpp 421 | int x = 2, y = 3; 422 | const auto l1 = []() { 423 | return l1; 424 | }; // 没有捕获 425 | const auto l2 = [=]() { 426 | return x; 427 | }; // 值捕获(拷贝) 428 | const auto l3 = [&]() { 429 | return y; 430 | }; // 引用捕获 431 | const auto l4 = [x]() { 432 | return x; 433 | }; // 仅值捕获x 434 | const auto lx = [= x]() { 435 | return x; 436 | }; // 错误的语法,不需要=来对x显式进行拷贝(值捕获) 437 | const auto l5 = [&y]() { 438 | return y; 439 | }; // 仅引用捕获y 440 | const auto l6 = [x, &y]() { 441 | return x * y; 442 | }; // 值捕获x,引用捕获y 443 | const auto l7 = [=, &x]() { 444 | return x + y; 445 | }; // 全部都是值捕获,除了x是引用捕获 446 | const auto l8 = [&, y]() { 447 | return x - y; 448 | }; // 全都是引用捕获,除了y是值捕获 449 | ``` 450 | 451 | 为了理解在捕获变量的过程中发生了什么,让我们一起来思考下面这个例子: 452 | 453 | > 代码 2-5 捕获一个变量 454 | 455 | ```cpp 456 | std::string str{"Hello World"}; 457 | auto foo = [str]() { 458 | std::cout << str << '\n'; 459 | }; 460 | foo(); 461 | ``` 462 | 463 | 上面这个 Lambda,`str` 被值捕获(构造了一个拷贝)。 464 | 465 | 编译器将自动生成这样的仿函数: 466 | 467 | > 代码 2-6 编译器可能生成的仿函数,单变量 468 | 469 | ```cpp 470 | class __unnamedLambda { 471 | public: 472 | inline /*constexpr */ void operator()() const { 473 | std::operator<<(std::operator<<(std::cout, str), '\n'); 474 | } 475 | 476 | private: 477 | std::string str; 478 | 479 | public: 480 | __unnamedLambda(std::string _str) : str{_str} {} 481 | }; 482 | ``` 483 | 484 | 如上述的展开代码,一个变量被传进构造函数中,在 Lambda 声明中被称为“就地”。 485 | 486 | 更准确的定义在 [\[expr.prim.lambda#21\]](https://timsong-cpp.github.io/cppwp/n3337/expr.prim.lambda#21):当解析 Lambda 表达式时,通过值捕获的实体将直接初始化在每个对应生成的闭包对象中的非静态成员数据。 487 | 488 | 当然了,上述代码中的构造函数(`__unnamedLambda`)仅仅是用作演示和解释用途,编译器真正生成的内容会与此有所差别,并且不会暴露给用户。 489 | 490 | > 代码 2-7 引用捕获两个变量 491 | 492 | ```cpp 493 | int = 1, y = 1; 494 | std::cout << x << " " << y << std::endl; 495 | const auto foo = [&x, &y]() noexcept { 496 | ++x; 497 | ++y; 498 | }; 499 | foo(); 500 | std::cout << x << " " << y << std::endl; 501 | ``` 502 | 503 | 上述代码展开后,可能是: 504 | 505 | > 代码 2-8 [编译器可能生成的仿函数,双变量,引用](https://wandbox.org/permlink/da9ltcv53ECxnoEk) 506 | 507 | ```cpp 508 | class __unnamedLambda { 509 | public: 510 | inline /* constexpr */ void operator()() const noexcept { 511 | ++x; 512 | ++y; 513 | } 514 | 515 | private: 516 | int& x; 517 | int& y; 518 | 519 | public: 520 | __unnamedLambda(int& _x, int& _y) : x{_x}, y{_y} {} 521 | }; 522 | ``` 523 | 524 | 由于我们是通过引用的方式捕获 `x` 和 `y` 的,所以闭包类型中的成员变量也是引用类型的。 525 | 526 | > 请注意: 527 | > 值捕获变量的值是在 Lambda **定义** 时,而不是在 **使用** 时。 528 | > 但是引用捕获变量的内容是在 Lambda **使用** 时,而不是 **定义** 时。二者是有区别的。 529 | 530 | 虽然指定 `[=]` 或者 `[&]` 可能很方便,因为它会自动捕获仍在生命周期内的全部变量,但是,若能指明捕获的变量是哪些,将会更加清晰明确。 531 | 532 | 这样编译器才能警告出哪些非预期的影响(参见 全局变量 和 静态变量)。 533 | 534 | 当然,如果你想要了解更多更详细的内容,可以翻阅 Scott Meyers 所著的《Effective Modern C++》第 31 项——“避免默认捕获模式”的内容。 535 | 536 | > 请注意: 537 | > C++ 闭包不会延长被捕获引用对象的剩余生命周期。请务必确保捕获对象在 Lambda 调用时仍然“存活”。 538 | 539 | ### `mutable` 关键字 540 | 541 | 通过闭包类型默认调用操作符获取的,都是带有 `const` 关键字限定的,你无法在 Lambda 表达式内部对他们做出任何修改。 542 | 543 | 如果你希望进行修改的操作,那就需要在参数列表后添加 `mutable` 关键字。 544 | 545 | 它可以有效的去除闭包类型调用操作符中的 `const` `修饰符。举一个mutable` 的简单例子: 546 | 547 | ```cpp 548 | int x =1; 549 | auto foo =[x]() mutable { ++x; }; 550 | ``` 551 | 552 | 它会被展开为: 553 | 554 | ```cpp 555 | class __lambda_x1 { 556 | public: 557 | void operator()() { 558 | ++x; 559 | } 560 | 561 | private: 562 | int x; 563 | }; 564 | ``` 565 | 566 | 如你所见,现在调用操作符就可以修改捕获的成员变量了。 567 | 568 | > 代码 2-8 [通过值捕获两个 mutable 变量](https://wandbox.org/permlink/yQZUOIcK1ncKIyEI) 569 | 570 | ```cpp 571 | #include 572 | 573 | int main() { 574 | const auto print = [](const char* str, int x, int y) { 575 | std::cout << str << ": " << x << " " << y << '\n'; 576 | }; 577 | 578 | int x = 1, y = 1; 579 | print("in main()", x, y); 580 | 581 | auto foo = [x, y, &print]() mutable { 582 | ++x; 583 | ++y; 584 | print("in foo()", x, y); 585 | }; 586 | 587 | foo(); 588 | print("in main()", x, y); 589 | } 590 | ``` 591 | 592 | 输出: 593 | 594 | ```plaintext 595 | in main(): 1 1 596 | in foo(): 2 2 597 | in main(): 1 1 598 | ``` 599 | 600 | 在上述的例子中,我们可以修改 `x` 和 `y` 的值。 601 | 602 | 但是,由于是从封闭区域中获取的拷贝值,所以在调用 `foo` 之后,我们无法获取到在局部区域修改的新值。 603 | 604 | 另一方面,如果使用引用捕获,那么就不需要使用 `mutable` 修饰符来修改值了。 605 | 606 | 这是因为捕获的成员变量是“引用”过来的,并且不能和内部的 `const` 成员函数所绑定,所以可以对它的内容作出修改。 607 | 608 | > 代码 2-9 通过引用捕获一个变量 609 | 610 | ```cpp 611 | int x = 1; 612 | std::cout << x << '\n'; 613 | 614 | const auto foo = [&x]() noexcept { 615 | ++x; 616 | }; 617 | 618 | foo(); 619 | std::cout << x << '\n'; 620 | ``` 621 | 622 | 这个例子中,Lambda 并没有应用 `mutable` 修饰符,但是我们可以修改引用的值。 623 | 624 | 需要注意的一点:当使用 `mutable` 修饰符后,就无法使用 `const` 修饰符来修饰闭包对象了,因为它会阻止你调用这个 Lambda。 625 | 626 | ```cpp 627 | int x =10; 628 | const auto lam =[x]() mutable { ++x; } 629 | lam(); // 无法编译 630 | ``` 631 | 632 | 由于无法在 `const` 对象中调用非 `const` 成员函数,最后一行将提示编译失败。 633 | 634 | ### 调用计数器 - 捕获变量的一个例子 635 | 636 | 在我们深入探究捕获之前,先来看看一个有关 Lambda 使用的例子: 637 | 638 | 当你想使用一些现存的 STL 中的算法函数并改变默认行为规则时,用 Lambda 表达式是十分方便的。比如,对于 `std::sort` 函数,你可以写一个自定义的比较函数。 639 | 640 | 当然,我们也可以进一步强化比较函数的功能:调用计数。 641 | 642 | > 代码 2-10 [调用计数器](https://godbolt.org/z/jG5xK7) 643 | 644 | ```cpp 645 | #include 646 | #include 647 | #include 648 | int main() { 649 | std::vector vec{0, 5, 2, 9, 7, 6, 1, 3, 4, 8}; 650 | size_t compCounter = 0; 651 | 652 | std::sort(vec.begin(), vec.end(), [&compCounter](int a, int b) noexcept { 653 | ++compCounter; 654 | return a < b; 655 | }); 656 | 657 | std::cout << "number of comparisons: " << compCounter << '\n'; 658 | 659 | for (const auto& v : vec) 660 | std::cout << v << ", "; 661 | } 662 | ``` 663 | 664 | 自定义的比较器和默认比较器是一致的,返回二者较小的那一个,即自然排序(升序排列)。 665 | 666 | 同时,Lambda 也向 `std::sort` 传入了捕获的本地变量 `compCounter` 来计数调用了在排序过程中多少次的比较器。 667 | 668 | ### 捕获全局变量 669 | 670 | 如果有一个全局变量,并且在 Lambda 使用了 `[=]`,也许你会认为这样就可以值捕获全局变量了,很遗憾,事实并非如此: 671 | 672 | > 代码 2-11 [捕获全局变量](https://wandbox.org/permlink/n8wCuoeej8mGscql) 673 | 674 | ```cpp 675 | #include 676 | 677 | int global = 10; 678 | 679 | int main() { 680 | std::cout << global << std::endl; 681 | 682 | auto foo = [=]() mutable noexcept { 683 | ++global; 684 | }; 685 | foo(); 686 | std::cout << global << std::endl; 687 | 688 | const auto increaseGlobal = []() noexcept { 689 | ++global; 690 | }; 691 | increaseGlobal(); 692 | std::cout << global << std::endl; 693 | 694 | const auto moreIncreaseGlobal = [global]() noexcept { 695 | ++global; 696 | }; 697 | moreIncreaseGlobal(); 698 | std::cout << global << std::endl; 699 | } 700 | ``` 701 | 702 | 这个例子定义了全局变量 `global` 并且将它使用在多个 Lambda 表达式中,但是如果你运行这个程序会发现,无论通过何种方式捕获全局变量,都会发现它永远指向的是那个全局对象,而不会创建任何一个本地的拷贝对象出来。 703 | 704 | 这是因为,只有在自动存储期间的变量会被捕获。GCC 甚至会对此提出警告: 705 | 706 | ```plaintext 707 | warning: capture of variable 'global' with non-automatic storage duration. 708 | ``` 709 | 710 | 这个警告只会在显式捕获一个全局变量时出现,即便你用 `[=]`,编译器也无法帮你消除这个错误。 711 | 712 | 在 Clang 中甚至会直接提示错误: 713 | 714 | ```plaintext 715 | error: 'global' cannot be captured because it does not have 716 | automatic storage duration 717 | 718 | ``` 719 | 720 | ### 捕获静态变量 721 | 722 | 和捕获全局变量类似,在捕获静态变量的时候也会遇到类似的问题。 723 | 724 | > 代码 2-12 [捕获静态变量](https://wandbox.org/permlink/CpJt4PUSleIJNVf2) 725 | 726 | ```cpp 727 | #include 728 | 729 | void bar() { 730 | static int static_int = 10; 731 | std::cout << static_int << std::endl; 732 | auto foo = [=]() mutable noexcept { 733 | ++static_int; 734 | }; 735 | foo(); 736 | std::cout << static_int << std::endl; 737 | const auto increase = []() noexcept { 738 | ++static_int; 739 | }; 740 | increase(); 741 | std::cout << static_int << std::endl; 742 | const auto moreIncrease = [static_int]() noexcept { 743 | ++static_int; 744 | }; 745 | moreIncrease(); 746 | std::cout << static_int << std::endl; 747 | } 748 | 749 | int main() { 750 | bar(); 751 | } 752 | ``` 753 | 754 | 这一次,我们尝试捕获静态变量并修改它的值,但是由于它没有自动存储时间,编译器并不会允许你这么做。(GCC 会提示警告,而 Clang 会直接报错) 755 | 756 | 输出: 757 | 758 | ```plaintext 759 | 10 760 | 11 761 | 12 762 | 13 763 | ``` 764 | 765 | ### 捕获类成员和 `this` 指针 766 | 767 | 当你想在一个类的成员函数中尝试捕获一个成员变量,那么事情就会稍微变得有点复杂。 768 | 769 | 由于所有的数据成员都是和 `this` 指针关联起来的,当然了,这玩意必须被存储在某个地方。 770 | 771 | > 代码 2-13 [捕获成员变量时的错误](https://wandbox.org/permlink/mp5VgqIyu5LWLn0f) 772 | 773 | ```cpp 774 | #include 775 | 776 | struct Baz { 777 | void foo() { 778 | const auto lam = [s]() { 779 | std::cout << s; 780 | }; 781 | lam(); 782 | } 783 | std::string s; 784 | }; 785 | 786 | int main() { 787 | Baz b; 788 | b.foo(); 789 | } 790 | ``` 791 | 792 | 这段代码尝试去捕获一个成员变量,但是编译器并不同意,这会导致编译器编译错误: 793 | 794 | ```plaintext 795 | In member function 'void Baz::foo()': 796 | error: capture of non-variable 'Baz::s' 797 | error: 'this' was not captured for this lambda function 798 | ``` 799 | 800 | 为解决此问题,需要捕获 `this` 指针。这样就能访问到成员变量了。 801 | 802 | 而上面的代码也可以这样修改: 803 | 804 | ```cpp 805 | struct Baz { 806 | void foo() { 807 | const auto lam = [this]() { 808 | std::cout << s; 809 | }; 810 | lam(); 811 | } 812 | std::string s; 813 | }; 814 | ``` 815 | 816 | 这样就不会有编译错误了。 817 | 818 | 当然了,你也可以使用 `[=]` 或者 `[&]` 来捕获 `this` 指针,在 C++11/14 中他们的效果是一样的。 819 | 820 | 但是,请注意,值捕获 `this` 也是捕获指针,这就是为什么你能访问成员变量的原因。 821 | 822 | 在 C++11/14 中,你不能够这样写: 823 | 824 | ```cpp 825 | const auto lam = [*this]() { 826 | std::cout << s; 827 | }; 828 | ``` 829 | 830 | 但是它在 C++17 中是允许的。 831 | 832 | 如果你在单一方法的上下文中使用捕获 `this`,这挺好的。 833 | 834 | 但是稍微复杂的场景下使用捕获 `this` 呢? 835 | 836 | > 代码 2-14 从方法中返回 Lambda 837 | 838 | ```cpp 839 | #include 840 | #include 841 | 842 | struct Baz { 843 | std::function foo() { 844 | return [=] { 845 | std::cout << s << std::endl; 846 | }; 847 | } 848 | std::string s; 849 | }; 850 | 851 | int main() { 852 | auto f1 = Baz{"abc"}.foo(); 853 | auto f2 = Baz{"xyz"}.foo(); 854 | f1(); 855 | f2(); 856 | } 857 | ``` 858 | 859 | 代码中声明了 `Baz` 这个对象,并且调用了 `foo()`。 860 | 861 | 请注意,`foo()` 返回了一个从类中捕获成员的 Lambda(存储在 `std::function` 中)。 862 | 863 | `std::function` 在 C++11 中是必需的,因为常规函数没有返回类型推导。 864 | 865 | 但是 C++14 支持函数返回类型的推导。 866 | 867 | 由于我们使用的是临时对象,我们不能保证当我们调用 `f1` 和 `f2` 时会发生什么。 868 | 869 | 这是一个悬空引用的问题,并且是未定义行为(Undefined Behaviour)。 870 | 871 | 这种行为类似于下面这段代码: 872 | 873 | ```cpp 874 | struct Bar { 875 | std::string const& foo() const { 876 | return s; 877 | }; 878 | std::string s; 879 | }; 880 | auto&& f1 = Bar{"abc"}.foo(); // 一个悬空引用 881 | ``` 882 | 883 | 当然,如果你显式捕获,也是 [一样](https://wandbox.org/permlink/FOgbNGoQHOmepBgY) 的。 884 | 885 | ```cpp 886 | std::function foo() { 887 | return [s] { 888 | std::cout << s << std::endl; 889 | }; 890 | } 891 | ``` 892 | 893 | 总而言之,当 Lambda 生命周期比对象更长时,捕获 `this` 可能会变得棘手。 894 | 895 | 当您使用异步调用或多线程时,可能会发生这种情况。 896 | 897 | 在 C++17 章节中,我们会重新详细讨论这个话题 898 | 899 | ### 只能移动的对象 900 | 901 | 假如,现在有一个“仅可移动”的对象(像 `unique_ptr`),那么你就无法将它作为捕获对象移动到 Lambda 中。 902 | 903 | 值捕获将不起作用,只能进行引用捕获。 904 | 905 | ```cpp 906 | std::unique_ptr p(new int{10}); 907 | auto foo = [p]() {}; // does not compile.... 908 | auto foo_ref = [&p]() {}; // compiles, but the ownership is not passed 909 | ``` 910 | 911 | 上述例子中,你会发现捕获 `unique_ptr` 的唯一方式是引用捕获,但是这种方式并不是最好的方式,因为它并没有将 `unique_ptr` 的所属权进行转移。 912 | 913 | 在下一章 C++14 中,由于初始化捕获的引入,这个问题会被修复。你可以在初始化捕获直接查阅内容。 914 | 915 | ### 保留常量 916 | 917 | 如果捕获一个 `const` 修饰的变量,那么它的常量性将会被保留。 918 | 919 | > 代码 2-15 [保留常量的 `const` 特性](https://wandbox.org/permlink/h8lCuOXd9dHsopG1) 920 | 921 | ```cpp 922 | #include 923 | #include 924 | 925 | int main() { 926 | const int x = 10; 927 | auto foo = [x]() mutable { 928 | std::cout << std::is_const::value << std::endl; 929 | x = 11; 930 | }; 931 | foo(); 932 | } 933 | ``` 934 | 935 | 这段代码将不会被编译器通过,因为捕获的对象是一个常量,即便使用 `mutable` 来修饰也无济于事。 936 | 937 | ### 捕获参数包 938 | 939 | 为了结束我们对“捕获”的讨论,在最后我们来聊聊使用可变参数模板来进行捕获。 940 | 941 | 编译器会将参数包扩展为非静态数据成员列表,如果您想在模板化代码中使用 Lambda,这会十分方便。代码示例: 942 | 943 | > 代码 2-16 [捕获可变参数包](https://wandbox.org/permlink/29qxFbLefKf3wnNU) 944 | 945 | ```cpp 946 | #include 947 | #include 948 | 949 | template 950 | void captureTest(Args... args) { 951 | const auto lambda = [args...] { 952 | const auto tup = std::make_tuple(args...); 953 | std::cout << "tuple size: " << std::tuple_size::value << '\n'; 954 | std::cout << "tuple 1st: " << std::get<0>(tup) << '\n'; 955 | }; 956 | lambda(); // call it 957 | } 958 | 959 | int main() { 960 | captureTest(1, 2, 3, 4); 961 | captureTest("Hello world", 10.0f); 962 | } 963 | ``` 964 | 965 | 运行这段代码,结果为: 966 | 967 | ```plaintext 968 | tuple size: 4 969 | tuple 1st: 1 970 | tuple size: 2 971 | tuple 1st: Hello world 972 | ``` 973 | 974 | 在这里展示了使用可变长参数包进行值捕获(引用捕获同理),捕获的对象“存储”在一个 `tuple` 对象中,可以使用一些辅助函数来访问 `tuple` 中的数据和属性。 975 | 976 | 当然了,你也可以使用 [C++ Insight](https://cppinsights.io/s/19d3a45d) 来观察编译器是如果生成这个代码并且展开模板、参数包和 Lambda 的。 977 | 978 | > C++14 让捕获仅可移动类型成为可能,并且 C++20 中增强了对可变参数包的支持。 979 | 980 | ## 5. 返回类型 981 | 982 | 在多数情况下,您可以跳过 Lambda 的返回类型,让编译器为您推导类型。 983 | 984 | 最初,返回类型的推导仅限于函数体内仅包含单个 `return` 语句的 Lambda。 985 | 986 | 但是,由于 C++ 标准实现了一个更便捷的版本,因此这限制很快就取消了。 987 | 988 | 相关内容可以参考:[C++ Standard Core Language Defect Reports and Accepted Issues, Revision 104](http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_defects.html#975) 989 | 990 | 总结一下,从 C++11 开始,只要所有的 `return` 语句都是相同的类型,编译器就能够推断出返回类型。 991 | 992 | 如果所有的 `return` 语句都返回了一个表达式,并且返回表达式的类型都经过了一个从左值到右值的转换(7.1 \[conv.lavl])或者从数组到指针的转换(7.2 \[conv.array])或者从函数到指针的转换(7.3 \[conv.func]),那么他们的类型都是一样的,就是普通类型。 993 | 994 | > 代码 2-17 [返回类型推导](https://wandbox.org/permlink/sxtT30yKx9mwrYT3) 995 | 996 | ```cpp 997 | #include 998 | 999 | int main() { 1000 | const auto baz = [](int x) noexcept { 1001 | if (x < 20) 1002 | return x * 1.1; 1003 | else 1004 | return x * 2.1; 1005 | }; 1006 | static_assert(std::is_same::value, "has to be the same!"); 1007 | } 1008 | ``` 1009 | 1010 | 上面的例子中,有两个返回语句,但是他们都指向 `double` 类型,所以编译器能够推断出最终的类型。 1011 | 1012 | 在 C++14 中,推导常规函数时,lambda 的类型会自动更新以适应 `auto` 类型的规则。 1013 | 1014 | ### 尾部返回类型语法 1015 | 1016 | 如果你想显式的凸显返回类型,那么可以使用尾部返回类型的语法。 1017 | 1018 | 举个例子: 1019 | 1020 | > 代码 2-18 lambda 返回字符串序列 1021 | 1022 | ```cpp 1023 | #include 1024 | #include 1025 | 1026 | int main() { 1027 | const auto testSpeedString = [](int speed) noexcept { 1028 | if (speed > 100) 1029 | return "you're a super fast"; 1030 | return "you're a regular"; 1031 | }; 1032 | auto str = testSpeedString(100); 1033 | str += " driver"; // 出错! const char*类型没有+=操作符可以应用 1034 | std::cout << str; 1035 | return 0; 1036 | } 1037 | ``` 1038 | 1039 | 当然,这段代码是无法编译的,因为编译器自动推断的类型结果是 const char\*,作为 lambda 的返回类型,+= 操作符无法应用于 const char \* 类型,所以编译器阻止了这种行为。 1040 | 1041 | 当然,我们稍微修改一下,[上述代码](http://coliru.stacked-crooked.com/a/45cebc8b35d5b2a9) 就可以正常工作了。 1042 | 1043 | ```cpp 1044 | const auto testSpeedString = [](int speed) -> std::string { 1045 | if (speed > 100) 1046 | return "you're a super fast"; 1047 | return "you're a regular"; 1048 | }; 1049 | auto str = testSpeedString(100); 1050 | str += " driver"; 1051 | ``` 1052 | 1053 | 我们只是将 `noexcept` 移除,并更换为了 `std::string`。 1054 | 1055 | 当然,你也可以使用命名空间 `std::string_literals` 然后返回 `std::string` 类型的 `“you're a regular”`。 1056 | 1057 | ## 6. 转化为函数指针 1058 | 1059 | 当你的 Lambda 表达式没有捕获到任何变量时,编译器就会将其转换为一个常规函数指针。 1060 | 1061 | 可以查看标准草案中 [\[expr.prim.lambda#6\]](https://timsong-cpp.github.io/cppwp/n3337/expr.prim.lambda#6) 的定义: 1062 | 1063 | > 没有捕获的 Lambda 表达式的闭包类型具有公共非虚拟、非显式的 `const` 转换函数,指向与闭包类型的函数调用运算符具有相同参数和返回类型的函数的指针。 1064 | > 1065 | > 此转换函数返回的值应为函数的地址,该函数在调用时与调用闭包类型的函数调用运算符具有相同的效果。 1066 | 1067 | 为了阐明 Lambda 是如何支持这种转换,让我们考虑以下示例。 1068 | 1069 | 它定义了一个明确定义转换运算符的仿函数 `baz`: 1070 | 1071 | > 代码 2-19 [转化函数指针](https://wandbox.org/permlink/XAmjjJiojnFKyd44) 1072 | 1073 | ```cpp 1074 | #include 1075 | 1076 | void callWith10(void (*bar)(int)) { 1077 | bar(10); 1078 | } 1079 | 1080 | int main() { 1081 | struct { 1082 | using f_ptr = void (*)(int); 1083 | void operator()(int s) const { 1084 | return call(s); 1085 | } 1086 | operator f_ptr() const { 1087 | return &call; 1088 | } 1089 | 1090 | private: 1091 | static void call(int s) { 1092 | std::cout << s << '\n'; 1093 | }; 1094 | } baz; 1095 | 1096 | callWith10(baz); 1097 | callWith10([](int x) { 1098 | std::cout << x << '\n'; 1099 | }); 1100 | } 1101 | ``` 1102 | 1103 | 在这个程序中,有一个 `callWith10` 函数,它接受一个函数指针。 1104 | 1105 | 然后我们用两个参数调用它(第 23 行和第 24 行):第一个使用 `baz`,它是一个包含必要转换运算符的仿函数 - 它转换为 `f_ptr`,与 `callWith10` 的输入参数相同。第二个使用 Lambda。 1106 | 1107 | 在这种情况下,编译器将执行所需的转换。 1108 | 1109 | 当您需要调用需要回调的 C 风格的函数时,这种转换可能会很方便。 1110 | 1111 | 例如,下面的代码是从 C 标准库中调用 `qsort` 函数,同时使用 Lambda 来进行反向排序。 1112 | 1113 | > 代码 2-20 [调用 C 风格函数](https://wandbox.org/permlink/fEMhtqAXerDdCXG8) 1114 | 1115 | ```cpp 1116 | #include 1117 | #include 1118 | 1119 | int main() { 1120 | int values[] = {8, 9, 2, 5, 1, 4, 7, 3, 6}; 1121 | constexpr size_t numElements = sizeof(values) / sizeof(values[0]); 1122 | 1123 | std::qsort(values, numElements, sizeof(int), [](const void* a, const void* b) noexcept { 1124 | return (*(int*)b - *(int*)a); 1125 | }); 1126 | 1127 | for (const auto& val : values) 1128 | std::cout << val << ", "; 1129 | } 1130 | ``` 1131 | 1132 | 正如您在代码示例中看到的那样,使用 `std::qsort` 仅将函数指针作为比较器。 1133 | 1134 | 编译器可以对我们传递的无状态 Lambda 进行隐式转换。 1135 | 1136 | ### 一个有趣的例子 1137 | 1138 | 在讨论别的内容之前,这儿有一个可能十分有趣的例子,我们可以一起来研究下: 1139 | 1140 | > 代码 2-21 [加号和 Lambda 表达式](https://wandbox.org/permlink/0r0jDiJxlLuEzYwm) 1141 | 1142 | ```cpp 1143 | #include 1144 | 1145 | int main() { 1146 | auto funcPtr = +[] {}; 1147 | static_assert(std::is_same::value); 1148 | } 1149 | ``` 1150 | 1151 | 注意一下这个“奇怪”的“`+`”运算符的语法,如果你去掉这个加号,那整个 `static_assert` 就失败了。 1152 | 1153 | 这是什么神奇的原因? 1154 | 1155 | 要理解其中的工作原理,我们得先看看 C++ 生成的代码是什么样的: 1156 | 1157 | ```cpp 1158 | #include 1159 | 1160 | int main() 1161 | { 1162 | 1163 | class __lambda_4_18 1164 | { 1165 | public: 1166 | inline void operator()() const 1167 | { 1168 | } 1169 | 1170 | using retType_4_18 = auto (*)() -> void; 1171 | inline operator retType_4_18 () const noexcept 1172 | { 1173 | return __invoke; 1174 | } 1175 | 1176 | private: 1177 | static inline void __invoke() 1178 | { 1179 | } 1180 | 1181 | 1182 | } __lambda_4_18{}; 1183 | 1184 | using FuncPtr_4 = void (*)(); 1185 | FuncPtr_4 funcPtr = +static_cast(__lambda_4_18.operator __lambda_4_18::retType_4_18()); 1186 | /* PASSED: static_assert(std::integral_constant::value); */ 1187 | } 1188 | ``` 1189 | 1190 | 代码中的 `+` 是一个一元运算符,且可以运用在指针上。 1191 | 1192 | 因此编译器将无状态的 Lambda 转换成了函数指针,再分配给 `funcPtr`。 1193 | 1194 | 另一方面,如果删除了“`+`”号,那么 `funcPtr` 就仅仅是一个单纯的闭包对象,所以在 `static_assert` 时会失败。 1195 | 1196 | 虽然用 `+` 这样的语法可能不是一个最好的方式,但是你可以用 `static_cast` 来替代,可以实现和 `+` 一样的效果。 1197 | 1198 | 当您不希望编译器创建太多函数实例时,您可以应用此技术。例如: 1199 | 1200 | > 代码 2-22 [强制转换为函数调用](https://cppinsights.io/s/e4764e54) 1201 | 1202 | ```cpp 1203 | template 1204 | void call_function(F f) { 1205 | f(10); 1206 | } 1207 | 1208 | int main() { 1209 | call_function(static_cast([](int x) { 1210 | return x + 2; 1211 | })); 1212 | call_function(static_cast([](int x) { 1213 | return x * 2; 1214 | })); 1215 | } 1216 | ``` 1217 | 1218 | 在上面的例子中,编译器只需要创建一个 `call_function` 实例,因为它只需要一个函数指针 `int (*)(int)`。 1219 | 1220 | 但是如果你删除 `static_casts` 那么你将得到两个版本的 `call_function` 因为编译器必须为 Lambdas 创建两个单独的类型。 1221 | 1222 | ## 7. IIFE - 立即调用函数表达式 1223 | 1224 | 在多数例子中,你可能发现了,我经常都是先定义好 Lambda,在之后才去调用它。 1225 | 1226 | 然而,你也可以直接立即调用一个 lambda: 1227 | 1228 | > 代码 2-23 [“现写现用”Lambda](https://wandbox.org/permlink/fsFOxzBZuFS7bMVn) 1229 | 1230 | ```cpp 1231 | #include 1232 | 1233 | int main() { 1234 | int x = 1, y = 1; 1235 | 1236 | [&]() noexcept { 1237 | ++x; 1238 | ++y; 1239 | }(); // <-- call () 1240 | 1241 | std::cout << x << ", " << y; 1242 | } 1243 | ``` 1244 | 1245 | 上面这个例子,Lambda 在被创建之后没有赋给任何一个闭包对象,而是直接被调用(通过“`()`”操作符)。 1246 | 1247 | 如果你运行上述程序,期望结果应该是输出了: 1248 | 1249 | ```plaintext 1250 | 2, 2 1251 | ``` 1252 | 1253 | 在遇到复杂的常量对象的初始化时,这种表达式将十分地有用: 1254 | 1255 | ```cpp 1256 | const auto val =[]() { 1257 | /* several lines of code... */ 1258 | }(); // call it! 1259 | ``` 1260 | 1261 | 其中,`val` 是一个常量(constant value),并且其类型为 Lambda 表达式的返回类型: 1262 | 1263 | ```cpp 1264 | // val1 is int 1265 | const auto val1 =[]() { return 10; }(); 1266 | // val2 is std::string 1267 | const auto val2 =[]() ->std::string { return "ABC"; }(); 1268 | ``` 1269 | 1270 | 下面我们来看一个较长的用例,在函数内部使用 IIFE 形式来构造一个辅助 Lambda 函数,去创建一个常量。 1271 | 1272 | > 代码 2-24 [IIFE 和 HTML 生成器](https://wandbox.org/permlink/TtlM1t3sm9EZOUrw) 1273 | 1274 | ```cpp 1275 | #include 1276 | #include 1277 | 1278 | void ValidateHTML(const std::string&) {} 1279 | 1280 | std::string BuildAHref(const std::string& link, const std::string& text) { 1281 | const std::string html = [&link, &text] { 1282 | const auto& inText = text.empty() ? link : text; 1283 | return "" + inText + ""; 1284 | }(); // call! 1285 | ValidateHTML(html); 1286 | return html; 1287 | } 1288 | 1289 | int main() { 1290 | try { 1291 | const auto ahref = BuildAHref("www.leanpub.com", "Leanpub Store"); 1292 | std::cout << ahref; 1293 | } catch (...) { 1294 | std::cout << "bad format..."; 1295 | } 1296 | } 1297 | ``` 1298 | 1299 | 这个用例中,函数 `BuildAHref()`,它接受两个参数,然后构建一个 ` ` HTML 标签。 1300 | 1301 | 根据输入参数,我们构建 `html` 变量。 1302 | 1303 | 如果文本不为空,则我们将其用作内部 HTML 值。 1304 | 1305 | 否则,我们使用默认链接。 1306 | 1307 | 我们希望 html 变量是常量,但很难编写具有输入参数所需条件的紧凑代码。 1308 | 1309 | 多亏了 IIFE,我们可以编写单独的 Lambda,然后用 `const` 标记我们的变量。 1310 | 1311 | 稍后可以将变量传递给 `ValidateHTML`。 1312 | 1313 | ### 可读性提示 1314 | 1315 | 有些时候,利用现写现用的 Lambda 表达式会造成一些代码可读性上的困扰。 1316 | 1317 | 例如: 1318 | 1319 | ```cpp 1320 | const auto EnableErrorReporting = [&]() { 1321 | if (HighLevelWarningEnabled()) 1322 | return true; 1323 | if (HighLevelWarningEnabled()) 1324 | return UsersWantReporting(); 1325 | return false; 1326 | }(); 1327 | 1328 | if (EnableErrorReporting) { 1329 | // ... 1330 | } 1331 | ``` 1332 | 1333 | 在上面的例子中,Lambda 代码相当复杂,阅读代码的开发人员不仅要解密 Lambda 是立即调用的,而且还要对 `EnableErrorReporting` 类型进行推理。 1334 | 1335 | 他们可能会假设 `EnableErrorReporting` 是闭包对象而不仅仅是一个常量变量。 1336 | 1337 | 对于这种情况,您可能会考虑不使用 `auto`,以便我们可以轻松查看类型。 1338 | 1339 | 甚至可以在 `}()` 旁边添加注释,例如 `// call it now`。 1340 | 1341 | > 在 C++17 章节我们会遇到一个“升级版”的 IIFE。 1342 | 1343 | ## 8. Lambda 继承 1344 | 1345 | 也许你会有些吃惊,Lambda 居然还可以派生? 1346 | 1347 | 由于编译器将 Lambda 扩展为了一个仿函数对象,并重载了其调用操作符 `()`,所以我们可以从这点去继承 Lambda。 1348 | 1349 | 来看看一个基础代码: 1350 | 1351 | > 代码 2-25 [从单个 Lambda 中继承](https://wandbox.org/permlink/uA4q7Zy1kojUZmqb) 1352 | 1353 | ```cpp 1354 | #include 1355 | 1356 | template 1357 | class ComplexFunctor : public Callable { 1358 | public: 1359 | explicit ComplexFunctor(Callable f) : Callable(f) {} 1360 | }; 1361 | 1362 | template 1363 | ComplexFunctor MakeComplexFunctor(Callable&& cal) { 1364 | return ComplexFunctor(cal); 1365 | } 1366 | 1367 | int main() { 1368 | const auto func = MakeComplexFunctor([]() { 1369 | std::cout << "Hello Functor!"; 1370 | }); 1371 | func(); 1372 | } 1373 | ``` 1374 | 1375 | 这段代码中,有一个 `ComplexFunctor` 类,它派生自 `Callable`,它是一个模板参数。如果我们想从 Lambda 派生,我们需要做一个小技巧,因为我们无法拼出闭包类型的确切类型(除非我们将它包装到 `std::function` 中)。 1376 | 1377 | 这就是为什么我们需要可以执行模板参数推导并获取 Lambda 闭包类型的 `MakeComplexFunctor` 函数。 1378 | 1379 | 除了名称之外,`ComplexFunctor` 只是一个简单的包装器,没有多大用处。是否有此类代码模式的用例。 1380 | 1381 | 例如,我们可以扩展上面的代码并继承两个 Lambdas 并创建一个重载集: 1382 | 1383 | > 代码 2-25 [从两个 Lambda 中继承](https://wandbox.org/permlink/2AY4nRaHffrDWt6A) 1384 | 1385 | ```cpp 1386 | #include 1387 | 1388 | template 1389 | class SimpleOverloaded : public TCall, UCall { 1390 | public: 1391 | SimpleOverloaded(TCall tf, UCall uf) : TCall(tf), UCall(uf) {} 1392 | using TCall::operator(); 1393 | using UCall::operator(); 1394 | }; 1395 | 1396 | template 1397 | SimpleOverloaded MakeOverloaded(TCall&& tf, UCall&& uf) { 1398 | return SimpleOverloaded(tf, uf); 1399 | } 1400 | 1401 | int main() { 1402 | const auto func = MakeOverloaded( 1403 | [](int) { 1404 | std::cout << "Int!\n"; 1405 | }, 1406 | [](float) { 1407 | std::cout << "Float!\n"; 1408 | }); 1409 | func(10); 1410 | func(10.0f); 1411 | } 1412 | ``` 1413 | 1414 | 这次我们有更多的代码:我们从两个模板参数派生,但我们还需要显式地公开它们的调用运算符。 1415 | 1416 | 这是为什么呢?这是因为在寻找正确的函数重载时,编译器要求候选对象在同一范围内。 1417 | 1418 | 为了理解这一点,让我们编写一个派生自两个基类的简单类型。 1419 | 1420 | 该示例还注释掉了两个 `using` 语句。 1421 | 1422 | ```cpp 1423 | #include 1424 | 1425 | struct BaseInt { 1426 | void Func(int) { 1427 | std::cout << "BaseInt...\n"; 1428 | } 1429 | }; 1430 | struct BaseDouble { 1431 | void Func(double) { 1432 | std::cout << "BaseDouble...\n"; 1433 | } 1434 | }; 1435 | struct Derived : public BaseInt, BaseDouble { 1436 | // using BaseInt::Func; 1437 | // using BaseDouble::Func; 1438 | }; 1439 | 1440 | int main() { 1441 | Derived d; 1442 | d.Func(10.0); 1443 | } 1444 | ``` 1445 | 1446 | 我们有两个实现 `Func` 的基类。我们想从派生对象调用这个 `Func` 方法。 1447 | 1448 | [GCC](https://wandbox.org/permlink/fFRqVGUisdQh1qGV) 编译便会报出错误: 1449 | 1450 | ```plaintext 1451 | error:request formember 'Func' is ambiguous 1452 | ``` 1453 | 1454 | 因为我们注释掉了全部的声明来自 `BaseInt` 和 `BaseDouble` 的 `::Func` 的 `using` 语句。 1455 | 1456 | 编译器有两个作用域来搜索最佳候选,根据标准,这是不允许的。 1457 | 1458 | 好吧,让我们回到我们的更上面那个例子:`SimpleOverloaded` 是一个基本类,它不是生产就绪的。 1459 | 1460 | 看看 C++17 章,我们将讨论此模式的高级版本。 1461 | 1462 | 多亏了 C++17 的几个特性,我们将能够从多个 Lambda 继承(感谢可变参数模板)并利用更多的紧凑语法! 1463 | 1464 | ## 9. 在容器中存储 Lambda 1465 | 1466 | 作为本章的最后一个技巧,让我们来看看在容器中存储闭包的问题。 1467 | 1468 | 但是我不是写过不能默认创建和分配 Lambdas 吗? 1469 | 1470 | 是的……但是我们可以在这里做一些技巧。 1471 | 1472 | 技术之一是利用转换为函数指针的无状态 Lambda 的属性。 1473 | 1474 | 虽然您不能直接存储闭包对象,但您可以保存从 Lambda 表达式转换而来的函数指针。 1475 | 1476 | 例如: 1477 | 1478 | > 代码 2-26 [将 Lambda 存为函数指针](https://wandbox.org/permlink/CuoCtCHEEk6ZA6xF) 1479 | 1480 | ```cpp 1481 | #include 1482 | #include 1483 | int main() { 1484 | using TFunc = void (*)(int&); 1485 | std::vector ptrFuncVec; 1486 | ptrFuncVec.push_back([](int& x) { 1487 | std::cout << x << '\n'; 1488 | }); 1489 | ptrFuncVec.push_back([](int& x) { 1490 | x *= 2; 1491 | }); 1492 | ptrFuncVec.push_back(ptrFuncVec[0]); // print it again; 1493 | int x = 10; 1494 | for (const auto& entry : ptrFuncVec) entry(x); 1495 | } 1496 | ``` 1497 | 1498 | 在上面的例子中,我们创建了一个将应用于变量的函数向量。容器中有三个条目: 1499 | 1500 | * 第一个打印输入参数的值。 1501 | * 第二个修改值。 1502 | * 第三个是第一个的副本,因此它也打印值。 1503 | 1504 | 该解决方案有效,但仅限于无状态 Lambda。 1505 | 1506 | 如果我们想解除这个限制怎么办? 1507 | 1508 | 为了解决这个问题,我们可以使用重度助手:`std::function`。 1509 | 1510 | 为了使示例有趣,它还从简单的整数切换到处理 `std::string objects` 的 Lambdas: 1511 | 1512 | > 代码 2-27 [将 Lambda 存为 std::function](https://wandbox.org/permlink/XSK4ATo5HriOB6Gk) 1513 | 1514 | ```cpp 1515 | #include 1516 | #include 1517 | #include 1518 | #include 1519 | 1520 | int main() { 1521 | std::vector> vecFilters; 1522 | 1523 | size_t removedSpaceCounter = 0; 1524 | const auto removeSpacesCnt = [&removedSpaceCounter](const std::string& str) { 1525 | std::string tmp; 1526 | std::copy_if(str.begin(), str.end(), std::back_inserter(tmp), [](char ch) {return !isspace(ch); }); 1527 | removedSpaceCounter += str.length() - tmp.length(); 1528 | return tmp; 1529 | }; 1530 | 1531 | const auto makeUpperCase = [](const std::string& str) { 1532 | std::string tmp = str; 1533 | std::transform(tmp.begin(), tmp.end(), tmp.begin(), 1534 | [](unsigned char c){ return std::toupper(c); }); 1535 | return tmp; 1536 | }; 1537 | 1538 | vecFilters.emplace_back(removeSpacesCnt); 1539 | vecFilters.emplace_back([](const std::string& x) { return x + " Amazing"; }); 1540 | vecFilters.emplace_back([](const std::string& x) { return x + " Modern"; }); 1541 | vecFilters.emplace_back([](const std::string& x) { return x + " C++"; }); 1542 | vecFilters.emplace_back([](const std::string& x) { return x + " World!"; }); 1543 | vecFilters.emplace_back(makeUpperCase); 1544 | 1545 | const std::string str = " H e l l o "; 1546 | auto temp = str; 1547 | for (const auto &entryFunc : vecFilters) 1548 | temp = entryFunc(temp); 1549 | std::cout << temp; 1550 | 1551 | std::cout <<"\nremoved spaces: " << removedSpaceCounter << '\n'; 1552 | } 1553 | ``` 1554 | 1555 | 输出: 1556 | 1557 | ```plaintext 1558 | HELLO AMAZING MODERN C++ WORLD! 1559 | removed spaces: 12 1560 | ``` 1561 | 1562 | 这次我们将 `std::function` 存储在容器中。 1563 | 1564 | 这允许我们使用任何类型的函数对象,包括带有捕获变量的 Lambda 表达式。 1565 | 1566 | 其中一个 lambda `removeSpacesCnt` 捕获一个变量,该变量用于存储有关从输入字符串中删除的空格的信息。 1567 | 1568 | ## 10. 总结 1569 | 1570 | 在本章中,您学习了如何创建和使用 Lambda 表达式。我描述了 Lambda 的语法、捕获子句、类型,并且我们涵盖了许多示例和用例。 1571 | 1572 | 我们甚至更进一步,我向您展示了从 Lambda 派生或将其存储在容器中的模式。 1573 | 1574 | 但这还不是全部! 1575 | 1576 | Lambda 表达式成为现代 C++ 的重要组成部分。 1577 | 1578 | 随着应用场景越来越多,开发人员也看到了改进此功能的可能性。 1579 | 1580 | 这就是为什么您现在可以转到下一章并查看 ISO 委员会在 C++14 中添加的重要更新的原因。 1581 | --------------------------------------------------------------------------------