<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>P2Tree&#39;s Mill</title>
  
  
  <link href="https://p2tree.top/atom.xml" rel="self"/>
  
  <link href="https://p2tree.top/"/>
  <updated>2026-05-25T13:05:39.000Z</updated>
  <id>https://p2tree.top/</id>
  
  <author>
    <name>P2Tree</name>
    
  </author>
  
  <generator uri="https://hexo.io/">Hexo</generator>
  
  <entry>
    <title>Chapter.244</title>
    <link href="https://p2tree.top/posts/3fc5b93e.html"/>
    <id>https://p2tree.top/posts/3fc5b93e.html</id>
    <published>2026-05-25T20:59:13.000Z</published>
    <updated>2026-05-25T13:05:39.000Z</updated>
    
    <content type="html"><![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/assets/css/APlayer.min.css"><script src="/assets/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><p>人的能力是有限的，所以只可能在一个有限的范围内做出正确的决定，找到这个边界，认清这个边界，并在试图尝试在边界外发表观点时保持谦卑。</p><hr><div class="note primary flat"><p>封面图片来自豆包 AI。</p><p>转载自我自己的<a href="https://mp.weixin.qq.com/s/WitUKkt6ytHEQAMeEbBMkw">微信公众号</a>，欢迎关注。</p></div>]]></content>
    
    
      
      
    <summary type="html">&lt;link rel=&quot;stylesheet&quot; class=&quot;aplayer-secondary-style-marker&quot; href=&quot;/assets/css/APlayer.min.css&quot;&gt;&lt;script src=&quot;/assets/js/APlayer.min.js&quot; cla</summary>
      
    
    
    
    <category term="生活感悟" scheme="https://p2tree.top/categories/%E7%94%9F%E6%B4%BB%E6%84%9F%E6%82%9F/"/>
    
    
    <category term="个人发展" scheme="https://p2tree.top/tags/%E4%B8%AA%E4%BA%BA%E5%8F%91%E5%B1%95/"/>
    
  </entry>
  
  <entry>
    <title>Claude Code 实践指南（三）：组织 Skills 系统</title>
    <link href="https://p2tree.top/posts/37a32ba9.html"/>
    <id>https://p2tree.top/posts/37a32ba9.html</id>
    <published>2026-05-22T22:42:38.000Z</published>
    <updated>2026-05-22T14:47:56.000Z</updated>
    
    <content type="html"><![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/assets/css/APlayer.min.css"><script src="/assets/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><ol class="series-items"><li><a href="/posts/4aab6423.html" title="Claude Code 实践指南（一）：开始第一次对话">Claude Code 实践指南（一）：开始第一次对话</a></li><li><a href="/posts/7abd88fb.html" title="Claude-Code-实践指南（二）：编写操作规范">Claude-Code-实践指南（二）：编写操作规范</a></li><li><a href="/posts/37a32ba9.html" title="Claude Code 实践指南（三）：组织 Skills 系统">Claude Code 实践指南（三）：组织 Skills 系统</a></li></ol><p>很早之前就想写这篇关于 Skills 的介绍，但一直犹豫，Skills 的生态变化太快，我怕写完的时候，它已经又是另一副面孔了。不过想了想，核心机制是稳定的，变了的大多是表面，所以还是写吧。</p><p>上一篇我们聊了 CLAUDE.md，它是厨房操作规范，静态的规则和偏好，写一次天天生效。但你在后厨干活，光有规范还不够。炒宫保鸡丁有炒宫保鸡丁的套路，先腌肉、再调汁、最后大火爆炒，这是一套流程，不是一条规则。你没法把 ”宫保鸡丁做法” 塞进一条备忘录里，因为它不是偏好，而是一套有序的操作。</p><p>Skills，就是把这套流程打包起来。Claude 通过学习 skill 中的描述，就知道该按什么顺序、用什么标准来操作。</p><h2 id="CLAUDE-md-的局限在哪里"><a href="#CLAUDE-md-的局限在哪里" class="headerlink" title="CLAUDE.md 的局限在哪里"></a>CLAUDE.md 的局限在哪里</h2><p>CLAUDE.md 能做的事很明确，告诉 Claude 你的口味偏好和项目规矩。但它有个硬伤，只能表达静态规则，无法封装<strong>多步骤工作流</strong> 或<strong>特定领域知识</strong> 。</p><p>你可以在 CLAUDE.md 里写 ”代码提交前必须过审查”，但并不适合详细写清楚 ”审查具体分几步、每步看什么、输出什么格式的报告”。这就像告诉大厨 ”菜要好吃”，但没给他菜谱，方向对了，操作模糊。</p><p>Skills 的本质，就是将一套标准操作流程（SOP）打包成 Claude 可调用、可重复执行的 ”技能单元”。</p><h2 id="一个-Skill-里面到底装了什么"><a href="#一个-Skill-里面到底装了什么" class="headerlink" title="一个 Skill 里面到底装了什么"></a>一个 Skill 里面到底装了什么</h2><p>一个 Skill 就是一个包含 <code>SKILL.md</code> 文件的目录。结构很简单：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">.claude/skills/code-review/</span><br><span class="line">├── SKILL.md</span><br><span class="line">├── (可选) scripts/       # 可执行脚本，Claude 可直接调用</span><br><span class="line">├── (可选) references/    # 参考文档，按需加载进上下文</span><br><span class="line">└── (可选) assets/        # 模板、示例等附属资源</span><br></pre></td></tr></table></figure><p><code>SKILL.md</code> 的格式也不复杂，核心就是一段 YAML frontmatter 加正文，举个例子：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">---</span><br><span class="line">name: code-review</span><br><span class="line">description: Execute a thorough code review following team standards</span><br><span class="line">---</span><br><span class="line"></span><br><span class="line"># Code Review Process</span><br><span class="line"></span><br><span class="line">## Step 1: Read the diff</span><br><span class="line">...</span><br></pre></td></tr></table></figure><p>frontmatter（两个 <code>---</code> 之间的 YAML）定义技能的元数据。<code>name</code> 是标识符，必须和目录名一致；<code>description</code> 是唯一触发信号，Claude 只靠它来决定是否自动调用这个 skill。写得太模糊，Claude 不知道什么时候该用；写得太窄，又可能漏掉该触发的场景。</p><h3 id="description-的书写策略"><a href="#description-的书写策略" class="headerlink" title="description 的书写策略"></a>description 的书写策略</h3><p>既然 description 是自动触发的唯一依据，值得花点篇幅说说怎么写。</p><p>Claude 有一个倾向：不触发（undertrigger）。它宁可不用 skill 也不要误用，所以你的 description 得 ”推” 它一把。具体来说，description 里应该同时包含两件事：<strong>这个 skill 做什么</strong> ，以及 <strong>什么时候该用它</strong> 。</p><p>举个糟糕的例子：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">description: Code review skill</span><br></pre></td></tr></table></figure><p>太简短了，Claude 不知道该在什么场景下触发。更好的写法：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">description: Use when a major project step has been completed and needs</span><br><span class="line">review against the original plan and coding standards</span><br></pre></td></tr></table></figure><p>这个写法明确了触发场景（”项目里程碑完成时”），Claude 就知道该在什么时候主动调用它。</p><p>还有一个常见的误区：把 ”什么时候用” 的信息写在 SKILL.md 的正文中，而不是 description 里。这是没用的，因为正文在触发之前 Claude 根本看不到。关于这一点，下一节会解释原因。</p><h3 id="其他-frontmatter-字段"><a href="#其他-frontmatter-字段" class="headerlink" title="其他 frontmatter 字段"></a>其他 frontmatter 字段</h3><p>除了 <code>name</code> 和 <code>description</code>，还有一些可选字段，我挑几个常用的：</p><table><thead><tr><th>字段</th><th>用途</th></tr></thead><tbody><tr><td>model</td><td>指定执行该 skill 时使用的模型，可选 haiku、sonnet、opus，默认继承当前会话的模型</td></tr><tr><td>allowed-tools</td><td>预授权的工具列表，减少权限弹窗。支持通配符如 Bash(git:*)</td></tr><tr><td>argument-hint</td><td>给用户的参数提示，比如 [pr-number] [priority]，会在斜杠命令菜单中显示</td></tr><tr><td>disable-model-invocation</td><td>设为 true 后，这个 skill 只能由用户手动 &#x2F;skill-name 触发，Claude 不会自动调用它</td></tr></tbody></table><p>其中 <code>model</code> 字段挺实用。如果一个 skill 只是做简单的格式化，指定 <code>haiku</code> 就够了，没必要动用 <code>opus</code>，省 Token 又快。如果是要做深度推理的代码审查，就指定 <code>opus</code>。</p><p>正常情况下，Claude 每次调用工具都会弹权限确认，如果你的 skill 需要频繁执行 git 命令，弹窗能把人弹崩溃。在 frontmatter 里写上 <code>allowed-tools: Bash(git:*)</code>，这类操作就自动放行，不再打扰你。</p><p><code>disable-model-invocation</code> 适合那些必须由人主动发起的 skill。比如一个会删除分支的清理 skill，你肯定不想让 Claude 自作主张的触发，加了这个字段就锁死了，只有你亲手 <code>/clean-branches</code> 才会执行。</p><h2 id="怎么让-Claude-用上-Skill"><a href="#怎么让-Claude-用上-Skill" class="headerlink" title="怎么让 Claude 用上 Skill"></a>怎么让 Claude 用上 Skill</h2><p>Skills 有两种触发方式。</p><p><strong>自动触发</strong> ：Claude 会根据你提的问题，自动判断是否需要调用某个 skill。这个判断依赖于 <code>SKILL.md</code> 中的 <code>description</code> 字段，所以我说 description 要写好。</p><p><strong>手动触发</strong> ：在对话中使用 <code>/skill-name</code>，这就是一个斜杠命令。比如你想让 Claude 用 <code>code-review</code> 技能来审查代码，直接输入：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">/code-review 请审查 app.py</span><br></pre></td></tr></table></figure><p>在 Claude 会话中输入 <code>/skills</code>，就会列出所有已加载的 Skills 清单。</p><p>如果你还没有安装任何 skill，这里输出就是空的。后边我会介绍如何安装 skill。</p><h3 id="Skills-和斜杠命令的关系"><a href="#Skills-和斜杠命令的关系" class="headerlink" title="Skills 和斜杠命令的关系"></a>Skills 和斜杠命令的关系</h3><p>在 <code>~/.claude/commands/</code> 目录，里面放的是 ”命令”，用 <code>/command-name</code> 触发。Skills 和 Commands 共享同一套触发机制，区别只在文件布局。<code>.claude/commands/</code> 是旧格式，<code>.claude/skills/name/SKILL.md</code> 是新格式，两者加载方式完全一样。</p><p>对于用户来说，skill 就是一个斜杠命令。在 Claude 会话中输入 <code>/</code>，会出现一个命令菜单，你所有的 skill 都列在里面，<code>description</code> 就是菜单里显示的说明文字。所以 description 不只是给 Claude 看的触发信号，也是给人看的说明，写清楚点，两方都受益。</p><h3 id="渐进式披露：Claude-怎么加载你的-Skill"><a href="#渐进式披露：Claude-怎么加载你的-Skill" class="headerlink" title="渐进式披露：Claude 怎么加载你的 Skill"></a>渐进式披露：Claude 怎么加载你的 Skill</h3><p>这是 Skills 机制里最精巧的设计，也是很多人忽略的部分。</p><p>Claude 对 Skill 的加载分三层，逐层递进：</p><p><strong>第一层：元数据（始终在上下文中）。</strong>  每次会话启动时，Claude 会加载所有 skill 的 <code>name</code> 和 <code>description</code>，组成一个可用技能清单。这一层的体积很小，每个 skill 大概 100 词左右，但它是 Claude 判断 ”该不该用这个 skill” 的唯一依据。</p><p><strong>第二层：SKILL.md 正文（触发时加载）。</strong>  当 Claude 决定调用某个 skill，或者用户用 <code>/skill-name</code> 手动触发时，SKILL.md 的完整正文才被加载到上下文中。这就是为什么我说 ”什么时候用” 的信息必须写在 description 里，而不是正文里，正文 Claude 在触发之前根本看不到。</p><p><strong>第三层：附属资源（按需加载）。</strong>  skill 目录下可以有 <code>scripts/</code>、<code>references/</code>、<code>assets/</code> 等子目录，里面放脚本、参考文档、模板文件。这些内容不会自动加载，只有 SKILL.md 的正文里指示 Claude 去读的时候，才会被拉进上下文。而且脚本可以直接执行，不必先加载源码。</p><p>这个三层设计解决了一个矛盾：你想让 Claude 知道很多技能的存在，但又不想把所有技能的完整内容都塞进上下文。这就是渐进式披露。</p><p>理解了这个机制，几个最佳实践就自然浮现了：description 要写清触发场景；SKILL.md 正文控制在 500 行以内，超出的内容拆到 references&#x2F; 里；别在正文里藏触发条件，Claude 看不到。</p><p>虽然 SKILL.md 的格式并不复杂，但写一份好的技能描述需要仔细推敲，description 越精确，Claude 触发和执行的准确度也就越高。</p><p>好消息是，你不必手动写。后面我会介绍怎么让 Claude 帮你生成。</p><h3 id="Skills-放在哪里，谁优先"><a href="#Skills-放在哪里，谁优先" class="headerlink" title="Skills 放在哪里，谁优先"></a>Skills 放在哪里，谁优先</h3><p>Skills 有三种存放位置，同时存在时将按优先级从高到低覆盖，高优先级的同名 skill 会覆盖低优先级。</p><table><thead><tr><th>位置</th><th>路径</th><th>是否提交 Git</th><th>适用场景</th></tr></thead><tbody><tr><td>项目 Skills</td><td>.claude&#x2F;skills&#x2F;</td><td>提交</td><td>团队共享的项目特定技能</td></tr><tr><td>个人 Skills</td><td>~&#x2F;.claude&#x2F;skills&#x2F;</td><td>不提交</td><td>个人常用技能</td></tr><tr><td>插件 Skills</td><td>~&#x2F;.claude&#x2F;plugins&#x2F;&#96;</td><td>不提交</td><td>插件绑定的专用技能</td></tr></tbody></table><p>个人技能跟着你走，项目技能跟着仓库走，插件技能跟着社区走。如果你在个人目录和项目目录各放了一个同名的 skill，项目级的会优先生效，这和 CLAUDE.md 的层级逻辑一致，近的覆盖远的。</p><h2 id="有哪些现成的-Skills-可以直接用"><a href="#有哪些现成的-Skills-可以直接用" class="headerlink" title="有哪些现成的 Skills 可以直接用"></a>有哪些现成的 Skills 可以直接用</h2><p>不必什么都自己写。社区里已经有不少经过打磨的技能，拿来改改就能用：</p><table><thead><tr><th>Skill 名称</th><th>功能</th></tr></thead><tbody><tr><td>code-review</td><td>按团队规范审查代码，输出结构化报告</td></tr><tr><td>brainstorming</td><td>是 superpower plugin 的一个 skill，在社区非常热门，它帮助你梳理技术思路和生成 spec 文档</td></tr><tr><td>grill-me</td><td>是 mattpocock 维护的一个 skill，它可以让 Claude 扮演任何角色，通过多次挖掘式对话来启发你的思考</td></tr><tr><td>find-skills</td><td>它可以帮助你找到适合你的技能，也会在交互时自动推荐社区的 skill</td></tr><tr><td>skill-creator</td><td>它可以帮你编写自己的 skill，下边会介绍怎么手工开发 skill</td></tr><tr><td>pptx</td><td>这是 Anthropic 官方的一个 skill，可以帮助你生成 PPT</td></tr><tr><td>frontend-design</td><td>这也是 Anthropic 官方提供的 skill，它可以帮助你设计前端界面</td></tr></tbody></table><p>这些 skill 都开源在 GitHub 上。你也可以通过 <code>find-skills</code> 来找到自己需要的 skill 或者直接访问 <code>skills.sh</code> 来查看所有可用的 skill。</p><p>顺便提一嘴，插件（plugin）是对一些 skills、Hooks 等的封装，让它们可以在 Claude Code 中以插件的形式安装使用。Claude 有个命令是 <code>/plugin</code>，可以用它直接安装一个社区插件包。</p><h3 id="怎么安装一个第三方-Skill"><a href="#怎么安装一个第三方-Skill" class="headerlink" title="怎么安装一个第三方 Skill"></a>怎么安装一个第三方 Skill</h3><p>其实不需要安装器，也不需要包管理器。一个 Skill 就是一个包含 SKILL.md 的目录。所谓安装，就是把那个目录放到 Claude Code 约定好的位置。这就是整个过程。</p><p>前文提过 Skills 的三个存放位置：个人目录 <code>~/.claude/skills/</code>、项目目录 <code>.claude/skills/</code>、以及通过插件安装。个人目录适合放通用的技能，不管打开哪个项目都能用；项目目录适合放跟项目绑定的技能，跟着仓库走。你要做的只是把 skill 文件夹复制到对应的路径下。</p><p>不过，这种手动复制粘贴的办法还是太原始了。程序员都是 “偷懒” 的一帮人，所以能用工具就不动手。</p><p>在一些插件或 skill 开源仓库中，会介绍怎么一键安装。最常见的方法是使用 skills.sh 安装器，它通过 <code>npx</code> 运行，比如安装 mattpocock 的 skills 包：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npx skills@latest add mattpocock/skills</span><br></pre></td></tr></table></figure><p>其中 <code>mattpocock/skills</code> 是 GitHub 上的一个仓库 <a href="https://github.com/mattpocock/skills">https://github.com/mattpocock/skills</a>，包含了多个 skill。</p><p>若想要安装插件，可以在 Claude Code 中使用 <code>/plugin</code> 命令，比如：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">/plugin install superpowers@claude-plugins-official</span><br></pre></td></tr></table></figure><p>这是 superpowers 插件包，一个社区上很火的 AI Agent 扩展。</p><p>安装完毕后，重启 Claude Code 后在会话中输入 <code>/skills</code>，就能看到新安装的 skill 出现在列表里了。</p><p>就这样。没有编译步骤，没有依赖解析，没有注册中心。Skill 就是一份自然语言写成的操作手册，Claude Code 启动时扫描指定目录，找到 SKILL.md 就加载元数据，触发时读正文。</p><p>不过需要明确的一点是，安装第三方 skill 之前，建议先扫一眼它的 SKILL.md 内容，重点看里面有没有 Bash 命令。如果看到 <code>curl</code> 往外部地址发数据，就该警惕了，有可能会涉及到信息安全问题。这一点的详细讨论放在后边的文章中。</p><h2 id="手工编写一个-skill"><a href="#手工编写一个-skill" class="headerlink" title="手工编写一个 skill"></a>手工编写一个 skill</h2><p>除了安装现成的 skill，也许有时你想要一个高度定制化的版本，比如想把公司内的一些指导经验做成 skill。</p><p>让我们来创建一个实际的 skill 来演示这个过程。</p><p>开始之前，推荐先安装 <code>skill-creator</code> ，它可以指导 Claude code 生成 skill，可以理解成是一种”skill 生成器”。</p><h3 id="用自然语言描述你想要的技能"><a href="#用自然语言描述你想要的技能" class="headerlink" title="用自然语言描述你想要的技能"></a>用自然语言描述你想要的技能</h3><p>在 Claude 会话中输入：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">请帮我创建一个 SKILL，</span><br><span class="line">技能名称：pytest-gen</span><br><span class="line">功能：给定一个 Python 函数，生成对应的 pytest 单元测试。</span><br><span class="line">测试应该覆盖：正常输入、边界条件（空列表、None、零值）、异常处理。</span><br><span class="line">测试文件命名为 test_&lt;原文件名&gt;.py，放在 tests/ 目录下。</span><br></pre></td></tr></table></figure><p>注意这里的描述方式，我不仅说了 ”生成测试”，还具体说了覆盖哪些场景、文件怎么命名、放在哪里。描述越具体，生成出来的 skill 质量越高。</p><h3 id="Claude-生成-SKILL-md"><a href="#Claude-生成-SKILL-md" class="headerlink" title="Claude 生成 SKILL.md"></a>Claude 生成 SKILL.md</h3><p>Claude 会分析你的描述，生成完整的 <code>SKILL.md</code> 文件并保存到 <code>.claude/skills/pytest-gen/</code> 目录下。你可以要求它修改，比如 ”增加 mock 数据库的示例”，直到满意为止。</p><p>虽然 Claude 能帮你生成 SKILL.md，但生成的质量取决于你描述得有多精确，垃圾进，垃圾出。花时间想清楚你要什么，比生成后反复修改要高效得多。</p><h2 id="使用-Skills-需要注意什么"><a href="#使用-Skills-需要注意什么" class="headerlink" title="使用 Skills 需要注意什么"></a>使用 Skills 需要注意什么</h2><p>优先使用已有的公共 skill，不要重复造轮子。</p><p>定制优于从头写，如果公共技能不完全符合需求，复制到本地后让 Claude 修改它。</p><p>注意安全，安装第三方技能前，快速查看其 <code>SKILL.md</code> 内容，确认它不会做你不想让它做的事。</p><p>另外，可以用 <code>/skills</code> 命令随时查看当前激活的 Skills 清单，确认你想要的技能确实已经加载。</p><p>我自己也整理维护了一套 skills 包，把一些社区热门的 skills 整合在一起，做一些本土化定制，也方便一键安装。如果你感兴趣也可以去看看：<a href="https://github.com/P2Tree/ai-agent">https://github.com/P2Tree/ai-agent</a></p><h2 id="写在最后"><a href="#写在最后" class="headerlink" title="写在最后"></a>写在最后</h2><p>其实说实话，Claude Code 本身的能力已经很强了，它在更新过程中，也在不断融合一些常用的 skill 到系统中，所以在我看来，Claude Code 自己就能做到 80 分，安装恰当的 skill 和编写合适的 CLAUDE.md 后，可以做到 95 分，这是一个锦上添花的事情。</p><p>接下来，我计划介绍下 Claude Code 这种全托管式 AI Coding，和 Chat 形式的 “传统” vibe coding 相比，最大的优势来源自哪里。当你想让它帮你查 GitHub 上的 PR、查数据库里的数据、或者写个邮件并发出去时，它是怎么做的。</p><p>下一篇，我们看看怎么用 MCP 给大厨接通连接外部世界的电话线。</p><hr><div class="note info flat"><p>封面图片来自豆包 AI。</p><p>本文同步发布在知乎账号下：<a href="https://zhuanlan.zhihu.com/p/2041260921557873416">Claude Code 实践指南（三）：组织 Skills 系统</a></p></div>]]></content>
    
    
      
      
    <summary type="html">&lt;link rel=&quot;stylesheet&quot; class=&quot;aplayer-secondary-style-marker&quot; href=&quot;/assets/css/APlayer.min.css&quot;&gt;&lt;script src=&quot;/assets/js/APlayer.min.js&quot; cla</summary>
      
    
    
    
    <category term="软件工具" scheme="https://p2tree.top/categories/%E8%BD%AF%E4%BB%B6%E5%B7%A5%E5%85%B7/"/>
    
    
    <category term="VibeCoding" scheme="https://p2tree.top/tags/VibeCoding/"/>
    
    <category term="AI" scheme="https://p2tree.top/tags/AI/"/>
    
  </entry>
  
  <entry>
    <title>Claude-Code-实践指南（二）：编写操作规范</title>
    <link href="https://p2tree.top/posts/7abd88fb.html"/>
    <id>https://p2tree.top/posts/7abd88fb.html</id>
    <published>2026-05-15T23:06:27.000Z</published>
    <updated>2026-05-15T15:15:17.000Z</updated>
    
    <content type="html"><![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/assets/css/APlayer.min.css"><script src="/assets/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><ol class="series-items"><li><a href="/posts/4aab6423.html" title="Claude Code 实践指南（一）：开始第一次对话">Claude Code 实践指南（一）：开始第一次对话</a></li><li><a href="/posts/7abd88fb.html" title="Claude-Code-实践指南（二）：编写操作规范">Claude-Code-实践指南（二）：编写操作规范</a></li><li><a href="/posts/37a32ba9.html" title="Claude Code 实践指南（三）：组织 Skills 系统">Claude Code 实践指南（三）：组织 Skills 系统</a></li></ol><p>当把 Claude Code 用到一个新项目时，期待满满地让它帮忙写代码，结果发现我们可能反复在和它强调一些无聊的问题，比如写代码用什么规范，和后端怎么通信，写完代码要加测试用例。强调完，它当时记住了，但很快又会再犯。上下文一压缩，很多要求都会忘记。</p><p>一个下午，感觉用 Claude Code 写代码，还不如自己动手写。</p><p>其实，是因为你少做了一件事：把规矩写下来，写进 CLAUDE.md。</p><h2 id="每天都要重新培训的米其林大厨"><a href="#每天都要重新培训的米其林大厨" class="headerlink" title="每天都要重新培训的米其林大厨"></a>每天都要重新培训的米其林大厨</h2><p>想象你后厨的米其林大厨有个怪毛病：每天早上醒来，他对厨房的一切都丧失记忆。不知道调料架在哪，不知道你们家不吃辣，甚至连刀在哪都得你重新指一遍。</p><p>幸运的是，你可以准备一份”厨房操作规范”。每天大厨到岗，先读完规范再干活。规范里写了口味偏好、食材禁忌、操作规矩等。写一次，天天生效。</p><p>CLAUDE.md，就是这份规范。它是一个普通的 Markdown 文件，你可以像写便签一样用中文（或英文）写下你想让 Claude 记住的所有规则。</p><p>虽然 CLAUDE.md 写一次就永久生效，但写得不准反而会误导 Claude，所以写得精比写得多更重要。</p><h2 id="为什么需要-CLAUDE-md"><a href="#为什么需要-CLAUDE-md" class="headerlink" title="为什么需要 CLAUDE.md"></a>为什么需要 CLAUDE.md</h2><p>Claude Code 的每次会话都是无状态的。你敲下 <code>claude</code> 回车的那一刻，是一个全新的”空白大脑”在等你。你之前教过它什么？统统不记得了。</p><p>CLAUDE.md 会在每次会话启动时自动加载到<strong>系统提示词（System Prompt）</strong> 中。这就好比大厨每天到岗第一件事就是读你的厨房规范，不读不开火。</p><p>这里有一个很容易被忽视的关键机制：即使在会话中使用了 <code>/compact</code> 压缩上下文，CLAUDE.md 也会从磁盘重新读取并重新注入。也就是说，它里面的规则永远不会因为上下文压缩而丢失。</p><p>对比一下就明白了：你在对话中口头告诉 Claude “我们项目用 2 空格缩进”，一旦上下文被压缩，这条规则可能就没了。但如果你把它写进 CLAUDE.md，压缩之后它会自动回来。</p><p>口头指令是沙上建塔，CLAUDE.md 才是刻石为碑。</p><p>不过，CLAUDE.md 只能记住静态规则，你在上一轮会话中讨论过的设计决策、踩过的坑，它会忘记。</p><p>社区有一个叫 claude-mem 的工具，用 Hooks 和向量数据库把跨会话的对话记忆持久化下来，算是从另一个角度补了这个缺口。第六篇会简单介绍。</p><h2 id="Claude-怎么找到你的厨房规范：层级发现系统"><a href="#Claude-怎么找到你的厨房规范：层级发现系统" class="headerlink" title="Claude 怎么找到你的厨房规范：层级发现系统"></a>Claude 怎么找到你的厨房规范：层级发现系统</h2><p>Claude Code 会从当前目录向上查找，并<strong>合并加载</strong> 多个位置的 CLAUDE.md。这就好比你家厨房有一份总规范，每个灶台还能贴自己的小纸条，大厨会把所有纸条都看一遍，然后综合执行。</p><table><thead><tr><th>层级</th><th>文件路径</th><th>是否提交 Git</th><th>用途</th></tr></thead><tbody><tr><td>全局</td><td>~&#x2F;.claude&#x2F;CLAUDE.md</td><td>不提交</td><td>个人偏好约定</td></tr><tr><td>项目根目录</td><td>.&#x2F;CLAUDE.md</td><td>提交</td><td>团队共享的项目核心规则</td></tr><tr><td>本地覆盖</td><td>.&#x2F;CLAUDE.local.md</td><td>自动 ignore</td><td>个人的覆盖或调试配置</td></tr><tr><td>父目录</td><td>从 cwd 向上查找到 &#x2F;</td><td>不推荐</td><td>Monorepo 场景下根目录与子包同时生效</td></tr><tr><td>子目录</td><td>.&#x2F;subdir&#x2F;CLAUDE.md</td><td>按需</td><td>当 Claude 访问该子目录文件时才加载</td></tr></tbody></table><p>所谓 Monorepo，就是单个代码仓库包含多个独立项目，比如一个仓库里有 <code>frontend/</code>、<code>backend/</code>、<code>shared/</code>。在这种结构下，向上查找机制可以让根目录的规则对所有子包生效，而每个子包又可以有自己的补充规则。</p><p>还有一个细节值得注意：子目录的 CLAUDE.md 是<strong>按需加载</strong> 的。只有当 Claude 实际读取某个子目录下的文件时，才会加载该目录中的 CLAUDE.md。这个设计挺聪明，省 Token。</p><h2 id="用-init-快速生成第一版规范"><a href="#用-init-快速生成第一版规范" class="headerlink" title="用 &#x2F;init 快速生成第一版规范"></a>用 &#x2F;init 快速生成第一版规范</h2><p>如果你面对的是一个已有项目，不想从零写起，可以在项目根目录运行 <code>/init</code>（进入 Claude 会话后输入即可）。Claude 会自动扫描你的代码库，然后生成一份初始 CLAUDE.md，通常包含：</p><ul><li>项目概述（从 README 或 package.json 推测）</li><li>常用构建和测试命令</li><li>明显的代码规范线索</li></ul><p>不过，自动生成的东西难免有不准确的地方。生成之后一定要手动编辑，删掉猜错的，补上遗漏的。<code>/init</code> 给你的是毛坯房，精装修得自己来。</p><p>如果已经有一份 CLAUDE.md 想重新生成，可以用 <code>/init --force</code> 覆盖现有版本。</p><h2 id="怎样写出一份真正有用的-CLAUDE-md"><a href="#怎样写出一份真正有用的-CLAUDE-md" class="headerlink" title="怎样写出一份真正有用的 CLAUDE.md"></a>怎样写出一份真正有用的 CLAUDE.md</h2><p>首先是硬约束：<strong>保持 200 行以内</strong> 。太长了 Claude 会忽略尾部内容，这不是偷懒，是注意力机制的问题，就像你给大厨贴了一百条纸条，他大概率只记得前二十条。</p><p>其次是内容选择：只放<strong>对工作流有持续影响</strong> 的信息。API 文档、教程之类的参考材料不要放进去，那是在浪费每一条都会被读到的宝贵行数。</p><p>最后是结构：用清晰的 Markdown 标题分层。</p><p>一个 Python 项目的 CLAUDE.md 大概长这样：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"># Project: FastAPI Backend</span><br><span class="line"></span><br><span class="line">## Build &amp; Test Commands</span><br><span class="line">- Install: `pip install -r requirements.txt`</span><br><span class="line">- Run tests: `pytest tests/ -v`</span><br><span class="line">- Run dev server: `uvicorn main:app --reload`</span><br><span class="line"></span><br><span class="line">## Code Style</span><br><span class="line">- Follow PEP 8 with 88 character line length (Black defaults)</span><br><span class="line">- Use type hints for all function parameters and returns</span><br><span class="line">- Use async/await for all database calls</span><br><span class="line"></span><br><span class="line">## Architectural Constraints</span><br><span class="line">- Never import from `legacy/` module – it&#x27;s deprecated</span><br><span class="line">- All new endpoints must be added to `routers/` directory</span><br><span class="line">- Database migrations must be generated via Alembic</span><br><span class="line"></span><br><span class="line">## Common Pitfalls</span><br><span class="line">- Circular imports happen if you import models in `__init__.py`</span><br><span class="line">- Remember to call `session.close()` after database operations</span><br></pre></td></tr></table></figure><p>虽然模板看着整齐，但实际写的时候不必追求面面俱到，后边使用 Claude 中最常出错的环节，才是最值得写进备注的。如果你发现自己反复纠正 Claude 同一个问题，那这个问题就该进 CLAUDE.md 了。</p><h2 id="菜谱太长怎么办：-claude-rules-目录拆分"><a href="#菜谱太长怎么办：-claude-rules-目录拆分" class="headerlink" title="菜谱太长怎么办：.claude&#x2F;rules&#x2F; 目录拆分"></a>菜谱太长怎么办：.claude&#x2F;rules&#x2F; 目录拆分</h2><p>当项目规模增长，CLAUDE.md 变得臃肿，200 行装不下怎么办？可以把规则拆分到 <code>.claude/rules/</code> 目录下。</p><p>这个目录下可以放多个 Markdown 文件，分为两种：</p><p><strong>普通规则文件</strong> （没有 <code>path:</code> 字段）：会话启动时加载，效果等同于写在 CLAUDE.md 里。</p><p><strong>路径限定规则文件</strong> （包含 <code>path:</code> YAML 字段）：只有当 Claude 读取匹配路径的文件时才加载。这是省 Token 的大杀器，前端规则只在读前端代码时才生效，后端规则只在读后端代码时才生效。</p><p>比如你可以在 <code>.claude/rules/frontend.md</code> 里这样写：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">path: src/frontend/**</span><br><span class="line"></span><br><span class="line">---</span><br><span class="line">All frontend code must use React functional components with hooks.</span><br><span class="line">Never use class components.</span><br></pre></td></tr></table></figure><p><code>path:</code> 字段和正文之间用 <code>---</code> 分隔。上面那行声明了这条规则只在 <code>src/frontend/</code> 目录下生效，下面的正文就是规则本身。简洁，克制，该出场时再出场。</p><h2 id="CLAUDE-md-里的隐藏语法"><a href="#CLAUDE-md-里的隐藏语法" class="headerlink" title="CLAUDE.md 里的隐藏语法"></a>CLAUDE.md 里的隐藏语法</h2><p>到现在为止，我们一直把 CLAUDE.md 当成纯文本在用，写什么就注入什么。但其实它有几条不算显眼的专属语法，知道了能省不少事。</p><h3 id="用-导入其他文件"><a href="#用-导入其他文件" class="headerlink" title="用 @ 导入其他文件"></a>用 @ 导入其他文件</h3><p>CLAUDE.md 里可以写 <code>@path/to/file</code>，Claude Code 在加载时会把这个文件的内容展开并注入。就像你在菜谱里夹了一张纸条：”详细步骤见附录 A”，大厨会自动翻到附录 A 去看。</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"># 项目规范</span><br><span class="line"></span><br><span class="line">@README.md</span><br><span class="line"></span><br><span class="line">## 额外说明</span><br><span class="line">@docs/git-instructions.md</span><br></pre></td></tr></table></figure><p>几个要点：路径可以是相对路径（相对于写 <code>@</code> 的那个文件所在目录），也可以是绝对路径；导入可以嵌套，但最多 5 层，防止无限递归。</p><p>一个容易踩的坑：<code>@</code> 导入并不能省 Token。被导入的文件内容照样会完整注入上下文，所以它解决的是组织问题，不是成本问题。你把 500 行的 API 文档 <code>@</code> 进来，大厨照样得逐字读完。</p><h3 id="HTML-注释是给人看的，不是给-Claude-看的"><a href="#HTML-注释是给人看的，不是给-Claude-看的" class="headerlink" title="HTML 注释是给人看的，不是给 Claude 看的"></a>HTML 注释是给人看的，不是给 Claude 看的</h3><p>如果你在 CLAUDE.md 里写了 HTML 注释，比如 <code>&lt;!-- 这段是给人看的维护笔记 --&gt;</code>，Claude Code 在注入上下文之前会把这些注释剥掉。也就是说，注释里的内容 Claude 永远看不到，也不消耗 Token。</p><p>所以，你可以在 CLAUDE.md 里留下为什么这么写的备忘给自己看，而不浪费 Claude 的注意力。不过要注意，注释写在代码块里面是不会被剥掉的，因为代码块里的内容是字面量。</p><h3 id="claude-rules-的路径限定字段"><a href="#claude-rules-的路径限定字段" class="headerlink" title=".claude&#x2F;rules&#x2F; 的路径限定字段"></a>.claude&#x2F;rules&#x2F; 的路径限定字段</h3><p>前面已经讲了 <code>path:</code> 字段的基本用法，再补充一个细节：<code>path:</code> 支持多个 glob 模式，用 YAML 列表写法。</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">---</span><br><span class="line">paths:</span><br><span class="line">  - &quot;src/api/**/*.ts&quot;</span><br><span class="line">  - &quot;lib/**/*.ts&quot;</span><br><span class="line">---</span><br><span class="line"></span><br><span class="line"># API 开发规范</span><br><span class="line">- 所有 API 端点必须包含输入校验</span><br></pre></td></tr></table></figure><p>当 Claude 读取匹配这些路径的文件时，这条规则才会被加载。不匹配就不加载，一分 Token 都不多花。</p><h2 id="CLAUDE-md-的加载顺序"><a href="#CLAUDE-md-的加载顺序" class="headerlink" title="CLAUDE.md 的加载顺序"></a>CLAUDE.md 的加载顺序</h2><p>所有这些语法背后，有一个加载顺序值得了解。Claude Code 从当前目录向上遍历，把沿途找到的 CLAUDE.md 全部拼接注入，根目录的在前、工作目录的在后。所以离你最近的那份 CLAUDE.md 是最后被读到的，权重也最高。</p><p>同一目录下，<code>CLAUDE.local.md</code> 排在 <code>CLAUDE.md</code> 后面，所以你个人的覆盖配置总是比团队共享的优先。子目录的 CLAUDE.md 是按需加载的，只有 Claude 实际访问了那个目录的文件，才会读取。</p><p>还有一个关键细节：使用 <code>/compact</code> 压缩上下文之后，只有项目根目录的 CLAUDE.md 会从磁盘重新读取并重新注入，子目录的不会。所以那些最不能丢的规则，一定要放在根目录的 CLAUDE.md 里。</p><h2 id="CLAUDE-md-和-AGENTS-md-是什么关系"><a href="#CLAUDE-md-和-AGENTS-md-是什么关系" class="headerlink" title="CLAUDE.md 和 AGENTS.md 是什么关系"></a>CLAUDE.md 和 AGENTS.md 是什么关系</h2><p>如果你在 GitHub 上逛过开源项目，可能见过一个叫 AGENTS.md 的文件。它和 CLAUDE.md 长得像、用途像，但不是一个东西。</p><p>AGENTS.md 是 GitHub 定义的规范，给 GitHub Copilot 的 Agent 用的；CLAUDE.md 是 Anthropic 定义的规范，给 Claude Code 用的。两家的 Agent 都需要一份”操作规范”，但各自只认自家的文件名。</p><p>它们都是放在仓库里的 Markdown 文件，都是给 AI Agent 注入项目上下文和规则的。</p><p><a href="http://agents.md/">AGENTS.md</a> 正在演变成通用的 AI Agent 配置文件标准，不只是 Copilot，Cursor、Codex 等 AI Agent 也开始支持，它也成为 Linux 基金会开始支持的标准。</p><p>不过，如果你只用 Claude Code，使用 <a href="http://claude.md/">CLAUDE.md</a> 更值得推荐，它有更强大的专属内置指令。</p><h3 id="两个都有的仓库怎么办"><a href="#两个都有的仓库怎么办" class="headerlink" title="两个都有的仓库怎么办"></a>两个都有的仓库怎么办</h3><p>如果你的团队既用 Claude Code 又用 Copilot，两份规范维护起来会头疼吗？倒也不必。推荐的做法是用 CLAUDE.md 导入 AGENTS.md：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">@AGENTS.md</span><br><span class="line"></span><br><span class="line">## Claude Code 专属</span><br><span class="line">- 修改 src/billing/ 下的文件时使用 plan mode</span><br></pre></td></tr></table></figure><p>这样，两份规范共享同一份基础内容（写在 AGENTS.md 里），Claude Code 特有的规则追加在后面。Copilot 只读 AGENTS.md，Claude Code 先读 AGENTS.md 再读追加部分，互不干扰，也不用维护两份重复的规范。</p><p>如果你只用 Claude Code，直接写 CLAUDE.md 就好，不必创建 AGENTS.md。反过来，只用 Copilot 的话写 AGENTS.md 即可。只有两边都用的时候，才需要考虑这个桥接方案。</p><p>另外，你也可能看到过 CONTEXT.md，<a href="http://project-context.md/">project-context.md</a> 一类的工程文件，它们本质上具有相同的功能，只是用于不同的 agent。</p><p>希望将来这些大佬们能把这些协议性的东西规范一下，避免大家理解上的差异。</p><p>还有一点：运行 <code>/init</code> 时，如果仓库里已经有 AGENTS.md（或者 <code>.cursorrules</code>、<code>.windsurfrules</code>），Claude Code 会读取这些文件，把相关内容整合进生成的 CLAUDE.md。所以即便你忘了手动桥接，<code>/init</code> 也能帮你兜个底。</p><h2 id="几个实用命令"><a href="#几个实用命令" class="headerlink" title="几个实用命令"></a>几个实用命令</h2><ul><li><code>/context</code> —— 查看当前上下文中已加载的全部 CLAUDE.md 内容。如果怀疑某条规则没生效，先看这个。</li><li><code>/init --force</code> —— 重新生成 CLAUDE.md，覆盖现有版本。</li><li><code>/cost</code> —— 查看加载 CLAUDE.md 消耗了多少 Token，通常很少，大约 200-500，不用太担心。</li></ul><h2 id="常见问题"><a href="#常见问题" class="headerlink" title="常见问题"></a>常见问题</h2><h3 id="CLAUDE-md-会泄露我的秘密吗？"><a href="#CLAUDE-md-会泄露我的秘密吗？" class="headerlink" title="CLAUDE.md 会泄露我的秘密吗？"></a>CLAUDE.md 会泄露我的秘密吗？</h3><p>如果你把项目级 CLAUDE.md 提交到公开仓库，那里面写的所有内容都会被看到。所以，不要在项目级 CLAUDE.md 里放 API Key、内部域名或敏感路径。这类信息应该放在 <code>CLAUDE.local.md</code> 里，并加入 <code>.gitignore</code> 避免泄露。</p><h3 id="Monorepo-中如何配置？"><a href="#Monorepo-中如何配置？" class="headerlink" title="Monorepo 中如何配置？"></a>Monorepo 中如何配置？</h3><p>在根目录放一份通用规则，在每个子包中放自己特定的规则。Claude Code 的向上查找机制会让根目录的规则对所有子包生效，而子包的规则又可以对根目录的规则做补充或覆盖。</p><h3 id="CLAUDE-md-里可以用中文吗？"><a href="#CLAUDE-md-里可以用中文吗？" class="headerlink" title="CLAUDE.md 里可以用中文吗？"></a>CLAUDE.md 里可以用中文吗？</h3><p>可以，Claude 完全理解中文。不过推荐命令和路径用英文，避免编码问题。</p><h3 id="Windows-路径有问题吗？"><a href="#Windows-路径有问题吗？" class="headerlink" title="Windows 路径有问题吗？"></a>Windows 路径有问题吗？</h3><p>Claude Code 会自动转换路径分隔符，这方面不用操心。</p><h2 id="写在最后"><a href="#写在最后" class="headerlink" title="写在最后"></a>写在最后</h2><p>CLAUDE.md 这东西，说到底就是一个 Markdown 文件，没什么技术门槛。但正是这种看似简单的机制，解决了一个根本性的问题：如何让一个无状态的 AI 助手保持对项目的持久记忆。</p><p>不过，记忆只是第一步。如果你想让大厨不只记住你家不吃香菜，还能按标准流程做出一整套招牌菜，比如”按公司规范做安全审查”或”自动生成周报”，光靠备注可不够。下一篇文章，我们看看怎么用 Skills 给 Claude 教一套完整的烹饪套路。</p><hr><div class="note info flat"><p>封面图片来自豆包 AI。</p><p>本文同步发布在知乎账号下：<a href="https://zhuanlan.zhihu.com/p/2038354462239110064">Claude-Code-实践指南（二）：编写操作规范</a></p></div>]]></content>
    
    
      
      
    <summary type="html">&lt;link rel=&quot;stylesheet&quot; class=&quot;aplayer-secondary-style-marker&quot; href=&quot;/assets/css/APlayer.min.css&quot;&gt;&lt;script src=&quot;/assets/js/APlayer.min.js&quot; cla</summary>
      
    
    
    
    <category term="软件工具" scheme="https://p2tree.top/categories/%E8%BD%AF%E4%BB%B6%E5%B7%A5%E5%85%B7/"/>
    
    
    <category term="VibeCoding" scheme="https://p2tree.top/tags/VibeCoding/"/>
    
    <category term="AI" scheme="https://p2tree.top/tags/AI/"/>
    
  </entry>
  
  <entry>
    <title>Claude Code 实践指南（一）：开始第一次对话</title>
    <link href="https://p2tree.top/posts/4aab6423.html"/>
    <id>https://p2tree.top/posts/4aab6423.html</id>
    <published>2026-05-07T20:24:27.000Z</published>
    <updated>2026-05-07T12:35:58.000Z</updated>
    
    <content type="html"><![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/assets/css/APlayer.min.css"><script src="/assets/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><ol class="series-items"><li><a href="/posts/4aab6423.html" title="Claude Code 实践指南（一）：开始第一次对话">Claude Code 实践指南（一）：开始第一次对话</a></li><li><a href="/posts/7abd88fb.html" title="Claude-Code-实践指南（二）：编写操作规范">Claude-Code-实践指南（二）：编写操作规范</a></li><li><a href="/posts/37a32ba9.html" title="Claude Code 实践指南（三）：组织 Skills 系统">Claude Code 实践指南（三）：组织 Skills 系统</a></li></ol><p>年初，我也搭上了 AI 时代的公交车。刚开始试用时，我的体验是： ”感觉 Claude Code 写出的代码就是一坨啊？AI agent 真的有用吗？”。</p><p>后来，又来一次在家做饭的时候突然想明白了——</p><p>一个人做饭，从洗菜、切菜、调味到掌勺，全靠自己。如果你家里请了一位米其林大厨，你只需要说”给我做个宫保鸡丁”，大厨自己就能做好了。Claude Code 就是这样一个大厨。</p><p>虽然它切菜很快，但它并不知道你的口味偏好，你家的厨房设施，这些还得你说了算。</p><p>嗯，我挺满意这个比喻。</p><p>这是我计划编写的 Claude Code 实践指南的第一篇文章，我想先简单介绍一些概念和用法。这种新的工作模式是否能提高工作效率暂且不提，了解他是什么，能干什么，依然很重要。</p><h2 id="Claude、Claude-Code-和其他工具到底有什么区别"><a href="#Claude、Claude-Code-和其他工具到底有什么区别" class="headerlink" title="Claude、Claude Code 和其他工具到底有什么区别"></a>Claude、Claude Code 和其他工具到底有什么区别</h2><p>先说 Claude。Claude 是 Anthropic 公司的大语言模型家族，包括 Claude Sonnet、Claude Opus 等等，它是一个”能听懂话的大厨”。</p><p>再说 Claude Code。Claude Code 是运行在终端里的 AI 编程助手，是一个 CLI（Command Line Interface，命令行界面）工具。它不光能听懂话，还能读取文件、编辑代码、执行命令、完成多步骤开发任务。相当于给大厨配齐了刀具、灶台和食材仓库的后厨系统，不光能听懂，还能动手。</p><p>打个比方：Claude 是那个你正在面试的大厨，你说”酸辣汤怎么做”，它能告诉你配方和步骤；而 Claude Code 是那个已经站在灶台前的大厨，你说”做一碗酸辣汤”，它直接走进厨房，把汤端出来。</p><p>除了 Claude Code，目前市面上 AI 编程工具不少，各有各的风格：</p><table><thead><tr><th>工具</th><th>模型</th><th>所属公司</th><th>关键特点</th><th>适合场景</th></tr></thead><tbody><tr><td>Claude Code</td><td>Claude 系列</td><td>Anthropic</td><td>代码专用优化，Agent 自主性高</td><td>复杂代码库重构、多文件修改</td></tr><tr><td>Copilot</td><td>GPT &#x2F; Claude</td><td>Microsoft</td><td>补全为主，对话辅助</td><td>日常编码补全</td></tr><tr><td>Cursor</td><td>多类模型</td><td>Cursor</td><td>基于 VS Code，内置 AI 的 IDE</td><td>喜欢独立 IDE 的用户</td></tr><tr><td>Zed</td><td>多类模型</td><td>Zed</td><td>极致性能，自研 GPUI，AI 原生集成</td><td>喜欢 IDE 但接受不了 VS Code 的速度的用户</td></tr><tr><td>Codex</td><td>o3 &#x2F; codex-1</td><td>OpenAI</td><td>开源终端编程代理，Agent 自主性高</td><td>自动化脚本、代码生成与执行</td></tr><tr><td>Gemini</td><td>Gemini 系列</td><td>Google</td><td>上下文窗口大，多模态能力强</td><td>大型项目、长上下文分析</td></tr><tr><td>OpenCode</td><td>多类模型</td><td>Anomaly</td><td>代码搜索和理解能力强</td><td>代码检索、Bug 定位</td></tr><tr><td>Windsurf</td><td>多类模型</td><td>Codeium</td><td>注重协作体验，适合团队</td><td>团队协作、初学者友好</td></tr></tbody></table><p>简单介绍一下这些新面孔：</p><p><strong>Codex</strong>  是 OpenAI 推出的开源终端编程代理，基于 o3 和 codex-1 系列模型，定位和 Claude Code 类似，也是一个能直接在终端里读写代码、执行命令的 Agent。</p><p><strong>Gemini</strong>  是 Google 的多模态大模型，上下文窗口可以做到 1M tokens 以上，理论上可以一次性读完一个中型代码库再分析，适合长上下文分析场景。</p><p><strong>OpenCode</strong>  来自 Anomaly，是一个开源项目，在行业内非常火，可以接入各大模型商的大模型。</p><p><strong>Windsurf</strong>  背后是 Codeium，就是那个早年免费替代 Copilot 的 Codeium。Windsurf 强调协作体验，定位更偏向团队使用和初学者。</p><h2 id="系统要求与安装"><a href="#系统要求与安装" class="headerlink" title="系统要求与安装"></a>系统要求与安装</h2><p>安装 Claude Code 的前提如下：</p><p><strong>操作系统</strong> ：macOS、Linux、Windows WSL2 均可。</p><p><strong>前置依赖</strong> ：Node.js 18+ 和 npm。</p><p><strong>主安装命令</strong> 很简单，一行搞定：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"># macOS / Linux / WSL</span><br><span class="line">curl -fsSL https://claude.ai/install.sh | bash</span><br></pre></td></tr></table></figure><p>Windows 用户用 PowerShell：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">irm https://claude.ai/install.ps1 | iex</span><br></pre></td></tr></table></figure><p>如果你更习惯 Homebrew，也可以：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">brew install --cask claude-code</span><br></pre></td></tr></table></figure><p>安装完成后，验证一下：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">claude --version</span><br></pre></td></tr></table></figure><p>如果能看到版本号输出，说明安装成功。如果提示”command not found”，大概率是 PATH 没配好，试试重新打开终端窗口。</p><h2 id="认证与计费"><a href="#认证与计费" class="headerlink" title="认证与计费"></a>认证与计费</h2><p>大厨请来了，但干活得开工资。Claude Code 提供多种付费方式。</p><h3 id="方式一：Subscription-登录"><a href="#方式一：Subscription-登录" class="headerlink" title="方式一：Subscription 登录"></a>方式一：Subscription 登录</h3><p>如果你已经有 Claude Pro、Max 或 Team 订阅，这是最省心的方式。直接在终端运行 <code>claude</code>，会自动打开浏览器要求登录 Anthropic 账号，登录后自动绑定订阅权益，不需要手动管理任何 API Key。</p><p>优点是简单，不需要记 Key，费用包含在月费中。缺点是重度使用可能触发速率限制。</p><h3 id="方式二：API-Key"><a href="#方式二：API-Key" class="headerlink" title="方式二：API Key"></a>方式二：API Key</h3><p>如果你没有订阅，或者用量波动大，可以用 API Key 按量付费。步骤如下：</p><ol><li>前往 <a href="http://console.anthropic.com/">http://console.anthropic.com</a> 注册并登录（目前需要一个境外手机号，可以找接码平台代劳）</li><li>进入 <strong>API Keys</strong>  页面，点击 <strong>Create Key</strong> </li><li>将 Key 设置为环境变量：</li></ol><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">export ANTHROPIC_API_KEY=&quot;sk-ant-...&quot;</span><br></pre></td></tr></table></figure><p>测试一下：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">claude &quot;用 Python 写一个计算斐波那契数列的函数&quot;</span><br></pre></td></tr></table></figure><p>如果 Claude 正常返回了一段代码，说明认证成功。</p><h3 id="相关问题"><a href="#相关问题" class="headerlink" title="相关问题"></a>相关问题</h3><p><strong>费用说明</strong> ：API 方式按 Token 计费。Token 是模型读取文本的最小单位，约等于 0.75 个英文单词或半个中文字（不同模型有差别，感性认识下）。输入和输出分别计价，具体费率以 Anthropic 官网为准。新注册用户通常有试用额度。</p><p><strong>什么时候用订阅，什么时候用 API？</strong> </p><p>如果你是个人用户，主要用于对话、写作或学习，编码的工作比较轻量级，选择订阅制更省心，容易控制预算。当然，前提是你会持续用下去。</p><p>如果你的工作涉及大量代码生成、系统自动化、数据处理，或者你将来可能希望更精确地控制成本，使用 API 更合适。</p><p>如果你认为 Cluade 太贵，只是想简单用用，其实借用 Claude Code 但不为 Claude 付费也是可以的，国产的模型会便宜一些。</p><p><strong>注意封锁</strong> </p><p>由于 Anthropic 公司的老板对我们比较抵触，所以在大陆地区订阅 Claude 可能（极大可能）会被封号。万一被封了，想开点，因为大家都一样。</p><p>如果被封锁，也许可以找付款平台申诉退款，当然也得做好 money 打水漂的心理准备。</p><p>封锁的机理其实一直在层层加码，所以我这里不展开讲如何规避了。</p><h2 id="几个需要先知道的概念"><a href="#几个需要先知道的概念" class="headerlink" title="几个需要先知道的概念"></a>几个需要先知道的概念</h2><p>在正式开始 cowork 之前，有几个概念值得先理清楚。</p><p><strong>词元（Token）</strong> ：每次调用的计费单位。在 Claude Code 会话中，你可以用 <code>/cost</code> 命令查看当前会话消耗了多少 Token、花了多少钱。</p><p><strong>上下文（Context）</strong> ：Claude Code 默认支持 200K tokens 的上下文窗口。Max 及以上订阅可以直接使用 1M 上下文窗口（仅限特定模型），Pro 订阅需要额外付费才能用。这意味着你可以在一次对话中给它相当多的代码和背景信息，它都能记住。</p><p><strong>会话（Session）</strong> ：从 <code>claude</code> 启动到退出的一次连续对话。在同一个会话中，Claude 会保持对之前交流的记忆（未压缩上下文情况下）。退出后不用担心丢失，下次启动时可以用 <code>claude --continue</code> 恢复最近的会话，或者用 <code>claude --resume</code> 从历史会话中选择一个继续。</p><h3 id="什么是-Agent"><a href="#什么是-Agent" class="headerlink" title="什么是 Agent"></a>什么是 Agent</h3><p>在 AI 圈子里，”Agent”这个词被用得太多了，每个人说的还不太一样，所以我们先把这个概念理清楚。</p><p>简单说：<strong>AI Agent &#x3D; 会用工具的 AI。</strong> </p><p>普通的 AI 对话，比如你在网页端用 Claude 或 ChatGPT，你说”帮我写个函数”，它给你一段文字代码，仅此而已。你还得自己复制粘贴到编辑器里，自己跑测试，自己修报错。</p><p>而 Agent 模式的 AI，比如 Claude Code，不光能给你代码，还能帮你：</p><ul><li>读取你现有的代码文件</li><li>编辑文件、创建新文件</li><li>执行 shell 命令（运行测试、编译项目）</li><li>搜索代码库、操作 Git</li></ul><p>类比一下：普通 AI 就是个只能的对话助手，它可以告诉你详细的菜谱，但需要你自己照着做；Agent 模式的 AI 像是直接走进厨房，帮你把菜做出来。</p><p>Claude Code 就是一个 AI Agent。你说”帮我重构 auth 模块”，它会先分析现有代码结构，然后制定重构计划，接着逐个修改相关文件，最后运行测试验证。</p><p>整个过程中，它自己做决策，不需要你一步步指挥。当然，它做的决策不一定每次都对，所以最后你还是要验收。</p><h3 id="什么是-Subagent"><a href="#什么是-Subagent" class="headerlink" title="什么是 Subagent"></a>什么是 Subagent</h3><p>当任务太复杂时，Claude Code 还可以启动 Subagent（子代理）来并行处理多个独立子任务。</p><p>比如你让 Claude “同时处理这三个独立功能”，主 Agent 负责任务分解和协调，Subagent A 处理功能一，Subagent B 处理功能二，Subagent C 处理功能三，各自独立工作，完成后结果汇总给主 Agent。</p><p>其实 Subagent 是 Claude Code 的核心机制之一。很多时候你不需要主动调用，Claude 会自动根据任务类型启动合适的子代理，比如用 Explore 类型的子代理搜索代码，用 Plan 类型的子代理设计方案。你在不知不觉中就已经用到了。</p><h3 id="Tool-Use-是什么"><a href="#Tool-Use-是什么" class="headerlink" title="Tool Use 是什么"></a>Tool Use 是什么</h3><p><strong>Tool Use（工具调用）</strong>  是 Agent 实现能力的技术基础。简单说，就是给 AI 模型装上”手”，让它可以调用外部接口来完成任务。</p><p>Claude Code 中的 Tool Use 包括：</p><ul><li><code>Read</code> &#x2F; <code>Write</code> &#x2F; <code>Edit</code>：文件系统操作，相当于大厨的刀和锅</li><li><code>Bash</code>：执行 shell 命令，相当于打开灶台的火</li><li><code>Grep</code> &#x2F; <code>Glob</code>：搜索代码内容和文件模式，相当于在食材仓库里找东西</li><li><code>WebFetch</code> &#x2F; <code>WebSearch</code>：获取网页内容和搜索互联网，相当于大厨去隔壁市场采购</li><li><code>Agent</code>：启动子代理，相当于叫一个帮厨来搭手</li></ul><p>这些工具让 Claude 从”能说会道”进化到”能说会做”。没有 Tool Use，大厨只能念菜谱给你听；有了 Tool Use，大厨才能真的走进厨房。</p><p>使用这些 Tool 依赖一个叫 MCP（Model Context Protocol，模型上下文协议）的东西，这个东西是 2024 年 Anthropic 开放的一个标准，现在不只有 Claude Code 在用，其他 AI Agent 也同样用。</p><h2 id="实战：和-Claude-Code-的第一次对话"><a href="#实战：和-Claude-Code-的第一次对话" class="headerlink" title="实战：和 Claude Code 的第一次对话"></a>实战：和 Claude Code 的第一次对话</h2><p>概念讲完了，让我们动手试一下。</p><p>找任意一个项目目录，哪怕是空的也行，然后启动 Claude Code：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">cd my-project</span><br><span class="line">claude</span><br></pre></td></tr></table></figure><p>进入交互界面后，输入：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">分析这个项目的目录结构，告诉我主要的模块有哪些</span><br></pre></td></tr></table></figure><p>Claude 会自动调用 <code>ls</code>、<code>cat</code> 等工具去读取你的项目文件，然后输出分析结果。如果你在一个 Node.js 项目中，它可能会告诉你 <code>src/</code> 是主代码目录、<code>tests/</code> 是测试目录、<code>package.json</code> 管理依赖等等。</p><p>这个过程中你不需要指定它用什么命令，它自己会判断需要读取哪些文件，就像你不需要告诉大厨用哪把刀切洋葱一样。</p><h3 id="常用命令速览"><a href="#常用命令速览" class="headerlink" title="常用命令速览"></a>常用命令速览</h3><p>在 Claude Code 的交互界面中，斜杠命令是常用的操作入口：</p><ul><li><code>/help</code>：查看所有命令</li><li><code>/model</code>：切换 Claude 模型（比如从 Sonnet 切到 Opus）</li><li><code>/init</code>：在项目中初始化 CLAUDE.md 配置文件</li><li><code>/resume</code>：恢复之前的历史会话</li><li><code>/clear</code>：清空当前会话历史（硬重置，会丢失所有上下文）</li><li><code>/compact</code>：压缩上下文（保留关键信息但释放 Token，不会完全丢失记忆）</li><li><code>/cost</code>：显示当前会话的费用估算</li><li><code>/diff</code>：查看当前未提交的代码变更</li><li><code>/exit</code>：退出</li></ul><p>其中 <code>/compact</code> 是个值得记住的命令，当会话变长、Token 消耗开始让人肉疼的时候，用它压缩一下，能省不少钱。Claude Code 自己也会自动压缩上下文。</p><h3 id="大厨请进了厨房"><a href="#大厨请进了厨房" class="headerlink" title="大厨请进了厨房"></a>大厨请进了厨房</h3><p>大厨请进了厨房，也试了第一刀。</p><p>不过有一个问题：每次新会话，它都会忘记你家厨房的规矩，比如盐罐放哪、不吃香菜、大火爆炒别太干。下一篇文章，我们会给 Claude 写一份”厨房操作规范”，也就是 CLAUDE.md 配置文件。</p><hr><div class="note info flat"><p>封面图片来自豆包 AI，Prompt：构成主义、微观元素、自然景观、拼图艺术。</p><p>本文同步发布在知乎账号下：<a href="https://zhuanlan.zhihu.com/p/2032855948788749921">Claude Code 实践指南（一）：开始第一次对话</a></p></div>]]></content>
    
    
      
      
    <summary type="html">&lt;link rel=&quot;stylesheet&quot; class=&quot;aplayer-secondary-style-marker&quot; href=&quot;/assets/css/APlayer.min.css&quot;&gt;&lt;script src=&quot;/assets/js/APlayer.min.js&quot; cla</summary>
      
    
    
    
    <category term="软件工具" scheme="https://p2tree.top/categories/%E8%BD%AF%E4%BB%B6%E5%B7%A5%E5%85%B7/"/>
    
    
    <category term="VibeCoding" scheme="https://p2tree.top/tags/VibeCoding/"/>
    
    <category term="AI" scheme="https://p2tree.top/tags/AI/"/>
    
  </entry>
  
  <entry>
    <title>汇总几个终端小工具</title>
    <link href="https://p2tree.top/posts/21149469.html"/>
    <id>https://p2tree.top/posts/21149469.html</id>
    <published>2026-04-16T22:34:17.000Z</published>
    <updated>2026-04-16T14:40:11.000Z</updated>
    
    <content type="html"><![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/assets/css/APlayer.min.css"><script src="/assets/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><p>在日常终端操作中，许多经典命令行工具（如 <code>grep</code>、<code>ls</code>、<code>cd</code>、<code>df</code> 等）已经服务了几十年。它们稳定可靠，但也逐渐暴露出一些性能或用户体验上的不足。近年来，一批新的终端工具陆续出现，它们通过更智能的算法、更友好的交互界面以及更丰富的输出格式，为开发者提供了更高效的命令行体验。</p><p>本文介绍几个我在用的现代终端工具，你可以按需要选择是否去试用下，这些工具都是开源的，所以能保证它们的安全可靠。</p><p>由于不同操作系统、工具链有差异，我不会列出每个工具的安装方式，请自行到仓库路径下查找。</p><hr><h3 id="1-rg-ripgrep-——-高性能递归搜索工具"><a href="#1-rg-ripgrep-——-高性能递归搜索工具" class="headerlink" title="1. rg (ripgrep) —— 高性能递归搜索工具"></a>1. rg (ripgrep) —— 高性能递归搜索工具</h3><p><strong>替换目标</strong> ：<code>grep</code><br><strong>核心优势</strong> ：自动遵守 <code>.gitignore</code> 规则，支持并行递归搜索，内置高亮显示，并可搜索压缩文件。</p><p>与 <code>grep -r</code> 相比，<code>rg</code> 在大型代码库中的搜索速度通常快数倍至一个数量级。它默认启用正则表达式，并会自动跳过隐藏文件及二进制文件。</p><p><strong>常用选项与示例</strong> ：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">rg <span class="string">&quot;function_name&quot;</span>        <span class="comment"># 递归搜索当前目录</span></span><br><span class="line">rg -t py <span class="string">&quot;import&quot;</span>         <span class="comment"># 仅搜索 Python 文件</span></span><br><span class="line">rg -l <span class="string">&quot;TODO&quot;</span>              <span class="comment"># 仅列出包含匹配内容的文件名</span></span><br></pre></td></tr></table></figure><p><strong>同名替换？</strong>  ❌ <strong>不能</strong><br><code>rg</code> 并不完全兼容 <code>grep</code> 的所有选项（例如 <code>-P</code> 启用 Perl 兼容正则表达式，或某些输出格式控制选项）。许多现有脚本依赖 <code>grep</code> 的特定返回码或输出格式，直接替换可能导致意外行为。建议将 <code>rg</code> 作为独立工具使用。</p><p><strong>仓库位置：</strong>  <a href="https://github.com/BurntSushi/ripgrep">https://github.com/BurntSushi/ripgrep</a></p><hr><h3 id="2-fd-——-用户友好的文件查找工具"><a href="#2-fd-——-用户友好的文件查找工具" class="headerlink" title="2. fd —— 用户友好的文件查找工具"></a>2. fd —— 用户友好的文件查找工具</h3><p><strong>替换目标</strong> ：<code>find</code><br><strong>核心优势</strong> ：语法更简洁，默认忽略隐藏文件和 <code>.gitignore</code> 中指定的文件，输出彩色高亮，支持正则表达式。</p><p>相比 <code>find</code> 的复杂谓词组合，<code>fd</code> 采用直觉化的参数设计，例如 <code>fd pattern</code> 即可完成递归文件名匹配。</p><p><strong>常用选项与示例</strong> ：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">fd <span class="built_in">source</span>                 <span class="comment"># 查找路径下匹配 source 的文件</span></span><br><span class="line">fd -e rs                  <span class="comment"># 查找所有 .rs 文件</span></span><br><span class="line">fd -t d <span class="string">&quot;src&quot;</span>             <span class="comment"># 查找名为 src 的目录</span></span><br></pre></td></tr></table></figure><p><strong>同名替换？</strong>  ⚠️ <strong>谨慎，仅限交互式</strong><br><code>find</code> 的参数体系与 <code>fd</code> 完全不同，且大量自动化脚本依赖 <code>find</code> 的 <code>-exec</code>、<code>-print0</code> 等特性。强行设置 <code>alias find=&#39;fd&#39;</code> 会破坏这些脚本。建议将 <code>fd</code> 作为独立工具使用。</p><p><strong>仓库位置：</strong>  <a href="https://github.com/sharkdp/fd">https://github.com/sharkdp/fd</a></p><hr><h3 id="3-eza-——-现代化的-ls-替代"><a href="#3-eza-——-现代化的-ls-替代" class="headerlink" title="3. eza —— 现代化的 ls 替代"></a>3. eza —— 现代化的 ls 替代</h3><p><strong>替换目标</strong> ：<code>ls</code> <strong>核心优势</strong> ：支持图标显示、树形视图、Git 状态标识，并可按文件扩展名排序。</p><p><code>eza</code> 在输出中包含文件类型图标、每个文件的 Git 修改状态（例如 <code>M</code> 标记），并以不同颜色区分文件类型，显著提升了目录列表的可读性。</p><p>原 <code>exa</code> 项目已归档，不再推荐，<code>eza</code> 为其活跃分支。</p><p><strong>常用选项与示例</strong> ：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">eza -l --git              <span class="comment"># 显示详细信息及 Git 状态</span></span><br><span class="line">eza -T -L 2               <span class="comment"># 以树形结构显示两层目录</span></span><br><span class="line">eza --icons --<span class="built_in">sort</span>=size   <span class="comment"># 按文件大小排序并显示图标</span></span><br></pre></td></tr></table></figure><p><strong>同名替换？</strong>  ⚠️ <strong>谨慎，仅限交互式</strong><br><code>ls</code> 是 POSIX 标准定义的基本命令，许多脚本会解析其输出（例如通过管道获取列信息）。虽然 <code>eza</code> 实现了 <code>ls</code> 的大部分常见选项，但输出格式（如颜色代码、额外的 Git 列）可能破坏脚本逻辑。安全做法是仅在交互式配置中设置 <code>alias ls=&#39;eza&#39;</code>，并避免在脚本中使用该别名。</p><p>当然，更安全的做法依然是以独立工具使用。<code>eza</code> 对小拇指不友好，可以设置额外的一些别名：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> <span class="built_in">command</span> -v eza &amp;&gt; /dev/null; <span class="keyword">then</span> </span><br><span class="line">  <span class="built_in">alias</span> l=<span class="string">&#x27;eza&#x27;</span></span><br><span class="line">  <span class="built_in">alias</span> ll=<span class="string">&#x27;eza -l&#x27;</span></span><br><span class="line">  <span class="built_in">alias</span> la=<span class="string">&#x27;eza -a&#x27;</span></span><br><span class="line">  <span class="built_in">alias</span> lt=<span class="string">&#x27;eza --sort=time -l&#x27;</span></span><br><span class="line"><span class="keyword">fi</span></span><br></pre></td></tr></table></figure><p><strong>仓库位置：</strong>  <a href="https://github.com/eza-community/eza">https://github.com/eza-community/eza</a></p><hr><h3 id="4-difftastic-——-基于语法树的差异对比"><a href="#4-difftastic-——-基于语法树的差异对比" class="headerlink" title="4. difftastic —— 基于语法树的差异对比"></a>4. difftastic —— 基于语法树的差异对比</h3><p><strong>替换目标</strong> ：<code>diff</code><br><strong>核心优势</strong> ：解析源代码的抽象语法树（AST），从结构层面展示差异，而非逐行比较。</p><p><code>difftastic</code> 能够识别括号匹配、缩进变化等结构性修改，避免了传统 <code>diff</code> 因格式化变动而产生的大量无效差异。支持多种编程语言，输出带有侧边栏和可折叠区域的界面。</p><p><strong>常用选项与示例</strong> ：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">difft left.c right.c          <span class="comment"># 对比两个文件</span></span><br><span class="line">difft --display inline        <span class="comment"># 使用行内显示模式</span></span><br></pre></td></tr></table></figure><p>它同样可以用于 <code>git diff</code>，效果很显著。由于它懂语法，所以在展示代码修改差异时，可读性更强。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">git config --global diff.external <span class="string">&quot;difft --syntax-highlight off&quot;</span></span><br><span class="line"><span class="comment"># 我喜欢把语法高亮关闭，有点干扰显示</span></span><br></pre></td></tr></table></figure><p>有个缺点是，它需要分析语法树，所以在性能方面，比原生 <code>diff</code> 要慢，在打开大文件时会比较明显有延迟。</p><p><strong>同名替换？</strong>  ⚠️ <strong>谨慎，仅限交互式</strong><br><code>diff</code> 的退出码和输出格式被广泛用于自动化流程（例如 <code>patch</code> 工具、CI 脚本），而 <code>difftastic</code> 专为人工审阅设计，不具备兼容性。建议将 <code>difft</code> 作为独立工具使用。</p><p><strong>仓库位置：</strong>  <a href="https://github.com/Wilfred/difftastic">https://github.com/Wilfred/difftastic</a></p><hr><h3 id="5-zoxide-——-智能目录跳转"><a href="#5-zoxide-——-智能目录跳转" class="headerlink" title="5. zoxide —— 智能目录跳转"></a>5. zoxide —— 智能目录跳转</h3><p><strong>替换目标</strong> ：<code>cd</code> <strong>核心优势</strong> ：基于频率和最近使用时间记录访问过的目录，支持模糊匹配快速跳转。</p><p>使用 <code>z</code> 命令后跟部分目录名，即可直接跳转到匹配的目标目录，无需输入完整路径。<code>zoxide</code> 会维护一个本地数据库，自动更新访问权重。</p><p><strong>常用选项与示例</strong> ：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">z src                     <span class="comment"># 跳转到包含 &quot;src&quot; 的最常用目录</span></span><br><span class="line">z foo bar                 <span class="comment"># 多个匹配时显示交互式选择菜单</span></span><br></pre></td></tr></table></figure><p>如果配合 <code>fzf</code>，可以实现打开交互式窗口来选择性跳转，也挺有意思的，不过我不怎么用。<code>fzf</code> 是一个高性能的模糊匹配工具，也很不错，但它不属于可替换原生终端程序的范畴，我便没有列在本文中。</p><p><strong>同名替换？</strong>  ❌ <strong>尽量不要</strong>  <code>cd</code> 是 shell 内建命令，直接影响当前 shell 的进程工作目录。<code>zoxide</code> 虽然可以与 <code>cd</code> 配合使用，但无法完全模拟 <code>cd -</code>（返回上一目录）或 <code>cd &quot;$VAR&quot;</code> 等行为。建议将 <code>z</code> 作为独立工具使用。</p><p><strong>仓库位置：</strong>  <a href="https://github.com/ajeetdsouza/zoxide">https://github.com/ajeetdsouza/zoxide</a></p><hr><h3 id="6-htop-——-增强型进程监控器"><a href="#6-htop-——-增强型进程监控器" class="headerlink" title="6. htop —— 增强型进程监控器"></a>6. htop —— 增强型进程监控器</h3><p><strong>替换目标</strong> ：<code>top</code><br><strong>核心优势</strong> ：彩色界面、鼠标支持、树状进程视图、可直接发送信号，并显示每个 CPU 核心的使用率。</p><p><code>htop</code> 允许用户使用上下键滚动进程列表，按 <code>F9</code> 快速终止进程，按 <code>F5</code> 切换树状视图，比 <code>top</code> 的默认交互更为直观。</p><p><strong>常用选项与示例</strong> ：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">htop                      <span class="comment"># 启动交互式监控</span></span><br><span class="line">htop -t                   <span class="comment"># 以树形结构显示进程</span></span><br><span class="line">htop -p 1234,5678         <span class="comment"># 仅监控指定的 PID</span></span><br></pre></td></tr></table></figure><p><strong>同名替换？</strong>  ⚠️ <strong>仅限交互式，不适合脚本</strong><br><code>top</code> 的批处理模式（<code>-b</code>）常被脚本用于采集系统信息，而 <code>htop</code> 不支持该模式。因此可以在交互环境中设置 <code>alias top=&#39;htop&#39;</code>，但不应在 <code>.bashrc</code> 等全局配置中覆盖，以免影响自动化任务。</p><p><strong>仓库位置：</strong>  <a href="https://github.com/htop-dev/htop">https://github.com/htop-dev/htop</a></p><hr><h3 id="7-axel-——-多线程下载工具"><a href="#7-axel-——-多线程下载工具" class="headerlink" title="7. axel —— 多线程下载工具"></a>7. axel —— 多线程下载工具</h3><p><strong>替换目标</strong> ：<code>wget</code> &#x2F; <code>curl</code>（下载文件场景）<br><strong>核心优势</strong> ：通过多个连接分段下载同一文件，可显著提升下载速度（尤其在网络延迟较高或服务端限速时）。</p><p><code>axel</code> 会将目标文件切分为多个部分并行获取，最后自动合并。它支持断点续传和限速功能。</p><p><strong>常用选项与示例</strong> ：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">axel -n 10 -o ./file.zip <span class="string">&quot;https://example.com/bigfile.zip&quot;</span>   <span class="comment"># 10 线程下载</span></span><br><span class="line">axel -a                                                      <span class="comment"># 显示详细进度</span></span><br><span class="line">axel --limit-speed=100K                                      <span class="comment"># 限制速度为 100KB/s</span></span><br></pre></td></tr></table></figure><p><strong>同名替换？</strong>  ❌ <strong>完全不能</strong><br><code>wget</code> 和 <code>curl</code> 拥有极其丰富的参数体系（HTTP 头、Cookie、POST 数据等），<code>axel</code> 仅覆盖最简单的下载场景。不应尝试替换，而是将其作为补充工具使用。</p><p><strong>仓库位置：</strong>  <a href="https://github.com/axel-download-accelerator/axel">https://github.com/axel-download-accelerator/axel</a></p><hr><h3 id="8-bat-——-带语法高亮的文件查看器"><a href="#8-bat-——-带语法高亮的文件查看器" class="headerlink" title="8. bat —— 带语法高亮的文件查看器"></a>8. bat —— 带语法高亮的文件查看器</h3><p><strong>替换目标</strong> ：<code>cat</code><br><strong>核心优势</strong> ：自动识别文件类型并添加语法高亮，集成 Git 行级修改标记，默认使用分页器（<code>less</code>）避免刷屏。</p><p><code>bat</code> 在显示代码文件时，会在行号旁标注 Git 的增删状态，并提供与 IDE 类似的色彩方案。</p><p><strong>常用选项与示例</strong> ：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">bat main.rs               <span class="comment"># 高亮显示 Rust 代码</span></span><br><span class="line">bat -A config.json        <span class="comment"># 显示不可见字符（类似 `cat -A`）</span></span><br><span class="line">bat -p                    <span class="comment"># 禁用所有增强特性，模拟纯 `cat`，但仍包含语法高亮</span></span><br></pre></td></tr></table></figure><p><strong>同名替换？</strong>  ⚠️ <strong>部分可行，但有风险</strong><br>当 <code>bat</code> 的输出被重定向到管道时，它会自动关闭分页和高亮，行为接近 <code>cat</code>。但它仍可能输出额外的元信息（如默认的文件头）。需要谨慎考虑是否替换。</p><p><strong>仓库位置：</strong>  <a href="https://github.com/sharkdp/bat">https://github.com/sharkdp/bat</a></p><hr><h3 id="9-tldr-——-简化的命令示例手册"><a href="#9-tldr-——-简化的命令示例手册" class="headerlink" title="9. tldr —— 简化的命令示例手册"></a>9. tldr —— 简化的命令示例手册</h3><p><strong>替换目标</strong> ：<code>man</code>（仅限快速查阅常用用法）<br><strong>核心优势</strong> ：提供社区维护的常见命令示例，每个命令通常包含 4–6 个典型场景，避免翻阅冗长的手册页。根据系统语言还能自动显示不同的翻译版本（比如中文）。</p><p><code>tldr</code> 适合快速回忆参数格式，例如 <code>tldr tar</code> 会直接显示压缩和解压的常用命令。每个工具的示例描述都是网友手工维护的，如果没有你想要的工具，也可以到仓库去提交。</p><p><strong>常用选项与示例</strong> ：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">tldr tar                  <span class="comment"># 显示 tar 命令的常见示例</span></span><br><span class="line">tldr --update             <span class="comment"># 更新本地示例缓存</span></span><br><span class="line">tldr -p linux <span class="built_in">dd</span>          <span class="comment"># 显示 Linux 平台下的 dd 示例</span></span><br></pre></td></tr></table></figure><p><strong>同名替换？</strong>  ❌ <strong>完全不能</strong><br><code>man</code> 提供完整的结构化文档及 <code>man -k</code> 关键词搜索功能，<code>tldr</code> 无法覆盖这些用途，它们的选项也完全不同。两者属于是同一功能的不同实现。简单查询可以用 <code>tldr</code>，如果想完整了解一个工具，依然得打开 <code>man</code>。</p><p><strong>仓库位置：</strong>  <a href="https://github.com/tldr-pages/tldr">https://github.com/tldr-pages/tldr</a> </p><p><strong>一个 Rust 实现的客户端：</strong>  <a href="https://github.com/dbrgn/tealdeer">https://github.com/dbrgn/tealdeer</a></p><hr><h3 id="10-dua-——-交互式磁盘使用分析器"><a href="#10-dua-——-交互式磁盘使用分析器" class="headerlink" title="10. dua —— 交互式磁盘使用分析器"></a>10. dua —— 交互式磁盘使用分析器</h3><p><strong>替换目标</strong> ：<code>du</code><br><strong>核心优势</strong> ：更实用的展示路径下各文件和目录的磁盘使用量。它是并发统计的，所以性能更好，显示也更人性化。</p><p><strong>常用选项与示例</strong> ：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">dua                       <span class="comment"># 统计当前路径下的磁盘使用量</span></span><br><span class="line">dua *                     <span class="comment"># 只统计非隐藏文件和路径的磁盘使用量</span></span><br></pre></td></tr></table></figure><p><strong>同名替换？</strong>  ❌ <strong>完全不能</strong><br><code>du</code> 的输出经常被脚本解析（例如 <code>du -k | cut -f1</code>），两者是完全不同的交互实现。请将 <code>dua</code> 作为独立的分析工具使用。</p><p><strong>仓库位置：</strong>  <a href="https://github.com/Byron/dua-cli">https://github.com/Byron/dua-cli</a></p><hr><h3 id="11-duf-——-改进的磁盘空间查看器"><a href="#11-duf-——-改进的磁盘空间查看器" class="headerlink" title="11. duf —— 改进的磁盘空间查看器"></a>11. duf —— 改进的磁盘空间查看器</h3><p><strong>替换目标</strong> ：<code>df</code><br><strong>核心优势</strong> ：使用进度条和颜色区分不同设备类型（本地磁盘、挂载点、临时文件系统），支持 JSON 输出。</p><p><code>duf</code> 会自动分组显示设备，并用绿色到红色的渐变表示使用率，便于快速识别磁盘占用过高的分区。</p><p><strong>常用选项与示例</strong> ：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">duf                       <span class="comment"># 显示所有挂载点</span></span><br><span class="line">duf --only <span class="built_in">local</span>          <span class="comment"># 仅显示本地磁盘</span></span><br><span class="line">duf --output mountpoint,size,used   <span class="comment"># 仅输出指定列</span></span><br></pre></td></tr></table></figure><p><strong>同名替换？</strong>  ❌ <strong>完全不能</strong><br><code>df</code> 的 POSIX 兼容输出格式（<code>-P</code> 选项）被许多运维脚本依赖，而 <code>duf</code> 的输出结构与之不同，所以不要同名替代 <code>df</code>。请直接使用 <code>duf</code> 作为磁盘统计工具使用。</p><p><strong>仓库位置：</strong>  <a href="https://github.com/muesli/duf">https://github.com/muesli/duf</a></p><hr><h3 id="总结：工具的演进与选择策略"><a href="#总结：工具的演进与选择策略" class="headerlink" title="总结：工具的演进与选择策略"></a>总结：工具的演进与选择策略</h3><p>上述工具在特定场景下显著提升了终端操作效率，但它们并非完全兼容 POSIX 标准命令。因此在实际使用中，建议采取以下策略：</p><ul><li><strong>交互式环境</strong> ：可以自由设置 <code>alias</code> 指向这些现代工具，享受更丰富的功能和更好的用户体验。</li><li><strong>编写脚本</strong> ：坚持使用系统原生命令（如 <code>/bin/grep</code>、<code>/bin/find</code>），或通过绝对路径调用，以确保可移植性和可预测的行为。</li><li><strong>持续关注</strong> ：工具生态不断进化，今天优秀的替代品未来可能被更优的实现取代。保持开放心态，定期评估新工具，但始终以兼容性和稳定性为前提。</li></ul><p>命令行世界没有“终极”工具，只有不断适应需求演进的实用方案。合理利用这些现代终端工具，可以在不牺牲可靠性的前提下，大幅提升日常开发与运维的舒适度。</p><hr><div class="note info flat"><p>本文同步发布在知乎账号下：<a href="https://zhuanlan.zhihu.com/p/2028198763173359650">汇总几个终端小工具</a></p><p>标题图片使用豆包 AI 生成，提示词：终端窗口、黑色背景、涂色画板、毛笔、16:9 画幅。</p></div>]]></content>
    
    
      
      
    <summary type="html">&lt;link rel=&quot;stylesheet&quot; class=&quot;aplayer-secondary-style-marker&quot; href=&quot;/assets/css/APlayer.min.css&quot;&gt;&lt;script src=&quot;/assets/js/APlayer.min.js&quot; cla</summary>
      
    
    
    
    <category term="软件工具" scheme="https://p2tree.top/categories/%E8%BD%AF%E4%BB%B6%E5%B7%A5%E5%85%B7/"/>
    
    
    <category term="高效编程" scheme="https://p2tree.top/tags/%E9%AB%98%E6%95%88%E7%BC%96%E7%A8%8B/"/>
    
    <category term="终端开发" scheme="https://p2tree.top/tags/%E7%BB%88%E7%AB%AF%E5%BC%80%E5%8F%91/"/>
    
    <category term="Terminal" scheme="https://p2tree.top/tags/Terminal/"/>
    
    <category term="软件工具" scheme="https://p2tree.top/tags/%E8%BD%AF%E4%BB%B6%E5%B7%A5%E5%85%B7/"/>
    
  </entry>
  
  <entry>
    <title>我的名字</title>
    <link href="https://p2tree.top/posts/f12fc85f.html"/>
    <id>https://p2tree.top/posts/f12fc85f.html</id>
    <published>2026-04-10T23:26:36.000Z</published>
    <updated>2026-04-10T16:16:36.000Z</updated>
    
    <content type="html"><![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/assets/css/APlayer.min.css"><script src="/assets/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><h2 id="我的乳名"><a href="#我的乳名" class="headerlink" title="我的乳名"></a>我的乳名</h2><p>我出生在一个普通的工薪家庭。我父母为我赐名 “<strong>杨柳</strong>”，据他们后来给我的解释，并未对这个名字有什么特别的深意。不过，它一直携带着一个有趣的故事，让母亲反复提及。</p><p>我有个堂哥，叫杨树。我出生时，父亲向我爷爷奶奶请求赐名，爷爷奶奶是农民，没有什么文化，奶奶说，既然有个哥哥叫杨树，不如就叫 “<strong>杨二树</strong>” 吧。现在看起来滑稽的命名，在当时的农村，其实非常普遍，用数字来表明辈次关系，类似于古代的 “伯、仲、叔、季”，使用 “大、二、三、四” 更加口语化也更容易理解。</p><p>妈妈家比爸爸家有文化些，我姥爷是中学教师，妈妈委婉地说，不如叫 “杨柳” 吧，杨柳青青，富有生命力。在我看来，虽然 “杨柳” 并不是我很满意的一个名字，但总比 “二树” 要强很多了。</p><p>基于我的大名，我的乳名也就出现了 “柳柳”，“柳子”，“柳毛”，以及姥姥专用的 “臭毛”。</p><p>古人说，名者，命也。名字在出生时由长辈赋予，寄托着最初的期待，但也确实客观上塑造了我后天的性格和行为。</p><p>杨柳，是一个很美的词，杨树正直、柳树柔韧，杨柳代表着生命力，也代表着豪迈和通达。父母没有在我的名字中留下对我生命宏大的期许和祝福，反而做出了留白，似乎有意为我的创作留下空间。</p><h2 id="我的大名"><a href="#我的大名" class="headerlink" title="我的大名"></a>我的大名</h2><p>杨属于我的姓，所以很多时候，人们会直接评价你的名，“柳” 虽然中性，但在中国文化中更多指妖娆和婉约，也就多指代女性。在我性启蒙的那个年龄段，我盲目地对这个事儿特别固执，认为我一个男孩不应该拥有一个 “女名”。</p><p>在古代，行成人礼时，会为孩子赋予一个 “字”，之后，这个孩子就成为了 “人”。字是对名的补充和深化，赋字，也是自己在这十几年中，通过自己的学识和自我了解，重新对自己下定义的一个机会。</p><p>现在社会已经没有 “字” 这一说了，名和字合并成名字，成为人一生中的代词。不过，我想到了改名。我幼小时，性格内向，不善言谈，不喜外出，在我看来，这就是女孩才会有的习惯。在那个懵懂的、充满性别偏见的时代，我幼稚地把这一切归结为，我起了一个 “女名”。</p><p>我认为，姓名就像生命一样，是父母赋予的，妄不可随意弃置。但又考虑确实想改名，最终决定，在现有的 “杨柳” 后边，加一个字，作为我自己赠给自己的字，作为我对自己人生底色的期许。</p><p>我和父母讨论过，他们给了我一个建议，不如加一个 ming 音字。我表示同意，爸爸给的建议是 “杨柳明”，指柳暗花明之意，但我不想捧一贬一，我的 “柳” 字应该有更多的解释；妈妈给的建议是 “杨柳鸣”，指一鸣惊人之意，亦指树木也能说话的奇观，但我认为，鸟入树林而鸣叫，不那么酷。</p><p>我用了一下午的时间，翻遍了字典，最后决定用 “<strong>铭</strong>”。在我看来，我的 “柳” 代表了柔韧和变通，应该再加一个 “坚硬” 的词进来。“铭” 字指刻在器物上的文字，用于警醒自己，比如 “座右铭”，刻在石头金属上，也不容易磨灭，有青史留名之意。</p><p>古人在赋字时，有些会和名前后呼应，比如诸葛亮字孔明，有些会和名互为补充，比如孔丘字仲尼。“铭” 既能表名词，也能表动词，表动词时，可以指 “刻在石头上”。杨柳是自然所为，是一种被动接受，而“铭”是人工所刻，是一种主观行为。在这份天性之上，我想要靠自己刻出自己独特的人生。</p><h2 id="我的网名"><a href="#我的网名" class="headerlink" title="我的网名"></a>我的网名</h2><p>数字时代，人出现了新的代词。在网络生活中，为了能保护隐私，往往会起网名。网名总是天马行空，它代表着信息时代的新人类，好奇、兴奋，和对新时代的美好向往。网名就像是古人的 “号”，号是文人自取，任意发挥，表达志趣的代称，比如苏轼的 “东坡居士”。</p><p>我最初的网名，是在第一次接触互联网时，在创建 QQ 账号时起的，叫 “<strong>迁徙的森林</strong>”。这个有趣的名字，有这么个由来，我的大名中有 “杨柳” 二字，自然我希望在网名中也保有 “树” 相关的寓意。再加上那段时间比较沉迷魔兽争霸，对里边的精灵一族很向往，精灵族有很多可以移动的古树，看起来真的很酷。</p><p>另外，它其实也饱含着另一层意思。</p><p>我不但名字里带有 “树”，我的童年也实实在在的和树相关。我出生时，我姥爷就承包着当地的一大块果园，据说是妈妈还小的时候，就开始承包的。果园里大多数是酸果子（一种沙果），还有苹果、梨、少量的桃和杏，还有海红子（类似樱桃）。说我的课余生活，是在果园里长大的，一点也不为过。关于果子、鸟、蛇和蚊子，我都可以单独写一篇文，恰可仿着鲁迅的《从百草园到三味书屋》。</p><p>然而，美好的记忆终将随着时间化成沙子。</p><p>我上初中后，这片果园消失了。姥爷承包的果园是属于学校的，但学校倒闭了。果园作为学校资产被用来清算，最终全部砍伐，改为耕地。曾经，从院子里望去，满眼地绿树茵茵，现在，只看到远处起伏地沙丘。</p><p>这就是我第一个网名中，另一层意思。在大人眼里边，果园是一份资产，是一份契约，在我眼里边，是承载我童年生活的襁褓。</p><p>我的第二个网名，是在高中时改的，叫 “<strong>西伯利亚雪人</strong>”。高中时没有太多时间上网，那段时间也阅读了很多文学著作，对充满白雪皑皑的邻国北疆特别仰慕。我的生日是在冬天，我也很喜欢冬天下雪，于是，就换了这么个名字。后来直到大学，我的网名都围绕着这个名字转悠，比如 “<strong>SiberiaBear</strong>”。</p><p>父亲通讯录中对我的备注，就是 “雪人”，也来自于这个网名。不过我很不确定，它会不会还有另外一层意思？长大的孩子，犹如冬天的雪人，年前出现，年后消失。</p><p>工作后，才后知后觉地意识到，网名和名字一样，也是一个很重要的代称，它是一个人数字时代的标记。运营自己的社交账号和网络动态，就像是培养一个全新的自己，也非常重要。</p><p>我重新审视了自己的网名，最后，拟定了一个颇具调侃意味的名字，<strong>P2Tree</strong>。还记得，曾经奶奶为我赋予第一个名字，但最后没有使用，现在，那就重新用起来吧。2Tree 便指 “<strong>二树”</strong>。二树，就像二狗子，二哈喇子，二流子一样，用在真名上太过于粗陋，但网络极大的包容性，开放性，却恰好合适。</p><p>P 这个开头，来自于杨树的英语 Populus，间接地，我依然把我的姓氏，放在了网名中。我爱人也这么做了，不过，她是以音转形，我是以英转形。当然，另一个必须要加一个字母到前边的理由是，我的职业病。程序中定义一个符号名字，规则不允许以数字开头，加个字母，看起来更舒服。</p><p>2 除了 “二树” 中的 “二” 一义外，也有英语 “to” 之意，类比于 P2P，代表着一种连接，一种将传统姓氏和网络生活的连接，一种将宗族印记和情感经历的连接。</p><h2 id="未来"><a href="#未来" class="headerlink" title="未来"></a>未来</h2><p>二十一世纪的 “名与字”，已经不局限于传统和礼教的产物，而成为一种自我构建的痕迹。“杨柳铭” 和 “P2Tree” 分别同为三个音节，称起来朗朗上口，又存在着一种独特和隐晦的关联。</p><p>直到我的 30 岁后，我才意识到，我无意中走出了一条和古人命名神似的路，只是用了这个时代的语言，用了我独特的故事。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;link rel=&quot;stylesheet&quot; class=&quot;aplayer-secondary-style-marker&quot; href=&quot;/assets/css/APlayer.min.css&quot;&gt;&lt;script src=&quot;/assets/js/APlayer.min.js&quot; cla</summary>
      
    
    
    
    <category term="自我介绍" scheme="https://p2tree.top/categories/%E8%87%AA%E6%88%91%E4%BB%8B%E7%BB%8D/"/>
    
    
    <category term="人生" scheme="https://p2tree.top/tags/%E4%BA%BA%E7%94%9F/"/>
    
  </entry>
  
  <entry>
    <title>一个高中生的疑问</title>
    <link href="https://p2tree.top/posts/695fd99.html"/>
    <id>https://p2tree.top/posts/695fd99.html</id>
    <published>2026-03-04T22:07:17.000Z</published>
    <updated>2026-03-04T14:24:34.000Z</updated>
    
    <content type="html"><![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/assets/css/APlayer.min.css"><script src="/assets/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><p>在知乎闲逛时，发现了一个很有价值的老问题：<a href="https://www.zhihu.com/question/530184935/answer/2012546984393855869">数学究竟是自然界本就存在的某种联系，还是我们人类的创造？</a>。</p><p>这么好的问题，居然4年后我才看到。题主现在应该已经上大学了，估计也有了自己的答案，但我还是忍不住想试着参与下回答，就姑且帮我自己整理下思路吧。</p><hr><blockquote><p>乘法源于人们求土地面积的经验，那么，为什么这种丈量土地的经验能够运用在包括行星轨道在内的那么多领域？丈量土地与行星公转间存在着某种联系吗？宇宙间其它的文明认为这两者之间存在着规律吗？</p></blockquote><p>一个最基本的观点是，这个世界的信息，本身是混沌的，人类需要用自己的大脑来理解世界，那么必然需要建立一种语言，来描述这复杂的世界信息，形式化的描述有利于思考，也有利于交流。</p><p>所以，这个角度来说，<strong>数学是地球人创造的一种宇宙语言</strong>。</p><p>人类在日常生活中，很多问题可以通过这种语言来简化和抽象，进而发现，经过抽象后的数学，可以套用到不同的应用场景，比如丈量土地和计算行星轨道。</p><p>丈量土地和行星公转之间没有直接联系，但它们却拥有相同的数学基础，能用我们的数学语言来描述。如果是外星文明，它们的数学语法可能不同，但一定拥有相同的数学原理。其实也不用去地球之外，在我们自己的世界没有完全揭露前，不同大陆的文明，使用的数学就是不同的。</p><p>做个有趣的假设，如果外星文明是一种深海文明，它们不是生活在行星的地表，而是生活在大洋的深处。那么，它们没有视力，却有极强的听力，这种单纯使用听力在三维空间中演化的生物，它们的数学和我们将完全不同。比如，它们的数值单位可能是球壳体积，而不是二维值。</p><p>不过不管它们的数学是什么形态，当进入宇宙时，它们也会用自己的数学体系，来计算行星运行轨迹（看样子这种生物可能更容易理解三维宇宙的概念）。</p><hr><blockquote><p><strong>数学形式反映事物的本质吗？</strong> 库仑力和外有引力都合乎平方反比律，具有同一数学形式。那么，库仑力和万有引力具有某种共同本质吗？</p></blockquote><p>它们当然具有共同的本质。库仑力和万有引力刚好是微观和宏观两个领域有代表性的概念，它们反应着相同的性质，都是在三维各向同性空间中，传递粒子（引力子或光子）在静止质量为零时的相互作用。它们的数学形态是一致的。</p><p>脑洞大开一下，宇宙是否可能在更高维度上，是一个三维的回环。那么三维的极小，可能就连接着极大。此时能用同一种数学形式描述，也就合乎情理了。</p><p>科学界的观点是，数学反映了事物的本质。现代科学都在追求一个伟大的梦想，也就是大一统理论。我们想用一套数学语言来描述这个世界上所有的现象。</p><p>虽然近现代科学中，提出的一些假设和猜想，至今仍然没有被证明，但人们还是相信，这是人类科学能力的局限，而不是数学的局限。</p><p>反过来说，如果有一个现象，和现在的数学理论相悖，那么整个人类文明都将崩塌。不过大可放心，就像曾经发明复数、无理数一样，人类一定可以为数学大厦添砖加瓦，来解释更多的宇宙本质。</p><hr><blockquote><p>冷热、快慢、多少，人们为什么能把这些不同的联系归为一类？因果、并列等<strong>逻辑关系</strong>，是宇宙真理的一部分，还是我们人类思维的一部分？</p></blockquote><p>首先，这关乎人类如何建立“概念”，这是人类思维的基础。</p><p>冷热、快慢这些概念，都是人类创造的，它们用来描述人类感知到的一类客观体验。为了能描述两个不同事物的相对性，人们最初有了 “比…更…”这种描述，但为了能更细分不同体验的相对性，就发明了这些概念。</p><p>这些概念用于描述我们客观感受到的宇宙规律，也反应出了我们的思维状态。</p><p>因果，并列这种概念，既是宇宙真理，也是人类思维的体现。</p><p>当它被看作是宇宙真理时，它是一种逻辑论，也就是说，即便没有人类，甚至没有地球，宇宙运行还是具有逻辑。可能没有我们熟悉的数学，但逻辑本身是存在的。</p><p>当它被看作是人类思维时，它是一种工具论，我们创造了这些数学概念，来解释我们感知到的世界运行行为。此时，逻辑是我们叙述和分析世界的工具。</p><p>这个问题很有深度，它从数学出发，实则是一个哲学问题。数学只是它的表层语言。</p><hr><blockquote><p>许多动物的脑不能认识到某种联系。如果我们人类无法分辨差异，我们还会提出不等式的概念吗？会不会存在某种我们的大脑无法处理识别的逻辑关系，导致我们的数学不够完善？</p></blockquote><p>这个问题已经超出了数学，进入了认知科学的领域。</p><p>如果我们无法分辨差异，我们确实不会提出不等式的概念。</p><p>就像我们无法看到四维空间，所以我们也就没有提出在四维空间中能够适用的那些数学概念。或者，如果有一种生活在计算机中的生命，它们演化过程中看到的环境变化，只有 0 和 1 的跳变（他们的数学符号可能是 <code>.</code> 和 <code>,</code>），那么，在它们从计算机里出来前，就无法创造大于和小于这种逻辑关系。</p><p>就像前边说的，我们的数学是我们地球人发明的，用来描述世界的一种形式化语言，如果我们感知不到某些概念，自然也就不会对应创造那些概念的符号。</p><p>因为我们的大脑无法处理一些信息，所以，我们创造的数学一定是不完善的。也就是说，我们认识的世界是有限的。</p><p>这就是科学发展的动力，用我们有限的大脑，去探索和解释看起来无限的宇宙。我们的数学，实际上已经扩展到远大于我们能理解的世界，何况，我们还能创造概念。</p><hr><blockquote><p>宇宙应该遵循同一的运动法则，但**为什么”more is different”**，复杂系统的运动规律与其部分几乎全然不同？这是宇宙的某种特质还是我们的错觉呢？</p></blockquote><p>很有意思的一个问题。简单元素组成复杂系统时，复杂系统的规律和行为，确实和简单系统不相同。但这不违背事物的基本规律。</p><p>简单来说，<strong>在组成物质时，新的产物所拥有的物理状态，不只依赖于组成其的基础物质，还依赖于这些基础物质之间的关系</strong>。</p><p>就像是两个没有关系的陌生人，他们恰好出现在同一个电梯里，大概率什么也不会发生。但如果他们之间认识，那出现在同一个电梯里，大概率就会产生新的信息（简单对话或思维推演）。这种差异就是因为事物之间的关系。</p><p>小到基本粒子，大到行星之间，都可能产生关系，产生关系的初始条件和边界条件各不相同。</p><p>说的浪漫一些，宇宙的神奇之处就在于，它有基本的运行规则，比如数学和物理，但它也可能将这些基本规则不断组合，产生出新的未知的东西。比如生命、文明和思想。</p><hr><blockquote><p><strong>“直觉”是相对的吗？</strong> 如果有外星人在高速飞行的星系，它们会不会先发现相对论再发现牛顿力学？</p></blockquote><p>你说的“直觉”可能是指我们人类所处的环境和历史经验的总结。显然，“直觉”就是相对的。</p><p>我们的文明和科学发展，依赖于它一定是建立在解释这个“观察”的世界为前提的。</p><p>如果有一种外星文明，他们星球的时间流速非常快，比如说行星自转速度是光速的一半，也许行星围绕着一个黑洞运转。</p><p>显而易见，他们不会觉得自己“快”，因为“快慢”是相对的，在他们的参照体系下，他们的时间是正常的，而当他们开始探索宇宙时，才会发现宇宙中很多区域的时间很慢，也许会得出“时间流速太慢，以至于无法诞生生命”这种观点。</p><p>在已知的宇宙学中，光速是恒定的，所以我们才能清晰地假设这个问题。我们所生活的世界，让我们先发现了经典力学，之后才探索了相对论、量子力学这些我们平常观察不到的领域。</p><p>而对于外星文明，如果他们的时间流速是稳定的，那么他们也会先发现经典力学，因为早期文明不可能创造大尺度的时间差异。当然，如果他们的星球足够大，比如是一种有着极厚大气的行星，他们作为一种飞行生物，可以快速飞行穿梭，也许他们能够更容易推演出相对论。</p><hr><blockquote><p>发现物理规律，实际上就是找到一个数学公式，满足我们的实验数据。那么，我们可不可以通过计算机给出海量的数学公式，再把实验数据代入，以发现物理规律？</p></blockquote><p>理论上可行，这是人工智能领域的一个研究课题，它有个专门的名字：符号回归。</p><p>具体的步骤和你说的相似，先生成很多随机的数学公式，再代入实验数据，选择最优的公式。</p><p>然而，这种实验目前还存在困难。</p><p>首先，为了结果能足够精确，就需要穷举非常大数量级的输入，放在现在的计算机上，也依然是个非常耗费资源的过程。</p><p>其次，机器学习中存在过拟合、局部最优这些概念，计算机也许能给你一个新的、陌生的答案，但科学家经过重复验证，会发现结果并不总是可靠。如果不能通过正向的推导和验证，这样的答案得不到信服。</p><p>然后还有最大的一个问题，很多理论依赖于前序理论的出现，也就是说科学演进得一步步走。如果想发现万有引力定律，必须先发现重力、质量等概念。通过计算机发现的新公式，如果没有足够的先验信息，那这种公式是没有意义的。</p><p>我对 AI 能参与科学研究这个应用，还是比较保守。当然这个就得看接下来几十年人工智能的发展成果了。</p><hr><p>最后，对那个提出问题的网友一些主观评价。</p><p>请允许我对你表达敬意，你年纪轻轻就能跳出基础教育的“解题”框架，去思考“知识”的本质，这种习惯难能可贵。你让我再次看到那个年轻时的我。</p><p>想想看，好像高中时，如果对学习游刃有余，似乎脑子里就会忍不住蹦出这些看似奇奇怪怪的问题，很可惜我当时没有网络，也没有前辈能交流。记得我当时学椭圆公式时，试着猜想三维椭球的球面公式，但老师说我超纲了（&#x2F;苦笑）。</p><p>你拥有珍贵的<strong>元认知能力</strong>，也就是说，大多数人只是在学习知识，而你在思考知识的由来和去向。用前几年的网络流行语，便是“你预判了别人的预判”。</p><p>我希望未来的年轻人，都能早早有这种能力。它体现出多个不同的特质：</p><ul><li><strong>怀疑精神</strong>。当今世界，发展日新月异，曾经的知识可能用几代人，现在的一代人可能学几版不同的知识，甚至由计算机直接输出答案。这个过程中，最可贵的就是怀疑精神，有了怀疑的动机，才会有自主探索和追求真理的动力。</li><li><strong>迁移类比能力</strong>。知识是相通的，不但同一个学科中的概念相通，学科之间的概念也是相通的；不只是大类学科共享基础知识，大类之间也共享类似的规律。在这种探索学科边界和跨学科领域的过程中，才能更容易产生卓越的成就。</li><li><strong>想象力</strong>。就像我前边提到，我们的数学早已经能够解释很多我们未曾看到也未曾想到的现象，这是因为人类的想象力。科学家需要有想象力，因为他们需要创造工具给全人类用。务必珍惜这种想象力和好奇心，随着年龄的增长，他们可能一点点流逝。</li></ul><p>知识可能过时，但这种思考问题的源动力，不会过时，它比任何知识都更可贵。</p><hr><p>原文来自 <a href="https://www.zhihu.com/question/530184935/answer/2012546984393855869">知乎</a>。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;link rel=&quot;stylesheet&quot; class=&quot;aplayer-secondary-style-marker&quot; href=&quot;/assets/css/APlayer.min.css&quot;&gt;&lt;script src=&quot;/assets/js/APlayer.min.js&quot; cla</summary>
      
    
    
    
    <category term="生活感悟" scheme="https://p2tree.top/categories/%E7%94%9F%E6%B4%BB%E6%84%9F%E6%82%9F/"/>
    
    
    <category term="学习方法" scheme="https://p2tree.top/tags/%E5%AD%A6%E4%B9%A0%E6%96%B9%E6%B3%95/"/>
    
  </entry>
  
  <entry>
    <title>Chapter.116</title>
    <link href="https://p2tree.top/posts/aefa920e.html"/>
    <id>https://p2tree.top/posts/aefa920e.html</id>
    <published>2026-01-14T23:34:01.000Z</published>
    <updated>2026-01-14T15:48:05.000Z</updated>
    
    <content type="html"><![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/assets/css/APlayer.min.css"><script src="/assets/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><p>告诉你的孩子，十八岁前，不会让他们因为钱而感到窘迫，这是为了塑造自信的性格，但十八岁后，会让他们因为钱而感到烦恼，这是为了培养独立的金钱观。</p><p><img src="https://img.p2tree.top/2026/1/14/chapter-116.webp" alt="Chapter.116"></p><hr><div class="note primary flat"><p>封面图片是由豆包 AI 生成的图片，一只望向星空的小狗。</p><p>转载自我自己的<a href="https://mp.weixin.qq.com/s/J_0C7dqx5TDCGPCz061pLg">微信公众号</a>，欢迎关注。</p></div>]]></content>
    
    
      
      
    <summary type="html">&lt;link rel=&quot;stylesheet&quot; class=&quot;aplayer-secondary-style-marker&quot; href=&quot;/assets/css/APlayer.min.css&quot;&gt;&lt;script src=&quot;/assets/js/APlayer.min.js&quot; cla</summary>
      
    
    
    
    <category term="生活感悟" scheme="https://p2tree.top/categories/%E7%94%9F%E6%B4%BB%E6%84%9F%E6%82%9F/"/>
    
    
    <category term="育儿" scheme="https://p2tree.top/tags/%E8%82%B2%E5%84%BF/"/>
    
  </entry>
  
  <entry>
    <title>编译器视角下的未定义行为</title>
    <link href="https://p2tree.top/posts/bf9edcf9.html"/>
    <id>https://p2tree.top/posts/bf9edcf9.html</id>
    <published>2025-12-31T09:18:22.000Z</published>
    <updated>2025-12-31T01:23:34.000Z</updated>
    
    <content type="html"><![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/assets/css/APlayer.min.css"><script src="/assets/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><p>前段时间，公司内有过一次关于未定义行为的讨论，我认为很有价值，也从中学到了东西，整理成文。</p><p>我简单介绍下几个相关的概念，并着重探讨一些未定义行为的程序用法。</p><h2 id="什么是未定义行为"><a href="#什么是未定义行为" class="headerlink" title="什么是未定义行为"></a>什么是未定义行为</h2><p>未定义行为，Undefined Behavior（UB），并不是一个软件工程行业专有的话题，而是在各个领域都存在的普遍概念。</p><p>比如，开挖掘机可以挖沙子，也可以用来捞鱼，甚至，用它来挖鼻孔也可以，只要驾驶员有足够的能力驾驭它。而挖掘机说明书中，大概率不会提到用它捞鱼或挖鼻孔，虽然理论上有操作的可能性。</p><p>这些行为就可以称为使用挖掘机的未定义行为，又或者用手机捣蒜、用验尿机验茶水，都是 UB 行为。</p><p>未定义行为发生时，无法预测也无法保证它会产生什么后果，但一定是不推荐的行为。在软件上，通常指可以通过编译的编程用法，但不保证它实际会产生什么运行期效果。</p><p>程序存在未定义行为，就意味着它的执行结果有可能正确，也有可能错误，它可能会导致程序崩溃，但也可能看起来一切正常。而决定最终结果的因素，可能是编译器、操作系统、处理器或者运行时物理环境等。</p><p>不过需要明确的一点是，<strong>未定义行为应当被认为是一种 Bug</strong>。虽然很多系统编程中（尤其是使用 ASM、C 和 C++ 编程），程序员会主动利用未定义行为来达到一些目的，但这依然应该被认为是 “利用 Bug 实现特性” 的行为。</p><p>对于编译器来说，它永远假设程序中没有未定义行为。换句话说，编译器的编译行为，并不会考虑或处理未定义行为程序的输入。如果程序员意外写出了未定义行为，那编译器可能会忽略，也可能会因此生成错误的目标代码。</p><p>一个看似现在运行良好的程序，如果它带有未定义行为，很可能在将来更新编译器版本、或迁移运行平台、或升级其中某个依赖库时，将未定义行为的隐患激活。所以才说，它依然是一种 Bug。</p><p>一个注重安全的软件，应该避免出现未定义行为。</p><h2 id="几个相关的概念"><a href="#几个相关的概念" class="headerlink" title="几个相关的概念"></a>几个相关的概念</h2><p>除了未定义行为，还有几个类似的名词，有些人可能会搞混。</p><h3 id="未定义行为（Undefined-Behavior）"><a href="#未定义行为（Undefined-Behavior）" class="headerlink" title="未定义行为（Undefined Behavior）"></a>未定义行为（Undefined Behavior）</h3><p>程序不一定正确或错误，无法预测，但有潜在隐患。</p><p>编译器默认忽略并假设输入不含未定义行为。</p><p>比如说，内存越界访问是一种未定义行为，它可能不会异常，也可能某次运行时出现奇怪的现象。</p><h3 id="未指定行为（Unspecified-Behavior）"><a href="#未指定行为（Unspecified-Behavior）" class="headerlink" title="未指定行为（Unspecified Behavior）"></a>未指定行为（Unspecified Behavior）</h3><p>同一个程序，不同编译器的实现不固定。或者说，一种实现可能有多种不同的行为，这些行为都是符合标准的。</p><p>编译器会选择其中某一种实现来处理未指定行为。</p><p>如果程序员忽略了未指定行为，却写出了触发它的代码，那可能会带来意外的结果。</p><p>比如说，C++ 中函数参数的求值顺序是一种未指定行为，所以如果代码行为依赖参数求值顺序，可能出现不符合设计的结果。</p><h3 id="实现定义的行为（Implementation-Defined-Behavior）"><a href="#实现定义的行为（Implementation-Defined-Behavior）" class="headerlink" title="实现定义的行为（Implementation-Defined Behavior）"></a>实现定义的行为（Implementation-Defined Behavior）</h3><p>不常被提到。和未指定行为类似，不同编译器的实现可能不同，但区别在于，它不是标准定义的。</p><p>对于这种编译器自行决定的编译行为差异，编译器需要给出使用说明。</p><p>C++ 中，明确允许编译器实现自行决定如何操作的一些行为，而不强制统一，比如说，<code>char</code> 是有符号还是无符号，整形数据溢出时的行为等。</p><p>这么做是允许编译器针对不同硬件架构做优化，以及兼容历史遗留代码。和未定义行为的区别是，它和未指定行为，不被认为是 Bug，而是编程语言的一部分，由语言标准或编译器说明来规范。</p><p>本文先讨论未定义行为。</p><h2 id="几种常见的未定义行为"><a href="#几种常见的未定义行为" class="headerlink" title="几种常见的未定义行为"></a>几种常见的未定义行为</h2><h3 id="有符号整数溢出"><a href="#有符号整数溢出" class="headerlink" title="有符号整数溢出"></a>有符号整数溢出</h3><p>在 C++ 中，无符号整数溢出<strong>不是</strong>未定义行为，它的溢出行为由编译器自行决定；但有符号整数溢出，是未定义行为。</p><p>比如：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">int32_t</span> val = INT_MAX + <span class="number">1</span>;</span><br><span class="line">std::cout &lt;&lt; val &lt;&lt; std::endl;    <span class="comment">// print: -2147483648</span></span><br><span class="line"></span><br><span class="line"><span class="type">uint32_t</span> val = UINT_MAX + <span class="number">1</span>;</span><br><span class="line">std::cout &lt;&lt; val &lt;&lt; std::endl;    <span class="comment">// print: 0</span></span><br></pre></td></tr></table></figure><p>注释中是实际运行（Linux 5.15 64-bit， gcc 11) 打印的结果。</p><p>看起来，这个执行结果是正确的，有符号从最大值溢出 1 后，得到了最小值。但这依然是未定义行为。</p><p>因为从数学上，有符号的整数溢出是没有被准确定义的，它可以是自然环绕（如上例），也可以是饱和、抛出错误或 0。</p><p>这可能带来意外的编译器优化，比如：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">0</span>; i &lt;= INT_MAX; i++) &#123;</span><br><span class="line">  <span class="comment">// loop body</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>编译器可能认为 <code>i</code> 永远不会达到比 <code>INT_MAX</code> 更大的数，从而认为循环体永远会执行下去。所以我们建议，这种代码中，将 <code>i</code> 的类型改为 <code>unsigned int</code> 等无符号数。</p><p>虽然在标准中，有符号整型溢出是未定义行为，不过大多数编译器，在未开启激进优化时，会选择用自然环绕实现溢出。</p><p>如果不想刻意处理有符号数溢出的行为，使用数学库的 API 是一个推荐的实践。</p><p>类似的，整数位移溢出可以看作是对整数乘 2 或除 2 的结果溢出。它也是未定义行为。</p><h3 id="访问未初始化内存"><a href="#访问未初始化内存" class="headerlink" title="访问未初始化内存"></a>访问未初始化内存</h3><p>访问一个未赋初值的内存处的数据，是未定义行为，这个显而易见。</p><p>不过，有一个 C++ 初始化内存的坑，容易踩到这个 UB：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">struct</span> <span class="title class_">Custom</span> &#123; <span class="type">int</span> val; &#125;;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">int</span> <span class="title">main</span><span class="params">()</span> </span>&#123;</span><br><span class="line">  <span class="function">std::unique_ptr&lt;Custom&gt; <span class="title">obj1</span><span class="params">(<span class="keyword">new</span> Custom())</span></span>; </span><br><span class="line">  <span class="function">std::unique_ptr&lt;Custom&gt; <span class="title">obj2</span><span class="params">(<span class="keyword">new</span> Custom)</span></span>;</span><br><span class="line">  </span><br><span class="line">  obj1-&gt;val;    <span class="comment">// fine</span></span><br><span class="line">  obj2-&gt;val;    <span class="comment">// undefined behavior</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>类型 <code>Custom</code> 只有一个 POD 成员，没有其他任何成员函数和复杂的成员。对于第二个对象 <code>obj2</code>，构造后的 <code>val</code> 值不会被初始化，而对于 <code>obj1</code>，<code>val</code> 值会被初始化为 0。</p><p>对于只带有 POD 成员的类型，使用不带括号的 <code>new</code> 关键字创建的对象，其成员不会被默认初始化，从而访问时可能导致未定义行为。</p><h3 id="严格别名"><a href="#严格别名" class="headerlink" title="严格别名"></a>严格别名</h3><p>这个概念（Strict Aliasing），是一个 C++ 标准中的规则，它规定了哪些类型的指针可以合法的指向某个内存位置，而不会导致未定义行为。</p><p>编译器实现中有一个过程叫别名分析，它的一个作用就是对这些指针类型所指向的实际数据做分析，从而方便后续的优化和变换。多个不同的指针可能指向同一个内存位置，但意外的使用也可能导致未定义行为。</p><p>常见的合法别名用法有：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// access member</span></span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">Custom</span> &#123; <span class="type">int</span> x; &#125;;</span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">Custom</span>* ptr;</span><br><span class="line"><span class="type">int</span>* p = &amp;ptr-&gt;x;    <span class="comment">// fine</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// char-like pointer</span></span><br><span class="line"><span class="type">char</span>* c_ptr = <span class="built_in">reinterpret_cast</span>&lt;<span class="type">char</span>*&gt;(p);    <span class="comment">// fine</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// cv type</span></span><br><span class="line"><span class="type">const</span> <span class="type">int</span>* const_ptr = p;    <span class="comment">// fine</span></span><br><span class="line"><span class="keyword">volatile</span> <span class="type">int</span>* volatile_ptr = p;    <span class="comment">// fine</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// symbol value</span></span><br><span class="line"><span class="type">unsigned</span> <span class="type">int</span>* ui_ptr = p;    <span class="comment">// fine</span></span><br></pre></td></tr></table></figure><p>也有一些是未定义行为，比如：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">int</span> i = <span class="number">42</span>;</span><br><span class="line"><span class="type">float</span>* fptr = <span class="built_in">reinterpret_cast</span>&lt;<span class="type">float</span>*&gt;(&amp;i);    <span class="comment">// undefined behavior</span></span><br><span class="line"><span class="type">float</span> f = *fptr;</span><br></pre></td></tr></table></figure><p>编译器不会假设 <code>fptr</code> 和 <code>i</code> 有什么直接关系，很可能会产生非预期的结果。</p><p>比如说：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">void</span> <span class="title">transform</span><span class="params">(<span class="type">int</span>* arr, <span class="type">float</span>* output, <span class="type">size_t</span> n)</span> </span>&#123;</span><br><span class="line">  <span class="keyword">for</span> (<span class="type">size_t</span> i = <span class="number">0</span>; i &lt; n<span class="number">-1</span>; i++) &#123;</span><br><span class="line">    output[i] = <span class="built_in">static_cast</span>&lt;<span class="type">float</span>&gt;(arr[i<span class="number">+1</span>]) + <span class="number">1</span>;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">int</span> <span class="title">main</span><span class="params">()</span> </span>&#123;</span><br><span class="line">  <span class="type">int</span> data[<span class="number">100</span>];</span><br><span class="line">  <span class="built_in">transform</span>(data, <span class="built_in">reinterpret_cast</span>&lt;<span class="type">float</span>*&gt;(data), <span class="number">100</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>当发生激进优化时，可能做向量化处理或整体预取指令，导致最终结果错误。</p><h3 id="唯一定义原则"><a href="#唯一定义原则" class="headerlink" title="唯一定义原则"></a>唯一定义原则</h3><p>先介绍下这个概念（One Definition Rule）。C++ 中明确约定，对于一个符号，必须有唯一的一个定义。</p><p>如果同一个编译单元中，有多个同名的定义，那么编译会报错。</p><p>如果不同编译单元中，有多个同名的定义，那么链接时会报告符号重复定义。</p><p>这些都可以由编译器检查出来，但有一个例外。对于模板函数，C++ 允许同名模板函数，有多份定义。</p><p>编译器处理这种情况的思路是，在编译阶段，对由模板函数生成的符号增加 <code>.weak</code> 标记，也就是弱符号。在链接阶段，对于同名的弱符号，随机选择一个（通常是第一个遇到的），保留到目标文件，而不会报错。</p><p>如果多个定义的模板同名函数，他们的实现是一样的，那不会有什么问题，但如果不一样，则大概率遇到意外情况。比如：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// file1.cpp</span></span><br><span class="line"><span class="keyword">template</span> &lt;<span class="keyword">typename</span> T&gt;</span><br><span class="line"><span class="function">T <span class="title">process</span><span class="params">()</span> </span>&#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="number">2</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">int</span> <span class="title">foo</span><span class="params">()</span> </span>&#123;</span><br><span class="line">  std::cout &lt;&lt; <span class="built_in">process</span>&lt;<span class="type">int</span>&gt;() &lt;&lt; std::endl;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// file2.cpp</span></span><br><span class="line"><span class="keyword">template</span> &lt;<span class="keyword">typename</span> T&gt;</span><br><span class="line"><span class="function">T <span class="title">process</span><span class="params">()</span> </span>&#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="number">10</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">int</span> <span class="title">bar</span><span class="params">()</span> </span>&#123;</span><br><span class="line">  std::cout &lt;&lt; <span class="built_in">process</span>&lt;<span class="type">int</span>&gt;() &lt;&lt; std::endl;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// main.cpp</span></span><br><span class="line"><span class="function"><span class="type">int</span> <span class="title">main</span><span class="params">()</span> </span>&#123;</span><br><span class="line">  <span class="built_in">foo</span>();</span><br><span class="line">  <span class="built_in">bar</span>();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这个例子中，当使用 <code>-O0</code> 编译时，打印出两个 <code>2</code> 或两个 <code>10</code>，但我们预期的是一个 <code>2</code> 和一个 <code>10</code>。而使用 <code>-O2</code> 编译时，能打印出一个 <code>2</code> 和一个 <code>10</code>。</p><p>这是因为，当开启优化时，编译器会对简单的函数做内联操作，从而在编译阶段，就消除了符号 <code>process</code>，自然能得到正确的结果。不开启优化时，链接时会仅保留其中一个 <code>process</code> 的实现。</p><h3 id="线程模型的激进优化"><a href="#线程模型的激进优化" class="headerlink" title="线程模型的激进优化"></a>线程模型的激进优化</h3><p>并发编程中有很多未定义行为，其中，对程序重排序可能带来未定义行为：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">int</span> x = <span class="number">0</span>, y = <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">auto</span> fun = [&amp;] &#123; </span><br><span class="line">  x = <span class="number">1</span>;</span><br><span class="line">  y = <span class="number">2</span>;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="keyword">auto</span> observer = [&amp;] &#123;</span><br><span class="line">  <span class="keyword">if</span> (y == <span class="number">2</span>) &#123;</span><br><span class="line">    <span class="built_in">assert</span>(x == <span class="number">1</span>);</span><br><span class="line">  &#125;</span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="function">std::thread <span class="title">t1</span><span class="params">(fun)</span></span>;</span><br><span class="line"></span><br><span class="line"><span class="function">std::thread <span class="title">t2</span><span class="params">(observer)</span></span>;</span><br></pre></td></tr></table></figure><p>编译器可能假设，当 <code>y</code> 为 2 时，<code>x</code> 一定是 1，从而对 <code>observer</code> 中的代码做激进的优化。</p><p>然而，在没有一致性同步操作时，这个假设可能是错的。<code>x = 1</code> 和 <code>y = 2</code> 在编译器调度或硬件指令重排序时，完全可以交换顺序。</p><h2 id="为什么允许未定义行为"><a href="#为什么允许未定义行为" class="headerlink" title="为什么允许未定义行为"></a>为什么允许未定义行为</h2><p>原因是为了追求兼容性、灵活性和高性能实现。</p><p>以 C++ 为例。C++ 的核心目标是零成本抽象，是运行性能最优，它为了达到这个目标，选择信任程序员。从而，编译器也会信任程序员，无论程序员是意外使用了未定义行为，还是故意使用未定义行为来达到某些目的。</p><p>这些目的包括但不限于：</p><ul><li>向后兼容性：过去大量的代码已经这么用了，改变语言规则会破坏旧代码</li><li>零成本抽象：任何实现都只生成达成该目的所必要的代码</li><li>硬件灵活性：因为要运行在各种多样且复杂的底层硬件，只能通过未定义行为完成特定的目的</li></ul><p>未定义行为就像是一种行规，一种默认的使用约定。“用户应当知道这些行为且明白它可能带来的后果”。</p><h2 id="从-Rust-看未定义行为"><a href="#从-Rust-看未定义行为" class="headerlink" title="从 Rust 看未定义行为"></a>从 Rust 看未定义行为</h2><p>不同于 C++，Rust 在设计之初，就确定它的核心目标是系统安全。所以 Rust 语言和编译器能够从语言层面避免不安全的编码。</p><p>然而，Rust 这种设计也就限制了语言操作底层硬件和系统的灵活性，所以它不得不额外支持 <code>unsafe</code> 语法，允许在一定程度上允许出现未定义行为。</p><p>从而，我们可以换个视角，通过 Rust 的 <code>unsafe</code> 语法规则，来窥探可能存在的未定义行为。</p><p>在 <code>unsafe</code> 代码块中，Rust 允许以下 5 类不安全用法（它并未完全禁用安全检查，只是放宽限制）：</p><ol><li>解引用裸指针：在 <code>unsafe</code> 中，不关心裸指针是否为空、悬垂、未对齐或指向非法内存，这是很多未定义行为产生的根源</li><li>调用不安全的函数：释放了调用函数中存在的未定义行为</li><li>访问或修改可变静态变量：在并发环境下，可变静态变量作为一种共享资源，可能产生竞争，多线程竞争也是一种未定义行为</li><li>实现不安全的 trait：也是并发数据竞争问题，对于一些如 <code>Send</code> 和 <code>Sync</code> 的特征，跨线程可能带来数据安全问题</li><li>访问 union 的字段：严格别名约束的一种体现。union 有不同的类型，访问不同字段也就意味着可能通过不同类型的指针访问到同一块内存，自然可能引入未定义行为</li></ol><p>另外还有一些没有直接对应到规则的未定义行为被释放：</p><p>比如，内存模型违规：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">unsafe &#123;</span><br><span class="line">  use std::sync::atomic::&#123;AtomicBool, Ordering&#125;;</span><br><span class="line">  let flag = AtomicBool::<span class="built_in">new</span>(<span class="literal">false</span>);</span><br><span class="line">  flag.<span class="built_in">store</span>(<span class="literal">true</span>, Ordering::Relaxed);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>其中 <code>Ordering::Relaxed</code> 可能导致多个不同线程看到的 <code>flag</code> 状态不同。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>千万不要小看了每一个未定义行为，我上边列举的几个包括示例代码，都是简化后的结果。实际上，在工程实践中，这样的未定义行为藏在非常隐蔽的地方，调试排查很困难。</p><p>最好的策略就是始终保持对它的敏感性，并避免程序中出现任何未定义行为。</p><hr><div class="note info flat"><p>本文同步发布在知乎账号下：<a href="https://zhuanlan.zhihu.com/p/1989299344524994397">编译器视角下的未定义行为</a></p></div>]]></content>
    
    
      
      
    <summary type="html">&lt;link rel=&quot;stylesheet&quot; class=&quot;aplayer-secondary-style-marker&quot; href=&quot;/assets/css/APlayer.min.css&quot;&gt;&lt;script src=&quot;/assets/js/APlayer.min.js&quot; cla</summary>
      
    
    
    
    <category term="软件开发" scheme="https://p2tree.top/categories/%E8%BD%AF%E4%BB%B6%E5%BC%80%E5%8F%91/"/>
    
    
    <category term="CPP" scheme="https://p2tree.top/tags/CPP/"/>
    
    <category term="软件工程" scheme="https://p2tree.top/tags/%E8%BD%AF%E4%BB%B6%E5%B7%A5%E7%A8%8B/"/>
    
    <category term="编译器" scheme="https://p2tree.top/tags/%E7%BC%96%E8%AF%91%E5%99%A8/"/>
    
  </entry>
  
  <entry>
    <title>Chapter.57</title>
    <link href="https://p2tree.top/posts/a99d54c9.html"/>
    <id>https://p2tree.top/posts/a99d54c9.html</id>
    <published>2025-12-23T21:56:03.000Z</published>
    <updated>2025-12-23T14:19:41.000Z</updated>
    
    <content type="html"><![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/assets/css/APlayer.min.css"><script src="/assets/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><p>最近网上讨论 ChatGPT 的风潮特别旺盛，其中一个热点话题是 chatgpt 的答案准确性，说看似回答的有理有据，实际上可能其训练数据并不准确，从而导致它的回答是不准确的。这个观点我认同，但我还认为没必要只关注 chatgpt 的答案准确性，也要关注网络信息本身的信息准确性，而网络已经在我们身边存在几十年了。</p><p>过去，我们获取信息靠的是书本、论文、前辈的经验。现在，我们越来越倾向于靠网络搜索，但我们无法准确预知这些信息的正确性，但单纯的从信息来源的审查角度来看，书籍论文的审查制度和网络文章的发布约定相比，要严格的多，一定程度上，前者的信息可靠性一定更好。</p><p>看到一个观点：“我们在接收信息时，一定牢记：<strong>事实 &gt; 观点，框架和逻辑 &gt; 结论</strong>。” 遇到有质量的长文时，不要急功近利的去读它的简化版、缩水版，尤其是那种吸引流量的标题党，这种信息最容易断章取义、添油加醋，让有效的信息失真。</p><p>回到开始的话题。我们不要只看到 ChatGPT 这种新人工智能中出现的信息失准问题，也要努力去看到现在网络世界中的信息失准问题，归根结底，不要轻信没有依据的观点，培养主动探索问题本源的能力，才是我们真正要关心的事情。</p><hr><p><img src="https://img.p2tree.top/2025/12/23/chapter-57.webp" alt="Chapter.57"></p><hr><div class="note primary flat"><p>转载自我自己的微信公众号：<a href="https://mp.weixin.qq.com/s/J5HLdhnreSj9cL4_3yp30g">目的地</a>，欢迎关注。</p></div>]]></content>
    
    
    <summary type="html">网络世界的信息失准</summary>
    
    
    
    <category term="生活感悟" scheme="https://p2tree.top/categories/%E7%94%9F%E6%B4%BB%E6%84%9F%E6%82%9F/"/>
    
    
    <category term="网络安全" scheme="https://p2tree.top/tags/%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8/"/>
    
  </entry>
  
  <entry>
    <title>计算机体系结构模拟器简述</title>
    <link href="https://p2tree.top/posts/a866e07b.html"/>
    <id>https://p2tree.top/posts/a866e07b.html</id>
    <published>2025-12-05T22:46:39.000Z</published>
    <updated>2025-12-05T14:52:19.000Z</updated>
    
    <content type="html"><![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/assets/css/APlayer.min.css"><script src="/assets/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><blockquote><p>在芯片尚未流片，硬件还停留在前端设计时，芯片公司里那些软件开发工程师，已经在一个虚拟的环境上运行起了程序和算法。这个神奇的虚拟环境，就是计算机体系结构模拟器。</p></blockquote><p>前几年，苹果公司在着手开发自研 M 系列处理器时，他们的软件工程师们，并不需要等到自家的芯片从工厂生产出来寄到公司，就能够让新的 MacOS 系统在新硬件上运行起来；而 Intel 的工程师们，他们在针对不同硬件上设计缓存算法时，并不需要机房里运行各种型号的处理器。</p><p>这就是计算机体系结构模拟器的魅力。它是一个桥梁，连接着软件设计和硬件设计，使得芯片公司可以让软件团队和硬件团队一起开始在新项目上发力，也可以让架构师们在构想的硬件模型上，验证和优化自己的设计和算法。</p><hr><h2 id="什么是计算机体系结构模拟器？"><a href="#什么是计算机体系结构模拟器？" class="headerlink" title="什么是计算机体系结构模拟器？"></a>什么是计算机体系结构模拟器？</h2><p>简单来说，计算机体系结构模拟器（以下简称模拟器）是一套软件，这套软件用来重建一台计算机硬件。它可以精确或近似的模拟目标硬件的执行行为，让上层程序以为自己真的运行在一个硬件上。</p><p>每一项技术的诞生都有其存在的理由，模拟器的存在用来解决在硬件开发中的几个关键痛点。</p><h3 id="时间和成本"><a href="#时间和成本" class="headerlink" title="时间和成本"></a>时间和成本</h3><p>传统的硬件开发流程是先进行硬件设计，制造原型，原型测试，流片，硬件点亮，软件研发。其中还要考虑硬件测试的各个环节出现回退的问题，所以通常一款硬件从设计到回片，可能要经历一年到一年半的时间。软件研发如果等待硬件回片后进行，那么实际产品推出的时间只能更晚。</p><p>另外，硬件流片的成本很高，动辄几百万美元，如果回片后无法点亮，发现存在系统性故障，重新流片带来的经济损失也将很高。</p><p>引入模拟器后，无论对于硬件工程师，还是软件工程师，都具有很大的帮助。硬件工程师可以先在模拟器上进行设计和初步验证，并与硬件仿真的结果做对比，最大可能避免后期生产时的回退。</p><p>而软件工程师则可以不用等硬件生产回片，而是在硬件前端定型后，就开启相关的开发工作，软件的开发和调试，可以利用模拟器作为运行环境来完成。由于模拟器足够灵活，所以实际上软件开发在模拟器上调试要比在硬件上调试也更为便捷。</p><h3 id="超能力"><a href="#超能力" class="headerlink" title="超能力"></a>超能力</h3><p>实际的芯片，对于软件来说，基本是一个黑盒子。软件只能看到输入和输出，除此之外唯一能依靠的就是调试接口（研发早期调试功能也不是最紧迫的）。</p><p>软件无法洞察到硬件的细节，而模拟器则提供了另一个思路。</p><p>模拟器可以任意设断点、单步运行和回退；模拟器可以打印出任意寄存器、内存、信号量、缓存等信息，甚至软件运行过程中的整个快照；如果有需要，模拟器可以任意调整硬件配置（比如缓存大小）。这些功能都并不依赖于调试工具就可以实现。</p><p>另外，模拟器对于一些软件导致的随机问题，可能更容易复现，在一些极端测试下，也更容易发现隐藏在角落的问题。这些功能，让模拟器对软件来说，就像是对硬件开启了 “透视”功能，极大方便软件的开发工作。</p><h3 id="风险控制"><a href="#风险控制" class="headerlink" title="风险控制"></a>风险控制</h3><p>实际上，现在很多云服务，提供的都是虚拟设备，也就是模拟器的一种，它们将软件服务和硬件设备进一步隔离开，避免了软件的问题直接破坏物理设备。同时，虚拟运行也可以更方便的发现运行问题。</p><hr><h2 id="模拟器家族"><a href="#模拟器家族" class="headerlink" title="模拟器家族"></a>模拟器家族</h2><p>模拟器作为一类软件，在不同的场合下，其应用方式不同，所以做的也并不是同样的事情。根据不同的需求，工程师们开发了不同的模拟器类型，用于解决各种现实问题。</p><h3 id="按范围分类：从微观到宏观"><a href="#按范围分类：从微观到宏观" class="headerlink" title="按范围分类：从微观到宏观"></a>按范围分类：从微观到宏观</h3><p>第一种叫微体系结构模拟器。微体系结构是介于处理器体系结构和硬件设计之间的一层，它可以将硬件功能进行抽象。微体系结构模拟器则用来在这一个层面上对硬件功能做模拟。</p><p>比如说，它可以专注于模拟流水线行为、缓存行为等，这种模拟器可以计算出分支预测的准确率和缓存命中率，并完整地展现出每一次运行的通信细节。</p><blockquote><p>2000 年初，斯坦福大学的研究人员，使用 SimpleScalar 模拟器，改进了分支预测算法，这个算法后来用在了多个商业处理器上。SimpleScalar 是一个微体系结构模拟器。</p></blockquote><p>第二种叫系统模拟器。它则模拟整个计算机系统，除了处理器本身，它还负责去模拟内存、磁盘、I&#x2F;O 设备等。这类模拟器强大到其自身就可以启动一个操作系统，并与其他模拟器或硬件设备协同工作。</p><h3 id="按精度分类：速度和精度的权衡"><a href="#按精度分类：速度和精度的权衡" class="headerlink" title="按精度分类：速度和精度的权衡"></a>按精度分类：速度和精度的权衡</h3><p>模拟器设计中一个永远逃不开的话题，就是更重视速度还是精度。基于此，可以将模拟器分为：功能模拟器、周期精确模拟器和事件驱动模拟器。</p><p>功能模拟器只保证结果正确，它在一个较高的尺度下完成硬件模拟，通常在项目的早期会被使用。由于模拟精度不高，所以其运行速度相对较快，可以在 10-100 MIPS 之间。</p><p>周期精确模拟器则刚好相反，它可以模拟每一个时钟周期的行为，所以它对硬件细节的刻画最细致，可以用于性能评估、缓存优化等。由于它选择了精度，在运行速度上相对更慢，在 0.1-1 MIPS 之间。</p><p>事件驱动模拟器则刚好介于两者之间。它基于硬件事件来仿真，比如网络通信、系统互联等。它的运行速度在 1-10 MIPS 之间。</p><p>不过，无论哪种模拟器，都不可能达到硬件的运行速度（通常在上千 MIPS 甚至更高）。</p><h3 id="指令集模拟器"><a href="#指令集模拟器" class="headerlink" title="指令集模拟器"></a>指令集模拟器</h3><p>指令集模拟器是一种功能模拟器。它专注于一个重要的目标：正确模拟指令的行为。它既可以去精细地按照硬件行为去实现指令，也可以按照逻辑算法来实现指令，最终只保证指令确切的输入，可以产生确切的输出。</p><p>更细节一些，指令集模拟器的实现分为几种思路：</p><table><thead><tr><th>实现技术</th><th>工作原理</th><th>速度</th><th>灵活性</th></tr></thead><tbody><tr><td>解释执行</td><td>每读取一条指令，解析一条指令</td><td>慢</td><td>高</td></tr><tr><td>JIT</td><td>它预先将目标代码块翻译成本机代码后执行</td><td>中</td><td>中</td></tr><tr><td>虚拟化</td><td>利用 CPU 硬件虚拟化技术，来模拟目标程序</td><td>快</td><td>低</td></tr></tbody></table><p>值得一提的是，模拟器并不一定需要模拟某个硬件，也可以作为软件的一套运行时环境。比如 JVM 也是模拟器的一种。</p><p>虚拟化的灵活性最差，这是因为虚拟化需要目标程序的硬件架构，和运行模拟器的计算机架构（也就是提供虚拟化的硬件）保持一致。</p><p>即便是解释执行的方式，在指令集模拟器类型里表现效率较差，但在各种模拟器类型中，由于它模拟的精度低，反而运行效率不一定差。</p><blockquote><p>Spike 模拟器是一种指令集模拟器，他是 RISC-V 官方的参考模拟器，它提供了一套标准化的验证平台，硬件工程师用它来验证自己的实现是否符合标准，软件工程师用它来开发对应的软件。</p></blockquote><hr><h2 id="模拟器的隐藏价值"><a href="#模拟器的隐藏价值" class="headerlink" title="模拟器的隐藏价值"></a>模拟器的隐藏价值</h2><p>大多数人认为，在芯片公司，模拟器的价值在于 “在硬件回来之前就开发软件”，但这只是其中一方面。</p><p>模拟器的能力很强大，功能很灵活，所以它实际上有很多潜在的价值。</p><h3 id="暴露概率性问题"><a href="#暴露概率性问题" class="headerlink" title="暴露概率性问题"></a>暴露概率性问题</h3><p>软件上有一些可能概率性复现的问题，比如说多线程的竞争问题。在硬件上运行时，由于硬件运行速度快，反而这种竞争问题难以复现（比如跑 100 次出现一次），这对于软件开发工程师来说，就变得非常棘手，难以复现问题便难以调试问题。</p><p>模拟器的运行速度不快，再加上其实现中，物理世界的概率对逻辑的影响更小，所以同样的问题程序，再模拟器上可能更高频的复现，从而帮助软件调试。</p><h3 id="硬件错误仿真"><a href="#硬件错误仿真" class="headerlink" title="硬件错误仿真"></a>硬件错误仿真</h3><p>芯片设计并不是完美的，每一版芯片都可能带有一些 errata（硬件缺陷）。而模拟器能做的，就是屏蔽或灵活开关这些 errata，从而可以有助于重现硬件 bug，帮助工程师理解和修复这些问题。</p><p>另外，模拟器也支持注入异常，比如在特定的内存中插入异常数据，或主动发出硬件信号，这都有助于软硬件开发过程中，测试和调试一些边缘问题。</p><h3 id="控制软件质量"><a href="#控制软件质量" class="headerlink" title="控制软件质量"></a>控制软件质量</h3><p>芯片对于软件的运行相对是宽容的，比如对于一些未配置的值，未定义的行为，未初始化的内存，硬件会尽量选择静默处理，避免系统失效。而这些潜在的问题被掩盖，对于软件来说，可能不是预期的。</p><p>模拟器则完全没有这个顾虑，它可以对各种软件行为做检查，相对于硬件来说，它更像是严厉的替身，将各种软件意外的行为捕获并抛出来。</p><h3 id="性能分析"><a href="#性能分析" class="headerlink" title="性能分析"></a>性能分析</h3><p>虽然现在的芯片，也会提供一定的性能分析能力，但能做的依然比较有限。</p><p>硬件可能能够提供一定程度的采样、总体的缓存统计以及有限的内存访问分析，而这些硬件机制还需要基础软件去配合来发挥其能力。</p><p>而模拟器则可以灵活地提供这些功能，它可以捕获每一条指令的运行状态，对仿真组件的影响，每一次访存动作、缓存命中情况等。进而，对于性能分析的工作提供有利的参考。</p><p>虽然模拟器不能提供准确的绝对数据，但提供迭代改进的相对数据和横向对比数据，也非常有价值。</p><h3 id="前期交付"><a href="#前期交付" class="headerlink" title="前期交付"></a>前期交付</h3><p>对于一些公司，它们需要在硬件发售之前，先将软件交付给客户，为了能保证用户使用软件时正常运行调试，就需要一并把模拟器也发布出去。可能有不同的形式，比如交付二进制产物，或者提供云平台接入等。</p><p>另外，即便硬件已经能够交付，一些用户也希望自己购买的软件产品中能带有模拟仿真的功能，比如 IDE（软件集成开发环境）中带有模拟运行特性，这也需要模拟器团队去接下重担。</p><hr><h2 id="局限性和发展"><a href="#局限性和发展" class="headerlink" title="局限性和发展"></a>局限性和发展</h2><p>模拟器不是万能的，它在软硬件设计这个场景下，能够发挥出自己的优势，但确实也存在一些局限性。</p><p>最主要的就是速度。如果对硬件做详细的模拟，就不得不面对测试效率越来越低的现实。这种和硬件的差距，可能达到几千上万倍，以至于对于使用者来说无法容忍。毕竟，软件输入数据的规模，也影响着一次运行的总用时，越来越复杂的软件需求和硬件设计，使得模拟器夹在中间，迎来更多的质疑。模拟器开发工作中确实挺让人头大（笑哭）。</p><p>其次是准确性的问题。模拟器毕竟只是一套软件，它无法完全复刻硬件的行为，模拟器开发工作中的绝大多数维护时间，都用来去与硬件做参照。过于准确的设计又反过来带来了速度的损失，需要不断寻求平衡。</p><p>最后是新兴硬件越来越复杂，比如异构架构，而软件的需求也越来越高。比如在一个 GPGPU 公司做模拟器，来运行行业上各种大模型和算法。这对模拟器带来了新的机遇和挑战。</p><hr><h2 id="常见模拟器软件"><a href="#常见模拟器软件" class="headerlink" title="常见模拟器软件"></a>常见模拟器软件</h2><table><thead><tr><th>工具</th><th>类型</th><th>特点</th><th>应用场景</th></tr></thead><tbody><tr><td>QEMU</td><td>系统模拟器</td><td>支持丰富的硬件架构，社区活跃</td><td>虚拟化、嵌入式、操作系统移植</td></tr><tr><td>Gem5</td><td>微架构模拟器&#x2F;全系统模拟器</td><td>高自由度、研究导向</td><td>计算机架构研究、性能分析</td></tr><tr><td>spike</td><td>RISC-V 指令集模拟器</td><td>官方模拟器，简单易用</td><td>生态软件开发、教学</td></tr><tr><td>Bochs</td><td>x86 系统模拟器</td><td>移植性强</td><td>操作系统开发</td></tr><tr><td>LLVM simulator</td><td>指令集模拟器</td><td>基于 LLVM</td><td>编译器测试</td></tr><tr><td>SPIM</td><td>MIPS 指令集模拟器</td><td>简单易用</td><td>教学</td></tr></tbody></table><hr><p>计算机体系结构模拟器，这个隐藏在芯片设计和软件设计背后的强劲工具，是软硬件快速发展的翅膀。虽然大多数人并不会注意到这个领域和这些工程师，但它们实实在在的在默默支持着一线。</p><hr><div class="note info flat"><p>本文同步发布在知乎账号下：<a href="https://zhuanlan.zhihu.com/p/1980319520318063227">计算机体系结构模拟器简述</a></p><p>标题图片使用豆包 AI 生成，提示词：冬天、白雪皑皑的冰川、毛毡画、16:9 画幅。</p></div>]]></content>
    
    
      
      
    <summary type="html">&lt;link rel=&quot;stylesheet&quot; class=&quot;aplayer-secondary-style-marker&quot; href=&quot;/assets/css/APlayer.min.css&quot;&gt;&lt;script src=&quot;/assets/js/APlayer.min.js&quot; cla</summary>
      
    
    
    
    <category term="软件工具" scheme="https://p2tree.top/categories/%E8%BD%AF%E4%BB%B6%E5%B7%A5%E5%85%B7/"/>
    
    
    <category term="编译器" scheme="https://p2tree.top/tags/%E7%BC%96%E8%AF%91%E5%99%A8/"/>
    
    <category term="软件工具" scheme="https://p2tree.top/tags/%E8%BD%AF%E4%BB%B6%E5%B7%A5%E5%85%B7/"/>
    
  </entry>
  
  <entry>
    <title>Chapter.1071</title>
    <link href="https://p2tree.top/posts/7c136354.html"/>
    <id>https://p2tree.top/posts/7c136354.html</id>
    <published>2025-12-02T21:40:19.000Z</published>
    <updated>2025-12-23T14:19:41.000Z</updated>
    
    <content type="html"><![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/assets/css/APlayer.min.css"><script src="/assets/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><p>中医并不代表着落后，西医也不总代表着进步，这是一个特别大的误区，并且很多人都这么认为。落后的是传统中医和西医，进步的是现代中医和西医。曾经的西医，也需要尝草药，抹药酒，放血疗法；现代的中医，也会用设备监控身体状况，用仪器分析药理成分。</p><p><em><strong>我们要在科学的背景下选择性的看待中医和西医。</strong></em></p><p><img src="https://img.p2tree.top/2025/12/2/Chapter.1071.webp" alt="Chapter.1071"></p>]]></content>
    
    
    <summary type="html">理性看待中医和西医的关系</summary>
    
    
    
    <category term="生活感悟" scheme="https://p2tree.top/categories/%E7%94%9F%E6%B4%BB%E6%84%9F%E6%82%9F/"/>
    
    
    <category term="医学" scheme="https://p2tree.top/tags/%E5%8C%BB%E5%AD%A6/"/>
    
  </entry>
  
  <entry>
    <title>桃花源记</title>
    <link href="https://p2tree.top/posts/5f95e365.html"/>
    <id>https://p2tree.top/posts/5f95e365.html</id>
    <published>2025-10-28T21:10:28.000Z</published>
    <updated>2025-12-02T13:49:25.000Z</updated>
    
    <content type="html"><![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/assets/css/APlayer.min.css"><script src="/assets/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><p>晋太元中，武陵人捕鱼为业。缘溪行，忘路之远近。忽逢桃花林，夹岸数百步，中无杂树，芳草鲜美，落英缤纷。渔人甚异之。复前行，欲穷其林。</p><p>林尽水源，便得一山，山有小口，仿佛若有光。便舍船，从口入。初极狭，才通人。复行数十步，豁然开朗。土地平旷，屋舍俨然，有良田美池桑竹之属。阡陌交通，鸡犬相闻。其中往来种作，男女衣着，悉如外人。黄发垂髫，并怡然自乐。</p><p>见渔人，乃大惊，问所从来。具答之。便要还家，设酒杀鸡作食。村中闻有此人，咸来问讯。自云先世避秦时乱，率妻子邑人来此绝境，不复出焉，遂与外人间隔。问今是何世，乃不知有汉，无论魏晋。此人一一为具言所闻，皆叹惋。余人各复延至其家，皆出酒食。停数日，辞去。此中人语云：“不足为外人道也。”</p><p>既出，得其船，便扶向路，处处志之。及郡下，诣太守，说如此。太守即遣精卒随其往，携刀循所志往。</p><p>至山口入，村人见甲士，皆持耒耜拒之。喝令降，不从。遂纵火焚屋，相攻竟日。男丁死者相藉，妇孺多掳，鲜血漫浸林麓，沾濡桃花。</p><p>事毕，召渔人，予金帛胁之缄口，令毁途志，对外云：“寻向所志，遂迷而不得路”。</p><p>南阳刘子骥，高尚士也，闻之，欣然规往。桃林尚在，然溪间浮尸残木，岸侧焦垣断壁，腥气绕林。枝头桃花，尽染赤黑，风过簌簌，如泣如诉。后每每梦入桃林，满树黑花，触之粘腻如血，惊起汗透重衾。未几，寻病终。后遂无敢问津者矣。</p><hr><p>本文后半部分为我的杜撰，感谢你愿意读完。</p><p>原文末尾为：</p><blockquote><p>既出，得其船，便扶向路，处处志之。及郡下，诣太守，说如此。太守即遣人随其往，寻向所志，遂迷，不复得路。</p><p>南阳刘子骥，高尚士也，闻之，欣然规往。未果，寻病终。后遂无问津者。</p></blockquote><p>《桃花源记》是陶渊明的一篇著名文章，事实上桃花源是否在历史上真实存在，无从考证，由于陶渊明是一个理想主义者，所以他大概率会编写这样一个乌托邦的世界，来寄托自己的思绪。</p><p>不过，如果假设桃花源真实存在过，那么更现实主义一些猜测，结果很可能是我上边杜撰的结尾。渔人擅长寻路，而且寻向所志，不太可能找不到桃花源，更不可能迷路。另外，陶渊明所处的东晋末年，战乱不断，官府急需兵卒和粮食，发现桃花源的结果一定是管理和征税，而来自秦末就与世隔绝的桃源人，是断然不可能服从的。</p><p>这也就更容易解释，刘子骥为什么因为没找到桃源，就因为寻找而得病甚至病死。显然，看到梦想中美好的桃源是如此惨状，更容易引发精神压力和疾病。而后人无问津者，不是不再好奇，更可能是不敢问。</p><p>这个桃源二创的点子，来自于早期高分国产剧集《毛骗》，推荐观看。</p>]]></content>
    
    
      
      
    <summary type="html">&lt;link rel=&quot;stylesheet&quot; class=&quot;aplayer-secondary-style-marker&quot; href=&quot;/assets/css/APlayer.min.css&quot;&gt;&lt;script src=&quot;/assets/js/APlayer.min.js&quot; cla</summary>
      
    
    
    
    <category term="文学" scheme="https://p2tree.top/categories/%E6%96%87%E5%AD%A6/"/>
    
    
    <category term="诗" scheme="https://p2tree.top/tags/%E8%AF%97/"/>
    
  </entry>
  
  <entry>
    <title>Effective Modern C++：微调</title>
    <link href="https://p2tree.top/posts/49749d08.html"/>
    <id>https://p2tree.top/posts/49749d08.html</id>
    <published>2025-10-27T22:53:20.000Z</published>
    <updated>2025-10-27T14:59:35.000Z</updated>
    
    <content type="html"><![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/assets/css/APlayer.min.css"><script src="/assets/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><p>这一章有两个对更具体话题的讨论，虽然不适于普适的应用场景，但通过阅读学习，也能一定程度上加深对 C++ 编程语言的理解。作者发挥了他一贯的先假设再否定的行文风格，如果先验知识不足，很可能跟不上思路。</p><h2 id="条款-41：对于可以复制的形参，在移动成本低并且一定会被复制的前提下，可以考虑按值传递"><a href="#条款-41：对于可以复制的形参，在移动成本低并且一定会被复制的前提下，可以考虑按值传递" class="headerlink" title="条款 41：对于可以复制的形参，在移动成本低并且一定会被复制的前提下，可以考虑按值传递"></a>条款 41：对于可以复制的形参，在移动成本低并且一定会被复制的前提下，可以考虑按值传递</h2><p>条款标题很长，因为这个条款的前置条件很多，只有在这些前置条件都满足的情况下，才可以考虑将一个形参按值来传递。</p><p>阅读下文之前，需要已经明确搞清楚以下几个概念：</p><ul><li>移动和复制</li><li>复制构造和复制赋值</li><li>左值和右值</li><li>左值引用和右值引用</li></ul><p>和前边一样，我不会按原文的行文顺序去复述，而是按我理解地最容易被读者接受的顺序来介绍。</p><p>首先我们梳理下条款要说明的内容。我们知道，函数形参的传递方式，可以分为按引用（包括按指针，这里不讨论）和按值传递。为了保证参数传递的效率，通常会建议对于用户自定义类型或者复杂的容器类型使用按引用传递，避免拷贝数据带来的开销。</p><p>本条款想提出一个建议，如果一个类型作为形参时，无论它的复制成本有多重，当同时满足以下条件时，可以按值传递：</p><ul><li>条件一：实现了复制操作（这意味着只移类型不满足）</li><li>条件二：移动操作成本很低</li><li>条件三：按值传递时，一定会被复制</li></ul><h3 id="条件二为什么是必要的？"><a href="#条件二为什么是必要的？" class="headerlink" title="条件二为什么是必要的？"></a>条件二为什么是必要的？</h3><p>下边给出要讨论的示例代码，我们需要实现一套普通成员函数：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Widget</span> &#123;</span><br><span class="line"><span class="keyword">public</span>:</span><br><span class="line">  <span class="comment">// 方案 1：通过引用传递形参，重载实现左值引用和右值引用</span></span><br><span class="line">  <span class="function"><span class="type">void</span> <span class="title">addName1</span><span class="params">(<span class="type">const</span> std::string&amp; newName)</span> </span>&#123; names.<span class="built_in">push_back</span>(newName); &#125;</span><br><span class="line">  <span class="function"><span class="type">void</span> <span class="title">addName1</span><span class="params">(std::string&amp;&amp; newName)</span> </span>&#123; names.<span class="built_in">push_back</span>(std::<span class="built_in">move</span>(newName)); &#125;</span><br><span class="line">  </span><br><span class="line">  <span class="comment">// 方案 2：通过引用传递形参，使用万能引用替换方案 1 的重载实现</span></span><br><span class="line">  <span class="keyword">template</span> &lt;<span class="keyword">typename</span> T&gt;</span><br><span class="line">  <span class="function"><span class="type">void</span> <span class="title">addName2</span><span class="params">(T&amp;&amp; newName)</span> </span>&#123; names.<span class="built_in">push_back</span>(std::forward&lt;T&gt;(newName)); &#125;</span><br><span class="line">  </span><br><span class="line">  <span class="comment">// 方案 3：按值传递形参</span></span><br><span class="line">  <span class="function"><span class="type">void</span> <span class="title">addName3</span><span class="params">(std::string newName)</span> </span>&#123; names.<span class="built_in">push_back</span>(std::<span class="built_in">move</span>(newName)); &#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">private</span>:</span><br><span class="line">  std::vector&lt;std::string&gt; names;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>示例中给出了三种实现，我们依次对这三种实现的参数传递代价做分析：</p><p>方案 1，通过引用传递参数，因为调用方的实参是通过引用传入被调用方形参，所以没有复制或移动的成本。在左值引用形参的重载版本中，引用形参 <code>newName</code> 通过复制传入 <code>names</code>；在右值引用形参的重载版本中，引用形参 <code>newName</code> 通过移动传入 <code>names</code>。</p><p>方案 1 成本合计是，对于传入左值实参，发生一次复制，对于传入右值实参，发生一次移动。</p><p>方案 2，通过万能引用传递参数，实参到形参的过程，依然没有复制或移动的成本。由于 <code>std::forward</code> 的操作，实际内部传入 <code>names</code> 时的行为，和方案 1 没有区别。</p><p>方案 2 成本合计同方案 1，对于传入左值实参，发生一次复制，对于传入右值实参，发生一次移动。</p><p>方案 3，按值传递参数，从调用方的实参到被调用方的形参过程中，无论是左值还是右值，都会进行一次构造，如果传入左值，发生一次复制构造，如果传入右值，发生一次移动构造（<code>std::string</code> 有移动构造，我们也假设本条款讨论的类型有移动构造）。在函数体，<code>newName</code> 一定会通过移动传入 <code>names</code>，因为 <code>newName</code> 作为局部变量，后边没有调用了。</p><p>方案 3 成本合计是，对于传入左值实参，发生一次复制一次移动，对于传入右值实参，发生两次移动。</p><p>这么看来，似乎方案 3 一定比方案 1 和方案 2 代价更高，无论左值还是右值，都会多一次移动操作。但回头看一下本条款的前置条件，其中包括 “移动操作成本很低”。也就是说，从上边的成本分析来看，满足条件的类型，使用方案 3 和使用 方案 1 或方案 2 的性能开销基本是相同的。</p><p>但使用方案 3 带来了什么好处呢？方案 1 中，我们需要编写多个类似的重载函数，代码更冗余，不利于维护；方案 2 虽然避免了方案 1 的问题，但使用万能引用又带来了其他问题，具体可回顾<strong>条款 26</strong>。方案 3 此时就有了用武之地。</p><h3 id="条件一为什么是必要的？"><a href="#条件一为什么是必要的？" class="headerlink" title="条件一为什么是必要的？"></a>条件一为什么是必要的？</h3><p>解释完条件二的成本代价，我们回头再分析下条件一，为什么是必要的。</p><p>对于只移形参来说，使用上文的方案 1，通过重载的方式，因为只移对象没有复制构造函数，所以我们只需要编写一个接受右值引用的形参的重载版本即可。</p><p>那么，使用方案 1 最明显的代价，也就是编写多个重载版本函数的问题，也就不存在了，反过来说，使用方案 3 针对方案 1 的优势也就不存在了。此时，方案 3 还存在着额外移动一次的成本，总体就不值得推荐了。</p><h3 id="条件三为什么是必要的？"><a href="#条件三为什么是必要的？" class="headerlink" title="条件三为什么是必要的？"></a>条件三为什么是必要的？</h3><p>这一条是表述歧义最大的一条，虽然我极力想要换一种描述方式，但并没有找到同样简略的用词，所以保留了书中翻译版本的结果（这里的 “复制” 二字是歧义的来源）。</p><p>条件三的 “一定发生复制” 是指，形参一定会被用于移动到另一个容器中，也就是上例中，<code>newName</code> 一定会被移动到 <code>names</code> 容器中。</p><p>书中给出的违反条件三的例子是，假设在将 <code>newName</code> 移动到 <code>names</code> 之前，先判断 <code>newName</code> 的长度，只选择一个范围内长度的 <code>newName</code> 才执行移动：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">void</span> <span class="title">addName3</span><span class="params">(std::string newName)</span> </span>&#123;</span><br><span class="line">  <span class="keyword">if</span> (newName.<span class="built_in">length</span>() &gt;= minLen) &amp;&amp; newName.<span class="built_in">length</span>() &lt;= maxLen)) &#123;</span><br><span class="line">    names.<span class="built_in">push_back</span>(std::<span class="built_in">move</span>(newName));</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>当一个输入参数，长度不满足 <code>[minLen, maxLen]</code> 区间，那么就不会执行移动到 <code>names</code> 的操作。此时，如果换成引用传递参数，则不会带来任何开销，但使用按值传递参数，则仍然有构造的开销，从而放大了按值传递的缺点，不值得使用。</p><p>总结一下：</p><table><thead><tr><th>阶段</th><th>按引用传递，重载函数</th><th>按引用传递，万能引用</th><th>按值传递</th></tr></thead><tbody><tr><td>形参构造阶段</td><td>无代价</td><td>无代价</td><td>左值一次复制构造，右值一次移动构造</td></tr><tr><td>形参传入容器</td><td>左值一次复制构造，右值一次移动构造</td><td>左值一次复制构造，右值一次移动构造</td><td>一次移动构造</td></tr></tbody></table><p>当三个条件都被满足时，使用按值传递参数，其代价才是可接受的。</p><h3 id="如果将复制构造改成赋值"><a href="#如果将复制构造改成赋值" class="headerlink" title="如果将复制构造改成赋值"></a>如果将复制构造改成赋值</h3><p>前边讨论的是，使用 <code>std::vector&lt;T&gt;::push_back</code> 来实现对形参的处理，<code>push_back</code> 内部，会直接在容器堆内存的末尾创建新元素的空间（这里不考虑扩容时重新分配内存的问题），然后把数据拷贝到新元素空间（通过复制构造或移动构造）。</p><p>但是，如果我们不使用构造的方式，而是使用赋值的方式来操作形参呢？考虑以下示例（我们这里假设实参一定是左值，省略右值的讨论，左右值实参的区别可见上文）：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Password</span> &#123;</span><br><span class="line"><span class="keyword">public</span>:</span><br><span class="line">  <span class="function"><span class="keyword">explicit</span> <span class="title">Password</span><span class="params">(std::string pwd)</span> : text(std::move(pwd)) &#123;</span>&#125;</span><br><span class="line">  <span class="comment">// 方案 1：按值传递参数</span></span><br><span class="line">  <span class="function"><span class="type">void</span> <span class="title">change1</span><span class="params">(std::string newPwd)</span> </span>&#123; text = std::<span class="built_in">move</span>(newPwd); &#125;</span><br><span class="line">  <span class="comment">// 方案 2：按引用传递参数</span></span><br><span class="line">  <span class="function"><span class="type">void</span> <span class="title">change2</span><span class="params">(std::string&amp; newPwd)</span> </span>&#123; text = newPwd; &#125;</span><br><span class="line"><span class="keyword">private</span>:</span><br><span class="line">  std::string text;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>两个方案中，都使用了赋值的方式来使用形参，但方案 1 是按值传递参数，方案 2 是按引用传递参数。两者有什么区别？</p><p>对于方案 1，传递参数时，调用一次构造函数（左值调用复制构造，右值调用移动构造），赋值 <code>text</code> 时，调用一次移动构造；对于方案 2，传递参数时，不会发生成本，赋值 <code>text</code> 时，调用一次构造函数（左值调用复制构造，右值调用移动构造）。</p><p>然而我们要讨论的重点是进一步的话题。如果 <code>text</code> 之前已经有值，也就是 <code>std::string</code> 类型的对象，已经分配了一块堆内存来存放之前的一个旧密码。那么，如果新密码比旧密码长，会发生什么？反之如果新密码比旧密码短，会发生什么成本？这里我们不考虑 <code>std::string</code> 的小字符串优化，假设数据一定被存放在堆空间中。</p><p>对于方案 1，当新值比旧值更短时，调用 <code>change1</code>，形参 <code>newPwd</code> 通过构造函数创建，发生一次堆内存分配和一次内存拷贝。在函数体内，因为是移动赋值，通常的实现是 <code>text</code> 中指向堆内存的指针由之前的旧密码空间，改为指向新密码空间。之前旧密码空间被释放，没有额外构造成本。</p><p>当新值比旧值更长时，调用 <code>change1</code>，形参 <code>newPwd</code> 通过构造函数创建，同样发生一次堆内存分配和一次内存拷贝。在函数体内，同样直接通过移动操作，没有构造成本。</p><p>对于方案 2，当新值比旧值更短时，调用 <code>change2</code>，形参通过引用传递，没有额外构造成本。在函数体内，发生拷贝赋值，由于新值更短，直接使用原旧密码空间，不用重新分配内存。</p><p>当新值比旧值更长时，调用 <code>change2</code>，形参构造同样没有成本。函数体内，原旧密码空间不足以存放新密码，所以需要重新分配空间，然后释放旧密码空间，并使用拷贝赋值填充新密码数据。</p><p>总结一下：</p><table><thead><tr><th>阶段</th><th>按值传递，新值更短</th><th>按值传递，新值更长</th><th>按引用传递，新值更短</th><th>按引用传递，新值更长</th></tr></thead><tbody><tr><td>形参构造阶段</td><td>一次分配空间、一次拷贝构造（左值实参）</td><td>一次分配空间、一次拷贝构造（左值实参）</td><td>无代价</td><td>无代价</td></tr><tr><td>赋值阶段</td><td>无代价</td><td>无代价</td><td>一次拷贝赋值（左值实参）</td><td>一次分配空间、一次拷贝赋值（左值实参）</td></tr></tbody></table><p>很清晰地可以看出，若按值传递参数，那么分配空间是一定会发生的，无法重复利用之前旧密码已经开辟的堆空间。而按引用传递参数，当新数据比旧数据短时，可以避免重新分配空间。</p><p>这是一个潜在的性能陷阱，只有在特定的输入时，即新值比旧值更长、没有触发小字符串优化时，才会发生。</p><p>采用按赋值的方式使用形参的函数，按值传递参数的代价，取决于左值和右值的成分和类型是否要按动态分配内存。当然还涉及到赋值运算符的实现，以及输入参数对空间的需求与原始动态分配空间的大小之间的关系。对于 <code>std::string</code> 这种类型，还需要考虑优化的影响。</p><h3 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h3><p>按值传递参数带来的成本是很复杂的，本条款就是借此来展开讨论。通常情况下，惯用的做法是，优先使用重载或万能引用的方式，使用引用传递参数，除非一定要在这里去探究性能差异，才去考虑是否能满足本条款的三个条件，从而生成更有益的代码（改善引用传递参数实现的不足）。</p><p>原文末尾还有一段，讨论了按值传递参数时，如果实参是派生类类型，形参是基类类型时，传递参数带来的类型退化问题。这也是按值传递参数的一个缺点，我认为和本条款关系不大，也比较简单，不展开介绍。</p><h2 id="条款-42：灵活选择置入还是插入"><a href="#条款-42：灵活选择置入还是插入" class="headerlink" title="条款 42：灵活选择置入还是插入"></a>条款 42：灵活选择置入还是插入</h2><p>一个老生常谈的话题了。对于很多 C++ 标准容器，通常会提供 <code>emplace</code> 和 <code>insert</code> 两种操作，比如对于 <code>std::vector</code> 会提供 <code>emplace_back</code> 和 <code>push_back</code> 这两种将元素追加到容器队列结尾的操作。</p><p>本条款讨论了这两种不同的操作之间的差异，以及各自适用的场合。由于我实在没有找到更合适的 <code>emplacement</code> 的中文翻译，所以选择了中文译本的 “置入” 一词（虽然平时完全不会这么说）。</p><p>首先最基础的一点，想必大家都了解。插入类的操作，是将一个对象以移动或复制的方式，放在与该对象类型相同的容器元素类型容器中；而置入操作，是在容器的选定位置，直接构造一个该对象类型的对象，传入的是类型构造函数的参数而非类型本身。</p><p>比如说：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">std::vector&lt;std::string&gt; vec;</span><br><span class="line">vec.<span class="built_in">push_back</span>(<span class="string">&quot;abc&quot;</span>);     <span class="comment">// a</span></span><br><span class="line">vec.<span class="built_in">emplace_back</span>(<span class="string">&quot;abc&quot;</span>);  <span class="comment">// b</span></span><br></pre></td></tr></table></figure><p>a 操作为插入操作，其实等价于：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">vec.<span class="built_in">push_back</span>(std::<span class="built_in">string</span>(<span class="string">&quot;abc&quot;</span>));</span><br></pre></td></tr></table></figure><p>b 操作为置入操作。两者的区别是，a 操作会在传入实参前，做一次构造创建一个临时对象，因为是右值，所以通过移动构造传入 <code>push_back</code> 形参后，临时对象被析构。b 操作不会有这种动作，而是直接在容器末尾通过构造函数创建一个对象。</p><p>在这个例子中，使用置入要比使用插入效率更高。<code>emplace</code> 函数使用了完美转发，所以并不会带来右值参数传入时的额外成本。反过来，如果对于插入函数并不会创建临时对象的情况，使用置入函数，<strong>通常</strong>也不会有问题，置入不会带来额外成本。</p><p>不只是 <code>emplace_back</code>，任何支持 <code>push_front</code> 操作的容器，也都支持 <code>emplace_front</code>，任何支持 <code>insert</code> 操作的容器，也都支持 <code>emplace</code> 操作，任何支持 <code>insert_after</code> 操作的容器，也都支持 <code>emplace_after</code> 操作。</p><h3 id="使用置入函数的推荐前提"><a href="#使用置入函数的推荐前提" class="headerlink" title="使用置入函数的推荐前提"></a>使用置入函数的推荐前提</h3><p>虽然这么说，但上边我还是把 “通常” 加粗了。理论上来说，置入能完全取代插入，避免性能问题，但这只是理论上，如果想确定哪个更好，定量分析还是需要做基准测试。</p><p>定性讨论的话，书中提到，满足以下三条情况，使用置入操作替代插入操作是没问题的：</p><ol><li>对于非节点型的容器（比如 <code>std::vector</code>, <code>std::string</code>）。它们以构造的方式添加值（而非赋值的方式），插入会带来额外的临时对象构造，但赋值的方式添加值，临时对象无法省略；</li><li>传递的实参类型和容器所持有的类型不同。实参类型不同时，一定会调用容器类型的构造函数来创建临时对象；</li><li>容器不太可能出现重复的情况而拒绝添加值。比如不会拒绝重复元素的容器，和那些虽然拒绝重复元素，但大概率运行时不会遇到重复元素的场景；</li></ol><h3 id="置入函数在非性能方面的问题"><a href="#置入函数在非性能方面的问题" class="headerlink" title="置入函数在非性能方面的问题"></a>置入函数在非性能方面的问题</h3><p>还有两个场景，置入函数不值得推荐。</p><p>第一个场景是有关于资源管理时发生异常导致资源泄漏的问题。假设我们有一个管理智能指针的容器：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">std::list&lt;std::shared_ptr&lt;Widget&gt;&gt; ptrs;</span><br></pre></td></tr></table></figure><p>在之前<strong>条款 21</strong> 中提到，如果在指定 <code>shared_ptr</code> 智能指针时，还需要指定自定义析构器，就不能使用 <code>make_shared</code> 函数来辅助，但如果不使用 <code>make_shared</code> 函数，就可能遇到发生异常时资源泄漏的问题。比如说：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 这是自定义析构器</span></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">killWidget</span><span class="params">(Widget* w)</span></span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 以下两种实现</span></span><br><span class="line">ptrs.<span class="built_in">push_back</span>(std::<span class="built_in">shared_ptr</span>&lt;Widget&gt;(<span class="keyword">new</span> Widget, killWidget));</span><br><span class="line">ptrs.<span class="built_in">emplace_back</span>(<span class="keyword">new</span> Widget, killWidget);</span><br></pre></td></tr></table></figure><p>当在插入到容器时发生异常（比如内存不足），两种实现有什么差异呢？对于插入来说，由于会产生一个临时对象，所以 <code>new Widget</code> 的内存指针会保存在临时对象中，如果 <code>std::list::push_back</code> 插入失败，会调用这个临时对象的析构函数，从而将分配好的内存释放掉。</p><p>对于置入来说，<code>new Widget</code> 的内存指针直接通过引用方式传入 <code>emplace_back</code> 中，当容器内插入时发生异常，没有能调用的析构函数，异常直接会被抛出外边，内存指针被彻底丢失，发生内存泄漏。</p><p>在这种情况下，建议使用插入而不是置入。当然更合理的做法是正如条款 21 中所言，将 <code>new Widget</code> 的结果单独保存成一个对象，再将对象以移动的方式传入插入或置入函数中。只要资源作为栈对象被命名，那它最终会被析构。</p><p>另一个场景是有关于带有 <code>explicit</code> 修饰的构造函数类型的情况。说实话作者想的是真细。</p><p>具体来说，有一些类型定义中，有些构造函数使用了 <code>explicit</code> 来修饰，这意味着它的这种构造方式，只允许通过显示调用的方式来访问，这就暴露了插入和置入两者的区别。书中的例子很有趣：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">// 保存正则表达式的容器</span><br><span class="line">std::vector&lt;std::regex&gt; regexes;</span><br><span class="line"></span><br><span class="line">regexes.emplace_back(nullptr);    // 正常编译，但行为错误 </span><br><span class="line">regexes.push_back(nullptr);       // 编译报错</span><br></pre></td></tr></table></figure><p>编译器允许置入一个空指针，但不允许插入一个空指针。虽然，用空指针构造 <code>std::regex</code> 类型本身是没有意义的，但标准库中支持，原因是 <code>std::regex</code> 实现了一个由 <code>const char*</code> 作为形参的构造函数。本身它是为了支持将字符串形态的正则表达式构造为 <code>std::regex</code>，但却没有将 <code>nullptr</code> 排除在外（也是指针，可以传给 <code>const char*</code>）。</p><p>由于这个构造函数使用 <code>explicit</code> 来修饰，所以只能显式调用。而插入操作中，从 <code>nullptr</code> 到 <code>std::regex</code> 中间，使用了复制初始化，隐式调用了构造函数，所以编译器报错了。不过对于置入操作，它将 <code>nullptr</code> 传入 <code>emplace_back</code> 内部后直接调用了构造函数，所以编译器没有报错。</p><blockquote><p><strong>谬误</strong>：置入函数一定比插入函数更推荐使用。</p></blockquote><h2 id="结尾"><a href="#结尾" class="headerlink" title="结尾"></a>结尾</h2><p>翻开最后一页，发现是空白了，又读完了一本书。总体来说，这本书的内容很不错，对于有一定基础的 C++ 开发者来说，是一个查漏补缺的机会，我收获很大。期待 Scott Meyers 能出下一本 Effective 系列。</p><hr><p>本系列的其他文章：</p><ol class="series-items"><li><a href="/posts/9bb75fe1.html" title="Effective Modern C++ 读书笔记：类型推导">Effective Modern C++ 读书笔记：类型推导</a></li><li><a href="/posts/f3206605.html" title="Effective Modern C++ 读书笔记：auto">Effective Modern C++ 读书笔记：auto</a></li><li><a href="/posts/57a898cb.html" title="Effective Modern C++ 读书笔记：转向现代C++">Effective Modern C++ 读书笔记：转向现代C++</a></li><li><a href="/posts/ce1aec80.html" title="Effective Modern C++ 读书笔记：智能指针">Effective Modern C++ 读书笔记：智能指针</a></li><li><a href="/posts/410ab8fb.html" title="Effective Modern C++：右值引用、移动语义和完美转发">Effective Modern C++：右值引用、移动语义和完美转发</a></li><li><a href="/posts/cd605d5c.html" title="Effective Modern C++：lambda 表达式">Effective Modern C++：lambda 表达式</a></li><li><a href="/posts/745d2232.html" title="Effective Modern C++：并发 API">Effective Modern C++：并发 API</a></li><li><a href="/posts/49749d08.html" title="Effective Modern C++：微调">Effective Modern C++：微调</a></li></ol><hr><div class="note info flat"><p>本文同步发布在知乎账号下：<a href="https://zhuanlan.zhihu.com/p/1966160349238629902">https://zhuanlan.zhihu.com/p/1966160349238629902</a></p></div>]]></content>
    
    
      
      
    <summary type="html">&lt;link rel=&quot;stylesheet&quot; class=&quot;aplayer-secondary-style-marker&quot; href=&quot;/assets/css/APlayer.min.css&quot;&gt;&lt;script src=&quot;/assets/js/APlayer.min.js&quot; cla</summary>
      
    
    
    
    <category term="软件开发" scheme="https://p2tree.top/categories/%E8%BD%AF%E4%BB%B6%E5%BC%80%E5%8F%91/"/>
    
    
    <category term="CPP" scheme="https://p2tree.top/tags/CPP/"/>
    
  </entry>
  
  <entry>
    <title>Effective Modern C++：并发 API</title>
    <link href="https://p2tree.top/posts/745d2232.html"/>
    <id>https://p2tree.top/posts/745d2232.html</id>
    <published>2025-10-16T22:55:39.000Z</published>
    <updated>2025-10-16T15:01:58.000Z</updated>
    
    <content type="html"><![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/assets/css/APlayer.min.css"><script src="/assets/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><p>C++11 中，提供了强大的并发特性，使得我们在编写并发程序时，不需要再去调用操作系统提供的并发接口（如 pthread 和 Windows 线程库）。Modern C++ 的程序员，必须熟练掌握并发 API 的用法，本章节会展开讨论几个使用 C++ 并发 API 时需要考虑和注意的问题。</p><h2 id="条款-35：优先选用-std-async-替代-std-thread"><a href="#条款-35：优先选用-std-async-替代-std-thread" class="headerlink" title="条款 35：优先选用 std::async 替代 std::thread"></a>条款 35：优先选用 std::async 替代 std::thread</h2><p>如果想要以异步运行的方式启动一个程序，有两种 API 可供使用：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">int</span> <span class="title">doSomeAsyncWork</span><span class="params">()</span></span>; </span><br><span class="line"></span><br><span class="line"><span class="comment">// 直接启动一个线程 </span></span><br><span class="line"><span class="function">std::thread <span class="title">t</span><span class="params">(doSomeAsyncWork)</span></span>; </span><br><span class="line"></span><br><span class="line"><span class="comment">// 启动一个任务 </span></span><br><span class="line"><span class="keyword">auto</span> fut = std::<span class="built_in">async</span>(doSomeAsyncWork);</span><br></pre></td></tr></table></figure><p>两种方式各有不同的特点。书中介绍的什么是线程、什么是硬件线程和软件线程，这里不再重复赘述。</p><p><code>std::thread</code> 就是启动一个软件线程，交给操作系统去调用执行。简单点来看，就是以一个独立的线程去执行这个异步函数。<code>std::async</code> 创建的对象 <code>fut</code> 的类型是 <code>std::future</code>，它的底层实现，也是一个软件线程，但它能做到的更多。</p><p>首先，<code>std::async</code> 可以返回一个期值 future，它作为一个对象，可以在之后通过 <code>get()</code> 等 API 来访问这个异步任务的返回状态，包括异步函数的返回值，和运行的异常。然而 <code>std::thread</code> 很难做到，<code>std::thread</code> 可以通过线程通信的一些方式（比如共享内存）来做数据交换，而其异常也无法在线程外部捕获到。<code>std::async</code> 也可对任务的状态做更详细的控制（如控制任务延迟启动，任务生命周期和 future 对象绑定）。</p><p>操作系统对软件线程的数量是有限制的，如果启动操作限制的数量，会导致线程启动失败，<code>std::thread</code> 会抛出 <code>std::system_error</code> 异常。而 <code>std::async</code> 默认的行为是，当软件线程超出限制时，采用同步的方式去执行这个程序，从而避免抛出异常。当然，也可以传入配置，要求 <code>std::async</code> 必须以异步方式执行程序。</p><p>即使没有超出操作系统限制，当软件线程数量大于硬件线程时，操作系统为了让所有线程都有机会运行，会对 CPU 做切片，按不同的时间片去以合适地调度策略去执行这些线程。虽然这种行为很常见，但线程数量过多（比如我之前做过的一个测试程序，要起很高的并发度去运行测试用例）时，还是会很容易触及性能瓶颈，因为过于频繁的线程切换，会带来额外的负担。<code>std::async</code> 会代替你去面对这种调度和负载均衡的问题。</p><p>所以，如果使用 <code>std::thread</code> 去编写一个多线程程序，我们会很容易触及操作系统线程管理的领域，比如是否要考虑硬件线程资源的数量，操作系统中其他进程的运行状态，我们的整个程序中，需要小心谨慎地处理线程耗尽、线程调度和负载均衡。而使用 <code>std::async</code> 则得心应手很多。</p><p>当然，有几种情况，依然需要使用 <code>std::thread</code>。</p><ol><li>如果需要访问底层线程的 API，比如 pthread 和 Windows 线程库，<code>std::thread</code> 提供了这种机会，而 <code>std::async</code> 则没有；</li><li>能完全掌握一个系统的运行状态，并且要深度优化线程调用，或者做性能分析和测试时；</li><li>需要实现一些 <code>std::async</code> 没有支持的线程计数时，如手动实现高性能线程池；</li></ol><p>这些都不常见，优先选择，依然是使用 <code>std::async</code>。</p><h2 id="条款-36：注意-std-async-的启动策略"><a href="#条款-36：注意-std-async-的启动策略" class="headerlink" title="条款 36：注意 std::async 的启动策略"></a>条款 36：注意 std::async 的启动策略</h2><p>上一个条款中提到，<code>std::async</code> 并不总是会以异步的方式启动程序。这是因为，它为了保证如果线程负载超出操作系统限制之后，不要抛出异常，所以会在合适的情况下，采用同步的方式去执行程序。</p><p>如果我们的应用依赖于这种异步的假设（比如盲目地将 <code>std::thread</code> 替代为 <code>std::async</code>），那可能会在一些极端情况时出现问题。</p><p>首先，了解下 <code>std::async</code> 的启动策略，它有两种模式：</p><ul><li><code>std::launch::async</code>：这种策略要求必须以异步的方式启动程序；</li><li><code>std::launch::defered</code>：这种策略，要求在 <code>std::async</code> 返回的期值对象，调用其 <code>get</code> 或 <code>wait</code> 接口时，才启动程序，也就是同步且延迟执行的策略。</li></ul><p>可以在使用 <code>std::async</code> 时，通过这个枚举来指定启动策略：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">auto</span> fut1 = std::<span class="built_in">async</span>(std::launch::async, f); </span><br><span class="line"><span class="keyword">auto</span> fut2 = std::<span class="built_in">async</span>(std::launch::defered, f);</span><br></pre></td></tr></table></figure><p>如果什么都不指定，那就是让 <code>std::async</code> 的实现自己去有策略的选择二者之一。通常的方法是，优先选用 <code>std::launch::async</code> 异步执行程序，如果线程负载超出操作系统限制，那就选用 <code>std::launch::defered</code> 延迟执行程序。</p><p>因此，如果使用默认行为，那就不要对启动的任务做任何异步的假设，也不要假设同步执行时，程序一定在当前线程环境（和 <code>fut</code> 定义同一个线程）中执行，这是因为，<code>fut</code> 对象完全可以被移动到另一个线程中，再去调用 <code>get</code> 接口。<br>如果以这种错误的假设去设计了软件，在大多数情况下，问题也并不会暴露，只有当线程负荷很大时，才有可能出现，这导致常规的测试用例难以暴露问题。</p><blockquote><p><strong>陷阱</strong>：<code>std::async</code> 的默认启动行为，不保证任务一定会异步启动。</p></blockquote><p>遗憾的是，期值对象并没有直接的接口返回它实际的启动策略。不过，可以用另一种方式来判断：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">using</span> <span class="keyword">namespace</span> std::literals; </span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">f</span><span class="params">()</span> </span>&#123; ... &#125; </span><br><span class="line"></span><br><span class="line"><span class="keyword">auto</span> fut = std::<span class="built_in">async</span>(f); </span><br><span class="line"><span class="keyword">if</span> (fut.<span class="built_in">wait_for</span>(<span class="number">0</span>s) == std::future_status::defered) &#123; </span><br><span class="line">  <span class="comment">// 任务以推迟执行的方式启动 </span></span><br><span class="line">&#125; <span class="keyword">else</span> &#123; </span><br><span class="line">  <span class="comment">// 任务以异步的方式启动 </span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>通过 <code>wait_for</code> 或 <code>wait_until</code> 的接口，如果任务以推迟执行的方式启动，它们将一定返回 <code>std::future_status::defered</code>。</p><p>如果一定需要任务以异步的方式启动，那么在调用 <code>std::async</code> 时，必须显式指定 <code>std::launch::async</code>。书中给出了一种包装异步启动任务的函数实现，它的返回值实现值得学习一下：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">template</span> &lt;<span class="keyword">typename</span> F, <span class="keyword">typename</span>... Ts&gt; </span><br><span class="line"><span class="keyword">inline</span> std::future&lt;<span class="keyword">typename</span> std::result_of&lt;<span class="built_in">F</span>(Ts...)&gt;::type&gt; </span><br><span class="line"><span class="built_in">reallyAsync</span>(F&amp;&amp; f, Ts&amp;&amp;... params) &#123; </span><br><span class="line">  <span class="keyword">return</span> std::<span class="built_in">async</span>(std::launch::async,</span><br><span class="line">                    std::forward&lt;F&gt;(f),</span><br><span class="line">                    std::forward&lt;Ts&gt;(params)...); </span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="条款-37：如果使用-std-thread，别忘了在结束时回收"><a href="#条款-37：如果使用-std-thread，别忘了在结束时回收" class="headerlink" title="条款 37：如果使用 std::thread，别忘了在结束时回收"></a>条款 37：如果使用 std::thread，别忘了在结束时回收</h2><p>每个 <code>std::thread</code> 都有两种状态：可联结和不可联结，使用 <code>t.joinable()</code> 接口可以判断它当前的状态。本条款介绍了一个很简单的知识点，所以我不会特别大篇幅的展开。</p><p>简单来说，如果使用 <code>std::thread</code> 创建了一个线程，那么，当线程对象离开作用域前，需要手动使用 <code>t.join()</code> 或 <code>t.detach()</code> 来回收线程（使其从可联结状态变成不可联结状态）。</p><p>这里需要区分两个概念，<code>std::thread</code> 类型的对象，被称为线程对象，它是一个 C++ 中的资源句柄，它对应的资源是一个操作系统线程（也可能不对应任何线程）。线程对象遵循 C++ 约定的生命周期和析构行为，而其对应的线程，遗憾的是，并没有默认添加到线程对象的析构函数中，并自动完成析构。</p><p>究其原因，可以分情况讨论：</p><ul><li>如果线程对象的析构函数中使用 <code>join</code> 来回收线程资源。由于 <code>join</code> 是一个阻塞行为，如果线程没有及时结束，那么线程对象的析构就会阻塞；如果线程永远不会结束，那么线程对象的析构（也就是父进程）行为，也将永远阻塞。在大型项目中，这种行为会产生很难调试的性能问题。</li><li>如果析构函数中使用 <code>detach</code> 来回收线程资源，或者说，这种操作是主动分离线程对象和线程资源，确实不会阻塞父进程，然而，很可能带来更严重的恶性 bug。比如说，线程中通过引用操作着父进程中的一些临时对象（比如栈），如果父进程通过 <code>detach</code> 提前退出并销毁临时对象，那么之后，线程依然会有可能修改这块内存空间。这种问题极难调试。</li></ul><p>标准委员会意识到了这个问题，如果主动将资源回收的任务代劳，就会带来性能问题或者潜在 bug 。倒不如开放给程序员，让软件在适当的时候，灵活地根据需求，回收线程资源。为了避免程序员忘记回收资源，C++ 规定，线程对象在析构时，如果线程还处于可联结状态，就让程序执行终止。</p><p>关于什么是不可联结的线程对象，书中有一个清单，我认为不错，可以了解一下。以下几类线程对象都是不可联结的：</p><ul><li>如果构造线程对象时，没有给它一个可执行的函数，那就没有对应的底层线程</li><li>线程对象已经通过移动操作移走</li><li>已经通过 <code>join</code> 或 <code>detach</code> 操作之后的线程对象</li></ul><p>书中后边部分介绍了使用 C++ 的 RAII 特性，对线程对象做一次包装，实现类似于智能指针的资源管理类型。将线程的回收动作放在包装类的析构函数中即可。实现比较简单，就不列出代码了。</p><h2 id="条款-38：期值的析构函数中，注意多样化的联结线程情况"><a href="#条款-38：期值的析构函数中，注意多样化的联结线程情况" class="headerlink" title="条款 38：期值的析构函数中，注意多样化的联结线程情况"></a>条款 38：期值的析构函数中，注意多样化的联结线程情况</h2><p>让我们回到任务和期值的话题。</p><p>上一条款中，我们提到，<code>std::thread</code> 是一个指向实际线程资源的资源句柄，所以讨论析构和资源回收的话题时，析构 <code>std::thread</code> 线程对象，和释放线程（物理资源），是两个不同的操作。在 <code>std::async</code> 这边，也有同样的问题需要关注。</p><p>当我们使用 <code>std::async</code> 创建一个异步任务时（通过 <code>std::launch::async</code> 手动指定，或者系统自动启动异步任务而非延迟任务），系统会分配一个线程资源，并通过 <code>std::future</code> 期值对象来指向这个线程资源。我们可以通过期值对象来获取线程的返回值，这个返回值既不位于子线程的空间，也不位于启动任务一方的作用域内，而是放在堆上，它是另一块需要处理的资源。</p><p>如果 <code>std::future</code> 对象指向的是一个异步任务，那情况很简单，它一定会需要去析构这些资源。表现在运行时行为，便是 <code>join</code> 当前的线程，阻塞父进程直到子线程结束。</p><p>但是，还有一种情况，任务创建后，可以交给 <code>std::shared_future</code> 这种共享型期值，它和 <code>std::shared_ptr</code> 一样，也拥有一个控制块，控制块中存在着引用计数。多个 <code>std::shared_future</code> 对象可以指向相同的线程资源，但只有最后一个执行资源的对象（当引用计数为 0 时），才负责回收资源。</p><p>所以总结以上的情况，当启动的是一个异步任务时，期值对象的析构动作，有两种可能：</p><ol><li>不对线程资源（包括返回值的内存）做任何处理，只负责析构期值对象自身，并将控制块中的引用计数减 1。如果对应 <code>std::thread</code>，这里对待线程的行为类似隐式 <code>detach</code>；</li><li>最后一个指向资源的期值对象，负责析构行为，这包括使用隐式的 <code>join</code> 操作回收线程，以及回收共享内存；</li></ol><p>如果最后一个期值对象走到生命周期的末尾时，子线程还没有运行结束，那么期值对象的析构操作就会阻塞。上一条款已经提到，隐式 <code>join</code> 会带来隐式的性能问题。但标准委员会考虑再三，还是决定在这里妥协这个问题。</p><p>这也带来了进一步的一些现象，比如下例中的情况，期值对象自己，并不知道什么时候会发生阻塞析构：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 容器的析构操作可能会阻塞 </span></span><br><span class="line">std::vector&lt;std::future&lt;<span class="type">void</span>&gt;&gt; futs; </span><br><span class="line"></span><br><span class="line"><span class="comment">// Widget 对象的析构操作可能会阻塞 </span></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">Widget</span> &#123; <span class="keyword">private</span>: </span><br><span class="line">  std::shared_future&lt;<span class="type">double</span>&gt; fut; </span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><h3 id="使用-packaged-task-创建返回值内存的情况"><a href="#使用-packaged-task-创建返回值内存的情况" class="headerlink" title="使用 packaged_task 创建返回值内存的情况"></a>使用 packaged_task 创建返回值内存的情况</h3><p>除了使用 <code>std::async</code> 来创建任务时，会产生共享的返回值资源外，还有另一种情况，使用 <code>std::packaged_task</code> 也可以创建返回值。它相当于是对函数的一层异步封装，封装后，对象交由 <code>std::thread</code> 或 <code>std::async</code> 可以以异步方式运行（书中的描述不准确）。</p><p>当使用 <code>std::thread</code> 运行时：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">int</span> <span class="title">func</span><span class="params">()</span></span>; </span><br><span class="line"><span class="function">std::packaged_task&lt;<span class="title">int</span><span class="params">()</span>&gt; <span class="title">pt</span><span class="params">(func())</span></span>; <span class="comment">// 封装函数后以异步方式执行 </span></span><br><span class="line"></span><br><span class="line"><span class="keyword">auto</span> fut = pt.<span class="built_in">get_future</span>(); <span class="comment">// 可以取得期值 </span></span><br><span class="line"><span class="function">std::thread <span class="title">t</span><span class="params">(std::move(pt))</span></span>; <span class="comment">// 创建一个线程对象</span></span><br></pre></td></tr></table></figure><p>可以通过 <code>get_future()</code> 接口来取得 <code>std::packaged_task</code> 的期值。也可以通过它创建 <code>std::thread</code> 线程对象。明显，线程对象和线程资源是分开的，所以期值对象和线程对象其实是可以同时存在的。</p><p>这就带来一个有趣的问题。考虑我们这一条款的话题，线程资源会被谁析构？</p><p>实际上，期值对象将不需要负责去析构线程资源。讨论可能的三种情况：</p><ol><li>如果线程对象 <code>t</code> 没有任何后续操作，在 <code>t</code> 的作用域结束时，线程是可联结的，这会导致程序终止；</li><li>如果线程对象 <code>t</code> 在作用域结束前，执行 <code>join</code>，那么 <code>t</code> 将负责析构线程资源，而期值对象 <code>fut</code> 不需要在析构时隐式 <code>join</code>；</li><li>如果线程对象 <code>t</code> 在作用域结束前，执行 <code>detach</code>，线程资源将由系统回收，<code>fut</code> 仍然不需要做任何事情。</li></ol><p>总结来说，就是如果期值对象是经由 <code>std::packaged_task</code> 获取，而不是通过 <code>std::async</code> 获取时，它的析构中，不需要负责回收线程资源。</p><p>使用 <code>std::async</code> 来运行 <code>std::packaged_task</code> 包装的任务函数，没有讨论的必要，它的析构行为和前述开头的结论一致。而且，使用 <code>std::async</code> 时，也不需要 <code>std::packaged_task</code>，它自己便可以完成函数包装的功能。</p><h2 id="条款-39：在一次性事件的异步通信时，可以选用期值方案"><a href="#条款-39：在一次性事件的异步通信时，可以选用期值方案" class="headerlink" title="条款 39：在一次性事件的异步通信时，可以选用期值方案"></a>条款 39：在一次性事件的异步通信时，可以选用期值方案</h2><p>这一条款，我们开始讨论一下异步任务之间通信的问题。先来看下问题的由来。</p><p>使用锁和条件变量实现</p><p>如果你做过一定程度的 C++ 异步编程工作，那么在异步通信时，很容易会遇到以下的问题。看这个代码：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 使用条件变量和锁配合，是最常见的做法 </span></span><br><span class="line">std::condition_variable cv; </span><br><span class="line">std::mutex m; </span><br><span class="line"></span><br><span class="line"><span class="comment">// 在发出事件的任务中 </span></span><br><span class="line">&#123;</span><br><span class="line">  cv.<span class="built_in">notify_all</span>(); </span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 在接收事件的任务中 </span></span><br><span class="line">&#123;</span><br><span class="line">  <span class="function">std::unique_lock&lt;std::mutex&gt; <span class="title">lk</span><span class="params">(m)</span></span>; </span><br><span class="line">  cv.<span class="built_in">wait</span>(lk);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这是一个很典型的面试题，考虑下其中存在哪些问题。</p><p>存在 2 个问题。首先，如果在发出事件任务执行完 <code>notify_all()</code> 之后，接收事件任务还没有执行到 <code>wait(lk)</code> 时，那么当接收事件任务最终执行到 <code>wait(lk)</code> 时，便会继续等待，如果后续没有新的通知过来，那将永远阻塞下去。</p><p>其次，<code>wait</code> 有一个叫 “虚假唤醒” 的特性，这是一个系统级的行为。简单展开来说，系统和硬件对于调度线程的成本很高，唤醒操作如果做到高精度，可能的性能代价是难以接受的，所以，在系统层面，明确指出了这个问题，并将精确接收并唤醒线程的最后确认工作交给软件完成。它不是一种 bug，虽然概率不高，但仍然需要认真对待。为了保证精确唤醒，在接收端，必须通过条件确认。</p><p>解决这两个问题的答案是统一的，也就是把 <code>cv.wait(lk)</code> 这一行改成：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cv.<span class="built_in">wait</span>(lk, []&#123; <span class="keyword">return</span> <span class="built_in">condition_check</span>(); &#125;);</span><br></pre></td></tr></table></figure><p>当后边的 lambda 表达式中，返回值为真时，<code>wait</code> 将不再继续阻塞。检查条件状态，需要在发出事件的任务中，对状态做修改（比如一个 bool 值），完整的改进：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">std::condition_variable cv; </span><br><span class="line">std::mutex m; </span><br><span class="line"><span class="function"><span class="type">bool</span> <span class="title">flag</span><span class="params">(<span class="literal">false</span>)</span></span>; </span><br><span class="line"></span><br><span class="line"><span class="comment">// 在发出事件的任务中 </span></span><br><span class="line">&#123; </span><br><span class="line">  <span class="function">std::unique_lock&lt;std::mutex&gt; <span class="title">g</span><span class="params">(m)</span></span>; <span class="comment">// 操作 flag，需要上锁避免竞争 </span></span><br><span class="line">  flag = <span class="literal">true</span>; </span><br><span class="line">  cv.<span class="built_in">notify_all</span>(); </span><br><span class="line">&#125; </span><br><span class="line"></span><br><span class="line"><span class="comment">// 在接收事件的任务中 </span></span><br><span class="line">&#123;</span><br><span class="line">  <span class="function">std::unique_lock&lt;std::mutex&gt; <span class="title">lk</span><span class="params">(m)</span></span>; </span><br><span class="line">  cv.<span class="built_in">wait</span>(lk, []&#123; <span class="keyword">return</span> flag; &#125;);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这是 C++ 异步编程中，处理通信的最常见答案。在 demo 程序中，可能看起来比较清晰，但当这样一个设计模式，放在一个大型的异步程序中，多个锁、多个条件变量，以及更复杂的状态检查条件，会让程序本身开发和维护变得很棘手。</p><h3 id="使用期值来实现"><a href="#使用期值来实现" class="headerlink" title="使用期值来实现"></a>使用期值来实现</h3><p>之前条款中，我们提到，创建的子任务中，返回值会通过 <code>std::promise</code> 类型的对象来发送，而父进程的返回值接收，是通过期值 <code>std::future</code> 对象来接收。</p><p>由于返回值通信并不是泛化的任务间通信，它只能通信一次，所以，本条款也明确指出，只有在<strong>一次性通信任务</strong>中，可以使用这个方案。由于我们只需要传送状态，而不需要带有任何额外信息，故而返回值的类型，只需要设置为 <code>void</code> 即可。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 需要共用的数据 </span></span><br><span class="line">std::promise&lt;<span class="type">void</span>&gt; p; </span><br><span class="line"></span><br><span class="line"><span class="comment">// 在发出事件的任务中 </span></span><br><span class="line">&#123;</span><br><span class="line">  p.<span class="built_in">set_value</span>(); <span class="comment">// 发出 </span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 在接收事件的任务中 </span></span><br><span class="line">&#123;</span><br><span class="line">  p.<span class="built_in">get_future</span>().<span class="built_in">wait</span>(); <span class="comment">// 等待接收事件</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这种实现，既可以解决上一节提到的通知遗漏和虚假唤醒问题，也看起来更整洁。但它也不是完美的：</p><ul><li>如上所述，它只能适合一次性通信，返回值状态不能被重复接收；</li><li>我们知道两端为了保存返回值，需要分配一块共享内存，这带来了堆分配成本；</li></ul><p>如果能接受这样的问题，使用期值来实现通信也是一个不错的方案。一个完整的 demo 如下：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">void</span> <span class="title">received</span><span class="params">()</span></span>; <span class="comment">// 实现接受事件后，需要做的事情 </span></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">send</span><span class="params">()</span></span>; <span class="comment">// 实现创建子任务、发送事件、回收资源等事情 </span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">send</span><span class="params">()</span> </span>&#123; </span><br><span class="line">  std::promise&lt;<span class="type">void</span>&gt; p; </span><br><span class="line">  <span class="function">std::thread <span class="title">t</span><span class="params">([] &#123; </span></span></span><br><span class="line"><span class="params"><span class="function">       p.get_future().wait(); </span></span></span><br><span class="line"><span class="params"><span class="function">       received(); </span></span></span><br><span class="line"><span class="params"><span class="function">    &#125;)</span></span>; </span><br><span class="line"></span><br><span class="line">  <span class="comment">// 发送事件之前的一些准备工作 </span></span><br><span class="line">  p.<span class="built_in">set_value</span>(); <span class="comment">// 发送事件 </span></span><br><span class="line">  t.<span class="built_in">join</span>(); <span class="comment">// 联结线程 </span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>书中还提到了这里存在的一个小问题。假设在线程 <code>t</code> 创建启动之后，<code>p.set_value()</code> 之前，父进程程序发生了异常，那么 <code>p.set_value()</code> 不会被执行，那么子线程就会始终阻塞下去。使用第一种条件变量的方案也会遇到这个问题。</p><p>书中没有给出这个问题的答案，以下是我的个人想法，仅供参考。完整的改善代码为：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">void</span> <span class="title">received</span><span class="params">()</span></span>; </span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">send</span><span class="params">()</span></span>; </span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">send</span><span class="params">()</span> </span>&#123; </span><br><span class="line">  std::thread t; </span><br><span class="line"> &#123;</span><br><span class="line">    std::promise&lt;<span class="type">void</span>&gt; p;</span><br><span class="line">    <span class="keyword">auto</span> fut = p.<span class="built_in">get_future</span>();</span><br><span class="line">    <span class="function">std::thread <span class="title">t</span><span class="params">([f = std::move(fut)]() <span class="keyword">mutable</span> &#123;   </span></span></span><br><span class="line"><span class="params"><span class="function">      f.wait(); <span class="comment">// 等待接收 </span></span></span></span><br><span class="line"><span class="params"><span class="function">      received();</span></span></span><br><span class="line"><span class="params"><span class="function">    &#125;)</span></span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">      <span class="comment">// 假设前边的代码会发生异常</span></span><br><span class="line">      p.<span class="built_in">set_value</span>();</span><br><span class="line">    &#125; <span class="built_in">catch</span> (<span class="type">const</span> std::exception&amp; e) &#123;  </span><br><span class="line">      std::cerr &lt;&lt; <span class="string">&quot;Catch exception: &quot;</span> &lt;&lt; e.<span class="built_in">what</span>() &lt;&lt; endl; </span><br><span class="line">    &#125; </span><br><span class="line">  &#125; </span><br><span class="line">  </span><br><span class="line">  <span class="keyword">if</span> (t.<span class="built_in">joinable</span>()) </span><br><span class="line">    t.<span class="built_in">join</span>(); </span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这块代码中的几个细节。首先，在创建线程 <code>t</code> 之前，先获取 <code>p</code> 的期值 <code>fut</code>。这个操作很关键，在获取期值的同时，编译器会为父进程和子线程创建共享的数据区域，来存放返回值，这块区域既不属于父进程，也不属于子线程，而是独立于两者的作用域，而且，只有当两个任务都结束之后，这块空间才会被释放。</p><p>其次，将期值 <code>fut</code> 以移动的方式交给子线程，这样，我们就同时获得了发送端、接收端和一块独立的共享对象。当对象 <code>p</code> 被析构后，原来的实现中，子线程中的对象 <code>p</code> 的资源以引用方式（<code>std::promise</code> 也是一个资源句柄）捕获，便会变成悬空引用。这种改动可以避免这个问题。</p><p>最后，也是最关键的一点，<code>std::promise</code> 的析构函数在执行时，如果其共享对象没有被设置（没有调用 <code>set_value</code> 或没有发生过异常），那么析构函数会将一个异常状态 <code>std::future_error</code> 存入共享状态中。当任何在该共享对象上等待的操作发生时（如 <code>f.wait()</code>），则会立即被唤醒。</p><p>书中还建议利用 RAII 来包装线程，我认为在这个示例中没必要，如果包装，包装类对象和 <code>std::promise</code> 对象 <code>p</code> 位于相同的作用域，如果包装类的析构先于 <code>p</code> 的析构执行，那么又会遇到 <code>t.join()</code> 与 <code>f.wait()</code> 之间死锁的问题，还需要去处理析构顺序，就有点偏题了。</p><p>总的来说，我个人的意见是，本本分分地去使用条件变量和锁的方案为好。换成期值方案，不但有一些限制条件，而且并没有完全弱化异步编程的难度。</p><blockquote><p><strong>技巧</strong>：在某些场合下，可以使用期值特性来实现异步通信，获得特别的一些好处。</p></blockquote><h2 id="条款-40：区分-std-atomic-和-volatile-的不用用法"><a href="#条款-40：区分-std-atomic-和-volatile-的不用用法" class="headerlink" title="条款 40：区分 std::atomic 和 volatile 的不用用法"></a>条款 40：区分 std::atomic 和 volatile 的不用用法</h2><p>实话说，在看到这个条款时，我是困惑的，因为我从来没有想过这两个东西可以并列在一起讨论。但估计有些人确实会混淆二者，所以我考虑还是展开陈述一下。</p><h3 id="std-atomic-的作用"><a href="#std-atomic-的作用" class="headerlink" title="std::atomic 的作用"></a>std::atomic 的作用</h3><p>本章讨论并发编程，所以原子操作是不应该缺席的，毕竟它太常用了，也有很多值得说道的地方。</p><p>首先，有关于 <code>std::atomic</code> 对象的操作，是原子的，它意味着多线程并发访问该对象时，硬件可以保证访问不会出现数据竞争，这是很简单的知识。不过需要注意，虽然对原子对象本身的操作是原子的，但如果原子操作位于另外一个非原子的表达式中，那么别把整个表达式也看作原子的。</p><p>其次，<code>std::atomic</code> 操作还会对指令的重排序产生限制。我们知道，编译器会做一些优化，对一些它认为没有数据依赖和控制依赖的表达式，做重排序，这样做可能会改进整个程序的性能；另外，硬件本身也有可能对指令乱序发射，它也会去分析程序的依赖性。如果我们的程序中使用了一条原子操作，那么在<strong>默认情况下，原子操作前后的指令重排序，不会跨过该原子操作</strong>。</p><p>这个地方涉及到内存模型的问题。重排序操作通常是在单线程模式下计算的，所以在多线程场景下，可能会出问题，原子操作利用内存屏障（memory fence）的机制，阻止了重排序跨过自身。</p><p>内存模型有多种不同的内存序，默认的内存序是顺序一致性，而顺序一致性保证了原子操作之前的指令，不会调度到之后，以及原子操作之后的指令，不会调度到之前。这是最强的一致性保证。</p><p>还有其他的内存序，不在这里单独展开。</p><h3 id="volatile-的作用"><a href="#volatile-的作用" class="headerlink" title="volatile 的作用"></a>volatile 的作用</h3><p>做过嵌入式软件开发的同学应该都对此关键字很熟悉。它最常用的地方在于，声明一个变量是外部接口，比如内存映射的 I&#x2F;O，端口，传感器等外设。它们的特点是，变量本身的读写，不完全取决于程序内部指令，还可能 “随机地” 出现在程序运行期间的任何时间点。</p><p>这种情况下，我们就不能完全按程序指令的静态假设，来对该变量做优化。比如：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">int</span> x; </span><br><span class="line"><span class="keyword">auto</span> y = x; </span><br><span class="line">y = x; </span><br><span class="line"></span><br><span class="line"><span class="keyword">volatile</span> <span class="type">int</span> v; </span><br><span class="line"><span class="keyword">auto</span> z = v; </span><br><span class="line">z = v;</span><br></pre></td></tr></table></figure><p>其中，第一条 <code>y=x</code> 和第二条 <code>y=x</code> 在程序逻辑中，是可以通过删除第一条赋值操作来优化的；但第一条 <code>z=v</code> 和第二条 <code>z=v</code>，由于变量 <code>v</code> 被声明为 <code>volatile</code>，两条赋值操作都不会被优化。也许这里的 <code>v</code> 映射到了外部的传感器，而传感器每时每刻的读取值都可能不同，所以每次赋值操作都可能不同。</p><p><code>volatile</code> 便是告诉编译器，不要对这个变量做任何优化。</p><p>最后总结一下，虽然看似两个概念都涉及到避免编译器或硬件操作改变程序行为，但两者的行为机制并不相同。</p><ul><li><code>std::atomic</code> 对于并发程序是有用的，它可以保证操作的原子性避免数据竞争，也能约束指令调度的范围。但它做不到声明访问特殊内存时，避免编译器优化；</li><li><code>volatile</code> 则相反，基本上专用于声明特殊内存，避免编译器优化；但做不到前者那些在并发程序中的用途；</li></ul><p>特别的，如果一个变量，既需要并发编程时的原子性，也刚好是个特殊内存，那么它可以同时声明二者：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">volatile</span> std::atomic&lt;<span class="type">int</span>&gt; va;</span><br></pre></td></tr></table></figure><blockquote><p><strong>谬误</strong>：在异步程序中，使用 <code>volatile</code> 来修饰原子操作。</p></blockquote><hr><p>本系列的其他文章：</p><ol class="series-items"><li><a href="/posts/9bb75fe1.html" title="Effective Modern C++ 读书笔记：类型推导">Effective Modern C++ 读书笔记：类型推导</a></li><li><a href="/posts/f3206605.html" title="Effective Modern C++ 读书笔记：auto">Effective Modern C++ 读书笔记：auto</a></li><li><a href="/posts/57a898cb.html" title="Effective Modern C++ 读书笔记：转向现代C++">Effective Modern C++ 读书笔记：转向现代C++</a></li><li><a href="/posts/ce1aec80.html" title="Effective Modern C++ 读书笔记：智能指针">Effective Modern C++ 读书笔记：智能指针</a></li><li><a href="/posts/410ab8fb.html" title="Effective Modern C++：右值引用、移动语义和完美转发">Effective Modern C++：右值引用、移动语义和完美转发</a></li><li><a href="/posts/cd605d5c.html" title="Effective Modern C++：lambda 表达式">Effective Modern C++：lambda 表达式</a></li><li><a href="/posts/745d2232.html" title="Effective Modern C++：并发 API">Effective Modern C++：并发 API</a></li><li><a href="/posts/49749d08.html" title="Effective Modern C++：微调">Effective Modern C++：微调</a></li></ol><hr><div class="note info flat"><p>本文同步发布在知乎账号下：<a href="https://zhuanlan.zhihu.com/p/1962242496819098139">https://zhuanlan.zhihu.com/p/1962242496819098139</a></p></div>]]></content>
    
    
      
      
    <summary type="html">&lt;link rel=&quot;stylesheet&quot; class=&quot;aplayer-secondary-style-marker&quot; href=&quot;/assets/css/APlayer.min.css&quot;&gt;&lt;script src=&quot;/assets/js/APlayer.min.js&quot; cla</summary>
      
    
    
    
    <category term="软件开发" scheme="https://p2tree.top/categories/%E8%BD%AF%E4%BB%B6%E5%BC%80%E5%8F%91/"/>
    
    
    <category term="CPP" scheme="https://p2tree.top/tags/CPP/"/>
    
  </entry>
  
  <entry>
    <title>Effective Modern C++：lambda 表达式</title>
    <link href="https://p2tree.top/posts/cd605d5c.html"/>
    <id>https://p2tree.top/posts/cd605d5c.html</id>
    <published>2025-09-19T22:57:50.000Z</published>
    <updated>2025-09-21T14:17:07.000Z</updated>
    
    <content type="html"><![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/assets/css/APlayer.min.css"><script src="/assets/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><p>lambda 表达式是 C++ 中一个极具颠覆性的语言特性，它主要用于创建匿名函数，从而可以更便捷地在一些场合下，快速生成函数对象后传递给需要的位置。这一章中，会陈述几个和 lambda 表达式相关的问题。</p><p>首先，需要明确几个概念。lambda 表达式是一种语法，它是静态的，编译器处理 lambda 表达式时，会利用它生成一个闭包类，然后在利用 lambda 表达式定义对象时，利用这个闭包类生成一个闭包对象。闭包对象是动态行为。</p><p>虽然我们无法方便地知道 lambda 表达式生成地闭包对象是什么类型，但好在可以用 <code>auto</code> 类型来让编译器自己推导。也由于闭包对象和其他对象没有什么大的不同，也可以实现赋值和移动等操作。</p><h2 id="条款-31：避免使用默认捕获模式"><a href="#条款-31：避免使用默认捕获模式" class="headerlink" title="条款 31：避免使用默认捕获模式"></a>条款 31：避免使用默认捕获模式</h2><p>所谓默认捕获模式，就是直接使用 <code>&amp;</code> 或 <code>=</code> 捕获当前作用域中所有可以捕获的对象（按引用或按值）。本条款建议，尽量将 lambda 表达式中需要使用到的外部对象，显式写在捕获列表中。虽然无法完全避免下文可能存在的问题，但默认捕获模式，可能会带来更危险的陷阱。</p><h3 id="情况-1：通过引用捕获"><a href="#情况-1：通过引用捕获" class="headerlink" title="情况 1：通过引用捕获"></a>情况 1：通过引用捕获</h3><p>一个显而易见的问题是，lambda 表达式生成的闭包对象，其生命周期可能会超出当前作用域，如果 lambda 表达式体中捕获了只在当前作用域中生存的对象，那么当被捕获对象离开作用域被析构时，lambda 表达式中的按引用捕获，实际就变成了空悬引用。</p><p>所以第一种最简单的情况就是，按引用捕获时，注意被捕获对象的生命周期，看例子：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 定义一个全局的闭包对象容器</span></span><br><span class="line"><span class="keyword">using</span> FilterContainer = std::vector&lt;std::function&lt;<span class="built_in">bool</span>(<span class="type">int</span>)&gt;&gt;;</span><br><span class="line">FilterContainer filters;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">func</span><span class="params">()</span> </span>&#123;</span><br><span class="line">  <span class="keyword">auto</span> divisor = <span class="built_in">getDivisor</span>();</span><br><span class="line">  filters.<span class="built_in">emplace_back</span>([&amp;](<span class="type">int</span> value) &#123; <span class="keyword">return</span> value % divisor == <span class="number">0</span>; &#125;);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这个代码中，<code>filters</code> 会保存多个满足类型的闭包对象（闭包对象是一种可调用类型，所以可以使用 <code>std::function&lt;&gt;</code> 来定义类型）。函数 <code>func</code> 中，局部对象 <code>divisor</code> 被按默认捕获的方式捕获到 lambda 表达式中。</p><p>那么，当在其他位置访问 <code>filters</code> 中的闭包对象时，闭包中的 <code>divisor</code> 就已经是空悬引用了，它的值是否合法是不确定的。</p><p>一个可以改进的策略便是，不要使用默认捕获模式，将需要捕获的对象 <code>divisor</code> 显式写在捕获列表中：<code>filters.emplace_back([&amp;divisor](int value) &#123; return value % divisor == 0; &#125;);</code>。当然，在这个例子中，这么写依然避免不了问题，但至少更容易发现问题。当 lambda 表达式体中的代码比较复杂时，默认捕获模式会更不容易看出来 lambda 表达式依赖哪些外部对象。</p><h3 id="情况-2：通过值捕获"><a href="#情况-2：通过值捕获" class="headerlink" title="情况 2：通过值捕获"></a>情况 2：通过值捕获</h3><p>当你预料到情况 1 的结论，打算在之后的代码中，谨慎使用引用捕获，转而使用按值捕获，可能依然会带来一些潜在的问题。 虽然按值捕获可以让值对象采用复制方式传入 lambda 表达式体，但如果按值捕获的是指针，那么指针指向的内容仍然会随时变化，这依然是和引用捕获一样的问题。</p><p>看一个书中很典型的例子：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Widget</span> &#123;</span><br><span class="line"><span class="keyword">public</span>:</span><br><span class="line">  <span class="function"><span class="type">void</span> <span class="title">addFilter</span><span class="params">()</span> <span class="type">const</span></span>;</span><br><span class="line"><span class="keyword">private</span>:</span><br><span class="line">  <span class="type">int</span> divisor;</span><br><span class="line">&#125;;</span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">Widget::addFilter</span><span class="params">()</span> <span class="type">const</span> </span>&#123;</span><br><span class="line">  filters.<span class="built_in">emplace_back</span>( [=](<span class="type">int</span> value) &#123; <span class="keyword">return</span> value % divisor == <span class="number">0</span>; &#125;);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这个例子中，在一个类内，定义一个类数据 <code>divisor</code>，然后在类的成员函数中实现保存闭包对象到容器。看似没有什么问题，每一次创建闭包对象时，都会拷贝一份 <code>divisor</code> 的值进去。然而，这是误解。</p><p><strong>捕获行为只能捕获在创建 lambda 表达式所在作用域内的可见非静态局部对象</strong>，而对于例子中的这种捕获 <code>divisor</code> 的方式，编译器会将指向当前类对象的 <code>this</code> 指针，捕获到 lambda 表达式中，也就是说，等价于 <code>[=this](int value) &#123; return value % this.divisor == 0; &#125;</code>。 那么，当 <code>Widget</code> 的对象，调用完 <code>addFilter</code> 之后，早于 <code>filters</code> 的生命周期而提前被析构，那 <code>this</code> 指针就会变成悬空指针，之后使用 <code>filters</code> 中的闭包，就会遇到未定义问题。</p><p>除了 <code>this</code> 指针，其他位于当前作用域内的普通指针，一样会遇到相同的问题。一种改善的方案便是，手动复制一份需要捕获的对象（平凡类型）到当前作用域。保险起见，也建议手动把捕获列表补充上去：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">void</span> <span class="title">Widget::addFilter</span><span class="params">()</span> <span class="type">const</span> </span>&#123;</span><br><span class="line">  <span class="keyword">auto</span> divisorCopy = divisor;</span><br><span class="line">  filters.<span class="built_in">emplace_back</span>( [=divisorCopy](<span class="type">int</span> value) &#123; <span class="keyword">return</span> value % divisorCopy == <span class="number">0</span>; &#125;);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在 C++17 中，提供了一种特性来帮助我们解决这个问题，使用 <code>[*this]</code> 这种捕获列表，可以允许我们将一个对象的副本捕获到 lambda 表达式作用域内，从而避免因原对象被析构后的悬挂引用问题。注意到因为这里创建了对象的副本给 lambda，所以可能会有较大的性能开销，具体选择哪个方案则因情况而定。</p><p>在 C++20 中，进一步改进了捕获 <code>this</code> 的特性，如果仍然使用 <code>[=]</code> 这种值捕获方式，将不会再包含 <code>this</code> 指针，如果想要捕获 <code>this</code>，需要手动写入捕获列表：<code>[=, this]</code>，这避免了上述可能存在的问题。</p><p>感谢 <a href="https://www.zhihu.com/people/e5d8e067644fd41afe5e6c2246d1e66a">@机械索尼克</a> 的补充。</p><h3 id="情况-3：捕获静态对象"><a href="#情况-3：捕获静态对象" class="headerlink" title="情况 3：捕获静态对象"></a>情况 3：捕获静态对象</h3><p>lambda 表达式除了可以通过捕获来使用局部变量和形参，也可以直接使用静态对象（和普通函数一样），也就是定义在全局或名字作用域中，或者是类中和函数中以 <code>static</code> 修饰的对象。这种静态对象，不是通过捕获来访问的，但却会给人以错觉，认为是捕获访问。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">void</span> <span class="title">func</span><span class="params">()</span> </span>&#123;</span><br><span class="line">  <span class="type">static</span> <span class="keyword">auto</span> divisor = <span class="built_in">getDivisor</span>();</span><br><span class="line">  filters.<span class="built_in">emplace_back</span>([=](<span class="type">int</span> value) &#123; <span class="keyword">return</span> value % divisor == <span class="number">0</span>; &#125;);</span><br><span class="line">  ++ divisor;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这个例子中，lambda 表达式使用默认值捕获，但实际上它什么也没有捕获，<code>divisor</code> 对象只是因为 <code>static</code> 属性而被直接访问的。这样，每次调用 <code>func</code> 函数时，<code>divisor</code> 都会发生变化，这可能会导致非预期的行为（我们应该是期待按值捕获 <code>divisor</code>，从而 lambda 中的值不会变化）。</p><blockquote><p>谬误：lambda 表达式访问静态对象，并不是通过捕获方式访问。</p></blockquote><p>虽然无法直接避免这种误解，但如果不使用默认值捕获方式，就更容易发现这个细节：<code>filters.emplace_back([](int value) &#123; return value % divisor == 0; &#125;);</code>，没有捕获任何东西，但访问了静态对象 <code>divisor</code>。</p><p>综上几种情况，做个总结，lambda 表达式的捕获行为可能会让程序出现一些非预期的错误，虽然从编译层面无法规避这种问题，但不要使用默认捕获方式（包括按值捕获和按引用捕获），就更容易发现问题。</p><h2 id="条款-32：将对象通过移动方式传入闭包"><a href="#条款-32：将对象通过移动方式传入闭包" class="headerlink" title="条款 32：将对象通过移动方式传入闭包"></a>条款 32：将对象通过移动方式传入闭包</h2><p>之前提到的无论是按值捕获还是按引用捕获，都无法将一个只移对象（比如 <code>std::unique_ptr</code>）传入闭包，C++ 11 做不到。在 C++14 中，支持了通过一种特殊的捕获方式，将对象移动入闭包。 这种捕获方式叫做初始化捕获（init capture），它可以实现除了默认捕获之外的任何捕获行为，所以也被称为通用 lambda 捕获（generalized lambda capture）。</p><p>它的语法是：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">auto</span> pw = std::<span class="built_in">make_unique</span>&lt;Widget&gt;();  <span class="comment">// 准备一个只移对象</span></span><br><span class="line"></span><br><span class="line"><span class="comment">//... some other code</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">auto</span> func = [pw = std::<span class="built_in">move</span>(pw)] &#123; ... &#125;;</span><br></pre></td></tr></table></figure><p>语法很巧妙的区分了捕获列表中 <code>=</code> 左右的作用域，左边的作用域在 lambda 表达式体内部，右边的作用域位于定义 lambda 表达式所在的作用域，所以可以使用相同的名称（如例子中的 <code>pw</code>）。</p><p>如果想要在定义 lambda 表达式时初始化对象，也可以：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">auto</span> func = [pw = std::<span class="built_in">make_unique</span>&lt;Widget&gt;()] &#123; ... &#125;;</span><br></pre></td></tr></table></figure><p>捕获列表中可以放置任何表达式，所以说它是一种通用捕获模式。例子中，表达式返回了一个只移右值，可以通过移动方式传入闭包。</p><p>以上是 C++14 中支持的语法，在 C++11 中，无法使用。如果 C++11 依然想要通过移动传入对象，可以用一些曲线救国的办法。</p><p>第一种办法是，手写一个可调用类型（仿函数），也就是带有 <code>operator()</code> 重载的类，我们知道，lambda 表达式经过编译器处理后，实际上也生成了这样一种类型。我们手动为其定义支持右值引用作为参数的构造函数，便可以在定义函数对象时，传入右值。之后使用函数对象的方法和 lambda 表达式完全一致。</p><p>第二种办法是利用 <code>std::bind</code>，虽然 C++11 不支持移动捕获 lambda，但支持绑定一个函数并通过移动方式传入参数。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">std::vector&lt;<span class="type">double</span>&gt; data;  <span class="comment">// 准备一个对象，希望通过移动传入闭包</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">auto</span> func = std::<span class="built_in">bind</span>(</span><br><span class="line">  [](<span class="type">const</span> std::vector&lt;<span class="type">double</span>&gt; &amp;data) &#123; ... &#125;,</span><br><span class="line">  std::<span class="built_in">move</span>(data);</span><br><span class="line">);</span><br></pre></td></tr></table></figure><p>这个代码中，将对象 <code>data</code> 转换为右值后，绑定到 lambda 表达式中的第一个形参，以此来实现移动一个对象到 lambda 表达式中的目的。这里没有利用 lambda 表达式的捕获，所以 C++11 是支持的。</p><p>最后，要提一下，无论是 C++14 的初始化捕获，还是 C++11 的 <code>std::bind</code>，他们中涉及到的表达式求值，都是在定义 lambda 或绑定对象时求值。但 C++11 中，绑定对象只有在被调用时（也就是 <code>func()</code> 调用时），存储在绑定对象中的实参（例子中的右值 <code>std::move(data)</code> 经移动构造生成在绑定对象中的副本 ）才会传入 lambda 表达式中，所以事实上，lambda 表达式中操作的 <code>data</code> 实际上是绑定对象中的副本（注意到，lambda 表达式参数列表中的 <code>data</code> 类型是左值引用）。</p><h2 id="条款-33：泛型-lambda-表达式"><a href="#条款-33：泛型-lambda-表达式" class="headerlink" title="条款 33：泛型 lambda 表达式"></a>条款 33：泛型 lambda 表达式</h2><p>在 C++14 中，增加了一个非常实用的特性，即泛型 lambda 表达式，它允许我们编写 lambda 表达式时，对其形参类型自推导：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">auto</span> f = [](<span class="keyword">auto</span> x) &#123; <span class="keyword">return</span> <span class="built_in">g</span>(x); &#125;;</span><br></pre></td></tr></table></figure><p>它类似于模板函数中的类型推导，方便我们编写泛型的匿名函数。</p><p>然而，这里要引出本条款要讨论的问题，如果我们需要将这个 lambda 表达式编写为完美转发形参，会遇到什么问题？回忆一下，使用模板函数编写完美转发函数：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">template</span> &lt;<span class="keyword">typename</span> T&gt;</span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">ff</span><span class="params">(T&amp;&amp; param)</span> </span>&#123;</span><br><span class="line">  <span class="built_in">gg</span>(std::forward&lt;T&gt;(param));</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>我们需要利用 <code>std::forward&lt;T&gt;</code> 来对形参 <code>param</code> 做一次转换，它的作用是，如果类型 <code>T</code> 是左值引用，则转换为左值引用；如果类型 <code>T</code> 是右值引用，则转换为右值引用。</p><p>然后，我们需要利用 <code>decltype</code> 这个工具，手动推导形参的类型，好在 <code>decltype</code> 的返回类型，也是符合预期的，当传入形参是左值时，返回左值引用，传入形参是右值时，返回右值引用。 最后，我们编写的完美转发的泛型 lambda 表达式为：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">auto</span> f = [](<span class="keyword">auto</span>&amp;&amp; x) &#123; <span class="keyword">return</span> <span class="built_in">g</span>(std::forward&lt;<span class="keyword">decltype</span>(x)&gt;(x)); &#125;;</span><br></pre></td></tr></table></figure><p>考虑不定长参数列表的版本：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">auto</span> f = [](<span class="keyword">auto</span>&amp;&amp;... x) &#123; <span class="keyword">return</span> <span class="built_in">g</span>(std::forward&lt;<span class="keyword">decltype</span>(x)&gt;(x)...); &#125;;</span><br></pre></td></tr></table></figure><p>对于书中对 <code>std::forward&lt;T&gt;</code> 和 <code>decltype</code> 在传入右值类型时的正确性讨论，我认为想地太复杂了，所以这里略掉，感兴趣的朋友可以去翻原文。</p><h2 id="条款-34：优先使用-lambda-表达式替代-std-bind"><a href="#条款-34：优先使用-lambda-表达式替代-std-bind" class="headerlink" title="条款 34：优先使用 lambda 表达式替代 std::bind"></a>条款 34：优先使用 lambda 表达式替代 std::bind</h2><p>lambda 表达式和 <code>std::bind</code> 在大多数场景下的功能是重叠的，在 C++98 那个没有 lambda 表达式可以使用的年代，想要灵活地包装一个函数，并返回一个新的函数对象，使用 <code>std::bind</code> 是非常常见的做法。</p><p>然而，随着 Modern C++ 的发展，lambda 表达式的能力越来越强大，以至于可以完全取代 <code>std::bind</code>。</p><h3 id="理由-1：lambda-表达式的语法更清晰，更易读"><a href="#理由-1：lambda-表达式的语法更清晰，更易读" class="headerlink" title="理由 1：lambda 表达式的语法更清晰，更易读"></a>理由 1：lambda 表达式的语法更清晰，更易读</h3><p>考虑一个书中的例子。现在有一个可以发出报警的函数，支持几个配置，我们希望使用 lambda 表达式或 <code>std::bind</code> 来包装这个函数，让其中一部分配置使用默认值：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">using</span> Time = std::chrono::steady_clock::time_point;</span><br><span class="line"><span class="keyword">using</span> Duration = std::chrono::steady_clock::duration;</span><br><span class="line"><span class="keyword">enum class</span> <span class="title class_">Sound</span> &#123; Beep, Siren, Whistle &#125;;  <span class="comment">// 声音类型</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 这是需要包装的函数，指定开始报警事件、声音类型和持续时间</span></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">setAlarm</span><span class="params">(Time t, Sound s, Duration d)</span></span>;</span><br></pre></td></tr></table></figure><p>如果我们想包装一个可调用对象，在调用发生时刻 1 小时后开始报警，持续 30 秒，但声音类型希望调用时指定。使用 lambda 表达式的实现方案非常简单：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">auto</span> setSoundL =  <span class="comment">// &#x27;L&#x27; 表示 lambda 方案</span></span><br><span class="line">  [](Sound s) &#123;</span><br><span class="line">    <span class="keyword">using</span> <span class="keyword">namespace</span> std::chrono;</span><br><span class="line">    <span class="built_in">setAlarm</span>(steady_clock::<span class="built_in">now</span>() + <span class="built_in">hours</span>(<span class="number">1</span>), s, <span class="built_in">seconds</span>(<span class="number">30</span>)); &#125;;</span><br><span class="line"></span><br><span class="line"><span class="comment">// c++14 中可以利用字面值常量优化：</span></span><br><span class="line"><span class="keyword">auto</span> setSoundL = </span><br><span class="line">  [](Sound s) &#123;</span><br><span class="line">    <span class="keyword">using</span> <span class="keyword">namespace</span> std::chrono;</span><br><span class="line">    <span class="keyword">using</span> <span class="keyword">namespace</span> std::literals;</span><br><span class="line">    <span class="built_in">setAlarm</span>(steady_clock::<span class="built_in">now</span>() + <span class="number">1</span>h, s, <span class="number">30</span>s); &#125;;</span><br></pre></td></tr></table></figure><p>然而，使用 <code>std::bind</code> 的方案，不但不易读，反而还存在问题：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">auto</span> setSoundB =  <span class="comment">// &#x27;B&#x27; 表示 std::bind 方案</span></span><br><span class="line">  std::<span class="built_in">bind</span>(setAlarm, steady_clock::<span class="built_in">now</span>() + <span class="number">1</span>h, _1, <span class="number">30</span>s);</span><br></pre></td></tr></table></figure><p>首先，我们在使用 <code>setSoundB</code> 时，需要搞清楚它的参数，对应原始函数 <code>setAlarm</code> 中的哪个参数。这里，我们可以通过函数名称来确定参数是 <code>Sound</code>，但很多时候，尤其是有多个占位符同时出现时，唯一的办法就是去查看 <code>setAlarm</code> 的声明。</p><p>而且，这里还存在一个问题，表达式延迟求值的问题。对于 lambda 的版本，<code>steady_clock::now()</code> 的求值（当前时间），是 <code>setSoundL</code> 被实际调用的时候；而 <code>std::bind</code> 的版本，<code>steady_clock::now()</code> 的求值，却是在 <code>setSoundB</code> 被定义的地方，这可能产生 Bug。 一种改进 <code>setSoundB</code> 的方法是：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">auto</span> setSoundB = </span><br><span class="line">  std::<span class="built_in">bind</span>(setAlarm, </span><br><span class="line">            std::<span class="built_in">bind</span>(std::plus&lt;&gt;(), steady_clock::<span class="built_in">now</span>(), <span class="number">1</span>h),</span><br><span class="line">            _1, <span class="number">30</span>s);</span><br></pre></td></tr></table></figure><p>虽然问题解决了，但这种写法就看起来更复杂了。</p><h3 id="理由-2：如果被包装函数有重载版本"><a href="#理由-2：如果被包装函数有重载版本" class="headerlink" title="理由 2：如果被包装函数有重载版本"></a>理由 2：如果被包装函数有重载版本</h3><p>如果 <code>setAlarm</code> 有重载版本，比如有个带有 4 个形参的重载版本。那么 lambda 表达式依然可以正常找到正确的 3 参数重载版本，而 <code>std::bind</code> 则不行，会发生编译报错。</p><p>如果想继续改进 <code>setSoundB</code>，可以：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">using</span> SetAlarm3ParamType = <span class="built_in">void</span>(*)(Time t, Sound s, Duration d);</span><br><span class="line"></span><br><span class="line"><span class="keyword">auto</span> setSoundB = </span><br><span class="line">  std::<span class="built_in">bind</span>(<span class="built_in">static_cast</span>&lt;SetAlarm3ParamType&gt;(setAlarm),</span><br><span class="line">            std::<span class="built_in">bind</span>(std::plus&lt;&gt;(), steady_clock::<span class="built_in">now</span>(), <span class="number">1</span>h),</span><br><span class="line">            _1, <span class="number">30</span>s);</span><br></pre></td></tr></table></figure><p>也就是手动对函数做向确定函数类型方向做类型转换。</p><p>另外，这里还会引出另一个细节。对于 lambda 表达式的方案，lambda 表达式体，可以由编译器做优化，比如对 <code>setAlarm</code> 做函数内联；但 <code>std::bind</code> 却基本无法做编译器优化。最后导致 lambda 表达式的版本会比 <code>std::bind</code> 的版本性能更好。</p><h3 id="理由-3：std-bind-无法控制参数传入方式"><a href="#理由-3：std-bind-无法控制参数传入方式" class="headerlink" title="理由 3：std::bind 无法控制参数传入方式"></a>理由 3：std::bind 无法控制参数传入方式</h3><p>对于 lambda 表达式的方案，我们想传入的 <code>Sound</code> 参数，可以通过按值捕获或按引用捕获的方式，在 C++14 中还可以选择按移动捕获。但对于 <code>std::bind</code> 则无法做到这样的自由。</p><p><code>std::bind</code> 中的默认填好的绑定参数，只能按值传递，而其占位符参数，在绑定函数对象最终被调用时，只能按引用传递。如果想要写出正确的代码和高性能的代码，必须始终牢记这个设计原理。</p><blockquote><p>技巧：总是试着使用 lambda 表达式去替代 <code>std::bind</code> 的代码。</p></blockquote><p>最后，总结一下，在 Modern C++ 中，完全可以使用 lambda 表达式替代 <code>std::bind</code>，只有 C++11 中，有 2 个场景，还是需要 <code>std::bind</code>，在前边条款中提到过：</p><ol><li>C++11 中 lambda 表达式无法做到移动捕获，只能依赖 <code>std::bind</code> 实现；</li><li>C++11 中 lambda 表达式无法做到泛型，需要编写带有调用运算符模板的函数对象，再使用 <code>std::bind</code> 来实现；</li></ol><p>对于这两种情况，都是很少见的使用场景，而且 C++14 中的 lambda 表达式都已经得到妥善的支持。</p><hr><p>本系列的其他文章：</p><ol class="series-items"><li><a href="/posts/9bb75fe1.html" title="Effective Modern C++ 读书笔记：类型推导">Effective Modern C++ 读书笔记：类型推导</a></li><li><a href="/posts/f3206605.html" title="Effective Modern C++ 读书笔记：auto">Effective Modern C++ 读书笔记：auto</a></li><li><a href="/posts/57a898cb.html" title="Effective Modern C++ 读书笔记：转向现代C++">Effective Modern C++ 读书笔记：转向现代C++</a></li><li><a href="/posts/ce1aec80.html" title="Effective Modern C++ 读书笔记：智能指针">Effective Modern C++ 读书笔记：智能指针</a></li><li><a href="/posts/410ab8fb.html" title="Effective Modern C++：右值引用、移动语义和完美转发">Effective Modern C++：右值引用、移动语义和完美转发</a></li><li><a href="/posts/cd605d5c.html" title="Effective Modern C++：lambda 表达式">Effective Modern C++：lambda 表达式</a></li><li><a href="/posts/745d2232.html" title="Effective Modern C++：并发 API">Effective Modern C++：并发 API</a></li><li><a href="/posts/49749d08.html" title="Effective Modern C++：微调">Effective Modern C++：微调</a></li></ol><hr><div class="note info flat"><p>本文同步发布在知乎账号下：<a href="https://zhuanlan.zhihu.com/p/1952400191220094938">https://zhuanlan.zhihu.com/p/1952400191220094938</a></p></div>]]></content>
    
    
      
      
    <summary type="html">&lt;link rel=&quot;stylesheet&quot; class=&quot;aplayer-secondary-style-marker&quot; href=&quot;/assets/css/APlayer.min.css&quot;&gt;&lt;script src=&quot;/assets/js/APlayer.min.js&quot; cla</summary>
      
    
    
    
    <category term="软件开发" scheme="https://p2tree.top/categories/%E8%BD%AF%E4%BB%B6%E5%BC%80%E5%8F%91/"/>
    
    
    <category term="CPP" scheme="https://p2tree.top/tags/CPP/"/>
    
  </entry>
  
  <entry>
    <title>Effective Modern C++：右值引用、移动语义和完美转发</title>
    <link href="https://p2tree.top/posts/410ab8fb.html"/>
    <id>https://p2tree.top/posts/410ab8fb.html</id>
    <published>2025-09-10T22:38:19.000Z</published>
    <updated>2025-09-10T14:43:01.000Z</updated>
    
    <content type="html"><![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/assets/css/APlayer.min.css"><script src="/assets/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><p>在学习 C++ 时，当接触到右值引用和移动语义等概念时，就会劝退很多人。一方面，是因为这些概念本身已经不再是入门知识了，即使不懂，也可以写出一些程序了；另一方面，大多数教材中，并没有以最简洁易懂的方式完成这些知识的传授。</p><p>本章节会把这部分知识最底层最细节的内容，用另一个视角展示出来。但前提是，你需要已经理解什么是左值和右值，什么是引用这些基本的概念。</p><h2 id="条款-23：理解移动语义和完美转发"><a href="#条款-23：理解移动语义和完美转发" class="headerlink" title="条款 23：理解移动语义和完美转发"></a>条款 23：理解移动语义和完美转发</h2><h3 id="移动语义"><a href="#移动语义" class="headerlink" title="移动语义"></a>移动语义</h3><p>当我们写下一条赋值语句时，很多时候，实际上是完成了一次拷贝操作，也就是将数据复制一份，放入新声明的对象内存中。当数据量比较大时，并且赋值之后，原始的数据将不会再使用时，这种复制的代价会很大也很没必要。<br>直觉的理解，就是直接把地址复用一下，就能避免复制一份的开销，这种行为看起来像把数据 “移动” 到新的对象内存中，从而提高了性能。</p><p>在 Modern C++ 中，我们使用 <code>std::move</code> 这个操作，来支持移动语义。</p><p>然而，C++ 烦人的地方就在于，很多你看似显而易见的概念，实际上却暗藏玄机。比如，<code>std::move</code> 并没有移动任何东西，后边提到的 <code>std::forward</code> 也并没有转发任何东西。事实上，他们在运行期什么也不做，一个字节都不会生成。</p><p><code>std::move</code> 也不一定总是能保证会产生移动行为，它实际上只是一个强制类型转换，无条件将实参转换为右值引用。</p><p>我认为，如果你敢于说出自己 “熟练掌握” C++，那么，一定可以盲写出 <code>std::move</code> 的实现函数，因为它真的很简单：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">template</span> &lt;<span class="keyword">typename</span> T&gt; </span><br><span class="line"><span class="keyword">typename</span> remove_reference&lt;T&gt;::<span class="function">type&amp;&amp; <span class="title">move</span><span class="params">(T&amp;&amp; param)</span> </span>&#123; </span><br><span class="line">  <span class="keyword">using</span> ReturnType = <span class="keyword">typename</span> remove_reference&lt;T&gt;::type&amp;&amp;; </span><br><span class="line">  <span class="keyword">return</span> <span class="built_in">static_cast</span>&lt;ReturnType&gt;(param); </span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>代码中，核心是 <code>static_cast</code>，它将形参 <code>param</code> 转换为另一个类型。形参是一个万能引用，而不是一个右值引用（虽然有两个 <code>&amp;&amp;</code>），它即可以绑定左值引用，也可以绑定右值引用。函数返回的类型，首先使用 <code>remove_reference</code> 去除所有引用属性，然后取其类型后，再转变为右值引用，所以，返回类型一定是原始类型去掉引用之后的右值引用类型。</p><p>C++ 14 中的实现更加简洁：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">template</span> &lt;<span class="keyword">typename</span> T&gt; </span><br><span class="line"><span class="function"><span class="keyword">decltype</span>(<span class="keyword">auto</span>) <span class="title">move</span><span class="params">(T&amp;&amp; param)</span> </span>&#123;</span><br><span class="line">  <span class="keyword">using</span> ReturnType = remove_reference&lt;T&gt;&amp;&amp;; </span><br><span class="line">  <span class="keyword">return</span> <span class="built_in">static_cast</span>&lt;ReturnType&gt;(param); </span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>从源码中可以确定，<code>std::move</code> 确实是一个类型转换。</p><p>那为什么它和 “移动” 扯上关系呢？</p><p>我们的编译器，会在明确一个类型是一个右值时，调用这个类型的移动语义（移动构造函数和移动赋值运算符），而移动语义通常是更轻量级的实现，比如拷贝地址而不是拷贝值。<code>std::move</code> 帮我们确保将类型变成右值引用，从而 “尝试” 触发移动语义。</p><p>注意，我这里使用了 “尝试” 这个词，是因为 <code>std::move</code> 不保证一定会触发移动，因为它只能保证返回的是一个右值引用，两者之间还隔着一个编译器行为。书中一个典型的反例是：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Obj</span> &#123; </span><br><span class="line"><span class="keyword">public</span>: </span><br><span class="line">  <span class="built_in">Obj</span>(<span class="type">const</span> std::string text) : <span class="built_in">value</span>(std::<span class="built_in">move</span>(text)) &#123;&#125; </span><br><span class="line"><span class="keyword">private</span>:   </span><br><span class="line">  std::string value; </span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>这段代码中，构造函数的实参，到对象中的数据成员 <code>value</code>，就发生了拷贝而不是移动。原因是，构造函数形参是 <code>const std::string</code>，如果将一个 <code>const</code> 属性的数据，移动到一个非 <code>const</code> 属性的数据，那就会出现本不希望修改的东西可能被修改，编译器不接受这种行为，所以还是会拷贝而不是移动。</p><p>更专业的解释是，<code>std::string</code> 中只提供 <code>const string&amp;</code> 作为形参类型的拷贝构造函数和以 <code>string&amp;&amp;</code> 作为形参类型的移动构造函数，所以 <code>value</code> 的构造中，被 <code>std::move</code> 处理后的实参 <code>std::move(text)</code>，其类型虽然是 <code>const std::string&amp;&amp;</code>，但在实例化 <code>value</code> 时，选择了拷贝构造函数（常量右值引用退化为常量左值引用）。</p><blockquote><p><strong>谬误</strong>：<code>std::move</code> 只保证返回的是一个右值引用，而不保证一定发生移动语义。<br><strong>陷阱</strong>：针对常量对象的移动操作，一定会悄无声息的退化为拷贝操作。</p></blockquote><p>理论上来说，<code>std::move</code> 这个命名是有歧义的，更合适的命名是 <code>rvalue_cast</code> 之类，然而，当时 C++ 标准委员会考虑的可能是尽可能不让用户感知更下层的细节，避免陷入语法漩涡中。我个人的意见认为，既然 C++ 无法在语法层面掩藏 “右值引用” 的概念，那就不应该在 <code>std::move</code> 这里多虑，选择暴露细节并让用户权衡，或者选择隐藏细节和提供封装，二者择一即可，而不是既要又要，却引入了尴尬的歧义。</p><p>同样的，<code>std::move</code> 看似和 “移动” 千丝万缕，但即使真的发生了移动行为，移动之后的原对象，其依然是有效的，程序员需要手动关注移动后的原对象状态，比如在移动构造中将类型成员指针赋值 <code>nullptr</code>。否则，状态是未定义的。</p><p>另外，一个和 Rust 对比的有趣细节，C++ 中传递非引用类型参数时，默认的行为是拷贝，若类型实现了移动语义，且实参是右值（如临时对象或手动使用 <code>std::move</code> 的结果），传参行为变成移动；Rust 中默认的传参行为是移动（转移所有权），当类型实现 Copy trait 或手动调用 <code>.clone()</code> 方法时，传参行为变成拷贝。可见，Rust 通过默认移动语义保证了安全性和性能，而 C++ 的默认拷贝行为则需要开发者主动留意安全性和性能。</p><p>比如，在 C++ 代码中：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="function">Widget <span class="title">w</span><span class="params">(<span class="number">42</span>)</span></span>; </span><br><span class="line">Widget w2 = std::<span class="built_in">move</span>(w);  <span class="comment">// 假设真的触发了移动行为 </span></span><br><span class="line">std::cout &lt;&lt; w.<span class="built_in">getValue</span>(); <span class="comment">// 可能是垃圾值或者程序奔溃，取决于 Widget 实现</span></span><br></pre></td></tr></table></figure><p>在 Rust 代码中：</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> <span class="variable">w</span> = Widget::<span class="title function_ invoke__">new</span>(<span class="number">42</span>); </span><br><span class="line"><span class="keyword">let</span> <span class="variable">w2</span> = w;                   <span class="comment">// 一定发生了移动行为 </span></span><br><span class="line"><span class="built_in">println!</span>(<span class="string">&quot;&#123;&#125;&quot;</span>, w.<span class="title function_ invoke__">getValue</span>()); <span class="comment">// 编译错误</span></span><br></pre></td></tr></table></figure><h3 id="完美转发"><a href="#完美转发" class="headerlink" title="完美转发"></a>完美转发</h3><p>之前提到过，函数形参不可能是一个右值类型，所以，如果函数调用时实参是一个右值引用，那函数形参就丢失了右值引用属性，对于 <code>void func(Widget&amp;&amp; w)</code> 来说，当 <code>func</code> 内部操作 <code>w</code> 时，就会当作一个左值引用来处理。为了能让 <code>func</code> 内部的 <code>w</code> 也是和实参一样的右值引用，使用完美转发便可以做到。</p><p>在 Modern C++ 中，我们使用 <code>std::forward</code> 这个操作，来支持完美转发。</p><p><code>std::forward</code> 是一个 “有条件” 的强制类型转换。只有当函数形参是万能引用类型，实参是一个右值时，会把形参强制转换为一个右值类型。</p><p>它的常用场景是，如果我们希望把调用函数时的右值实参的右值性，保留在函数内部（而不是转化为左值），就去使用它。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">void</span> <span class="title">process</span><span class="params">(<span class="type">const</span> Widget&amp; arg)</span></span>; <span class="comment">// 接受左值作为参数 </span></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">process</span><span class="params">(Widget&amp;&amp; arg)</span></span>;      <span class="comment">// 接受右值作为参数 </span></span><br><span class="line"></span><br><span class="line"><span class="keyword">template</span> &lt;<span class="keyword">typename</span> T&gt; </span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">func</span><span class="params">(T&amp;&amp; param)</span> </span>&#123; </span><br><span class="line">  <span class="built_in">process</span>(std::forward&lt;T&gt;(param)); </span><br><span class="line">&#125; </span><br><span class="line"></span><br><span class="line"><span class="comment">// 使用时 </span></span><br><span class="line">Widget w; </span><br><span class="line"><span class="built_in">func</span>(w);            <span class="comment">// a. w 是一个左值 </span></span><br><span class="line"><span class="built_in">func</span>(std::<span class="built_in">move</span>(w)); <span class="comment">// b. w 被转换为一个右值</span></span><br></pre></td></tr></table></figure><p>使用时，对于 a 情况，w 作为左值传入 <code>func</code>，万能引用形参的模板被实例化为左值（T 的类型是 <code>Widget&amp;</code>，依据引用折叠规则，<code>param</code> 的类型是 <code>Widget&amp;</code>），所以 <code>std::forward</code> 什么也不会做，最终调用的是 <code>void process(const Widget&amp; arg)</code> 重载版本；对于 b 情况，w 作为右值传入 <code>func</code>，形参被实例化为右值（T 的类型是 <code>Widget</code>，<code>param</code> 的类型是 <code>Widget&amp;&amp;</code>，<strong>注意它此时是左值</strong>），所以 <code>std::forward</code> 会将其转换为右值，传入 <code>process</code> 中，最终调用的是<code>void process(Widget&amp;&amp; arg)</code>。</p><p>如果对这块类型推导不清楚，可以回到第一章，再看看模板类型推导的规则，并结合后边提到的引用折叠来理解。<br>如果读完该条款，还是没有理解万能引用，那么本章后续的内容对你可能太深奥了，万能引用只是后续模板化编程的门槛石，还是先从其他地方搞明白这些概念为好。</p><p>最后做一个总结，如果你想要将一个左值尝试通过移动的方式交给函数内部，就使用 <code>std::move</code>；如果你在函数内，希望将外边传入的右值（函数内部变成左值），保持其右值性质，就使用 <code>std::forward</code>。但要记住，两者本质都是类型转换。</p><h2 id="条款-24：区分万能引用和右值引用"><a href="#条款-24：区分万能引用和右值引用" class="headerlink" title="条款 24：区分万能引用和右值引用"></a>条款 24：区分万能引用和右值引用</h2><p>当我们在代码中看到一个类型的声明中带有 <code>&amp;&amp;</code> 的时候，按最初的学习内容，会认为这是一个右值引用，因为左值引用是 <code>&amp;</code> 来标记的。然而，还有一种叫万能引用的东西。</p><p>万能引用本身的概念很简单，它既可以绑定左值，也可以绑定右值，所以它既可能是一个左值引用，也可能是一个右值引用，具体是哪种，取决于实例化时的入参。</p><p>读懂代码，首先要能分得清万能引用：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">void</span> <span class="title">f</span><span class="params">(Object&amp;&amp; param)</span></span>;   <span class="comment">// 右值引用  </span></span><br><span class="line">Object&amp;&amp; var = <span class="built_in">Object</span>();  <span class="comment">// 右值引用 </span></span><br><span class="line"><span class="keyword">auto</span>&amp;&amp; var2 = var;        <span class="comment">// 万能引用 </span></span><br><span class="line"><span class="keyword">template</span> &lt;<span class="keyword">typename</span> T&gt; <span class="function"><span class="type">void</span> <span class="title">f</span><span class="params">(T&amp;&amp; param)</span></span>;              <span class="comment">// 万能引用 </span></span><br><span class="line"><span class="keyword">template</span> &lt;<span class="keyword">typename</span> T&gt; <span class="function"><span class="type">void</span> <span class="title">f</span><span class="params">(std::vector&lt;T&gt;&amp;&amp; param)</span></span>; <span class="comment">// 右值引用 </span></span><br><span class="line"><span class="type">const</span> <span class="keyword">auto</span>&amp;&amp; var3 = var;  <span class="comment">// 右值引用</span></span><br></pre></td></tr></table></figure><p>区分万能引用的依据是：</p><ol><li>类型都涉及类型推导；</li><li>不能带有 cv 属性；</li><li>必须是类型本身的推导，而不是二次推导的结果（比如 <code>std::vector&lt;T&gt;</code>）；</li><li>模板类中的函数，只有自身是一个模板函数；</li></ol><p>有关于最后一点的简单解释，书中的示例为：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">template</span> &lt;<span class="keyword">typename</span> T, <span class="keyword">class</span> <span class="title class_">Allocator</span> = allocator&lt;T&gt;&gt; </span><br><span class="line"><span class="keyword">class</span> vector &#123; </span><br><span class="line"><span class="keyword">public</span>: </span><br><span class="line">  <span class="type">void</span> <span class="built_in">push_back</span>(T&amp;&amp; x); <span class="comment">// 它是右值引用，而不是万能引用 </span></span><br><span class="line">  <span class="function"><span class="type">void</span> <span class="title">push_back</span><span class="params">(<span class="type">const</span> T&amp;&amp; x)</span></span>; <span class="comment">// 右值引用 </span></span><br><span class="line">  <span class="keyword">template</span> &lt;<span class="keyword">typename</span>... Args&gt; <span class="function"><span class="type">void</span> <span class="title">emplace_back</span><span class="params">(Args&amp;&amp;... args)</span></span>; <span class="comment">// 万能引用 </span></span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>这是标准库容器 <code>vector</code> 的实现，其中 <code>push_back</code> 中的 <code>T&amp;&amp;</code> 虽然满足万能引用的语法形式，实际上却是右值引用；而 <code>emplace_back</code> 参数才是万能引用，<code>Args&amp;&amp;</code> 是属于成员函数自己的模板类型。</p><blockquote><p><strong>谬误</strong>：符合 <code>T&amp;&amp;</code> 形式的模板参数类型，不一定是万能引用。</p></blockquote><p>记住这些规则没有意义，编译器也不会去用这种类型区分什么是万能引用。事实上万能引用只是一个抽象。它的本质是引用折叠，编译器使用引用折叠规则来推导这个引用类型是左值引用还是右值引用。</p><p>引用折叠的本质是第一章中提到的模板类型推导，包括 <code>auto</code> 类型推导，在后边会继续介绍。</p><p>虽然万能引用是一种抽象，但依然有必要了解什么是万能引用，一方面，是可以更方便的和其他开发成员沟通，另一方面，也能更好的读懂代码，写出高质量的代码。</p><h2 id="条款-25：灵活使用-std-move-和-std-forward"><a href="#条款-25：灵活使用-std-move-和-std-forward" class="headerlink" title="条款 25：灵活使用 std::move 和 std::forward"></a>条款 25：灵活使用 std::move 和 std::forward</h2><p>这一章节我看地很费劲，感觉一个很简单的知识点，翻译出来的内容非常晦涩难懂。</p><p>只要已经掌握本章节前边几个条款的介绍，这个条款的内容基本可以忽略了，都是重复的内容。</p><p>简单来说，当需要拿到一个右值引用时，使用 <code>std::move</code>，当需要拿到一个万能引用（也就是想要在入参的实参是右值时，才获得右值）时，使用 <code>std::forward</code>。</p><p>书中给出了一种错误地把 <code>std::forward</code> 替换为 <code>std::move</code> 导致的问题。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Widget</span> &#123; </span><br><span class="line"><span class="keyword">public</span>: </span><br><span class="line">  <span class="keyword">template</span> &lt;<span class="keyword">typename</span> T&gt; </span><br><span class="line">  <span class="function"><span class="type">void</span> <span class="title">setName</span><span class="params">(T&amp;&amp; newName)</span> </span>&#123;      </span><br><span class="line">    name = std::<span class="built_in">move</span>(newName);   <span class="comment">// 这里使用了 std::move </span></span><br><span class="line">  &#125; </span><br><span class="line"><span class="keyword">private</span>:   </span><br><span class="line">  std::string name; </span><br><span class="line">&#125;;  </span><br><span class="line"></span><br><span class="line">Widget w; </span><br><span class="line">std::string str = <span class="string">&quot;name&quot;</span>; </span><br><span class="line">w.<span class="built_in">setName</span>(str);   <span class="comment">// 这里将左值 str 传入</span></span><br></pre></td></tr></table></figure><p>这个例子中，<code>str</code> 是一个左值，但在 <code>setName</code> 函数中，通过 <code>std::move</code> 将其转换为右值。编译器会认为，外边的 <code>str</code> 已经是一个无效的值，在内部调用了 <code>std::string</code> 的移动赋值将内容放到了成员 <code>name</code> 中。<br>将 <code>std::move</code> 替换为 <code>std::forward</code> 就可以保证不出现意外。</p><p>另外一个值得聊的话题，是返回值优化（RVO）。考虑一下示例代码：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="function">Widget <span class="title">makeWidget1</span><span class="params">()</span> </span>&#123; </span><br><span class="line">  Widget w; </span><br><span class="line">  <span class="keyword">return</span> w; </span><br><span class="line">&#125; </span><br><span class="line"></span><br><span class="line"><span class="function">Widget <span class="title">makeWidget2</span><span class="params">()</span> </span>&#123; </span><br><span class="line">  Widget w; </span><br><span class="line">  <span class="keyword">return</span> std::<span class="built_in">move</span>(w); </span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>粗略地看，我们会认为第一版函数中，返回时会发生复制构造，而第二版函数中，会发生移动构造（假设 <code>Widget</code> 实现了移动语义）；然而，实际上第一版中，发生了移动构造，而第二版中，发生了复制构造。</p><p>究其原因，是因为第一版代码中，满足编译器的返回值优化，编译器会在函数外边的栈上分配 <code>Widget</code> 的内存，并将构造直接放在外边，从而避免复制操作；而在第二版中，由于手动使用了 <code>std::move</code> 操作，它返回的是右值引用，编译器不得不为引用对象生成一个原始对象，从而阻碍了编译器去做返回值优化。</p><p>有关于返回值优化不是特别难懂的一个知识点，可以从其他地方了解更多细节。</p><h2 id="条款-26：避免在重载函数中使用到万能引用类型的形参"><a href="#条款-26：避免在重载函数中使用到万能引用类型的形参" class="headerlink" title="条款 26：避免在重载函数中使用到万能引用类型的形参"></a>条款 26：避免在重载函数中使用到万能引用类型的形参</h2><p>重载是很常见的 C++ 应用技巧，万能引用作为一种函数形参的类型，自然也可以当作重载的一种实现，然而，当你这么做时，就开始掉入一个有点危险的坑里边。</p><p>考虑这样一个场景：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">std::vector&lt;std::string&gt; names; </span><br><span class="line"></span><br><span class="line"><span class="comment">// 万能引用版本的函数 </span></span><br><span class="line"><span class="keyword">template</span> &lt;<span class="keyword">typename</span> T&gt; </span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">func</span><span class="params">(T&amp;&amp; name)</span> </span>&#123; </span><br><span class="line">  names.<span class="built_in">emplace_back</span>(std::forward&lt;T&gt;(name));</span><br><span class="line">&#125; </span><br><span class="line"></span><br><span class="line"><span class="comment">// 调用代码 </span></span><br><span class="line"><span class="function">std::string <span class="title">name</span><span class="params">(<span class="string">&quot;name&quot;</span>)</span></span>; </span><br><span class="line"><span class="built_in">func</span>(name);   <span class="comment">// name 是一个 std::string 的左值，调用到了万能引用版本 </span></span><br><span class="line"><span class="built_in">func</span>(std::<span class="built_in">string</span>(<span class="string">&quot;name&quot;</span>));   <span class="comment">// 实参是 std::string 的右值，也会调用到万能引用版本 </span></span><br><span class="line"><span class="built_in">func</span>(<span class="string">&quot;name&quot;</span>); <span class="comment">// 字面量字符串，也会作为右值，调用到万能引用版本</span></span><br></pre></td></tr></table></figure><p>我们可能最初希望以上述这种代码来实现功能，事实上工作地很好，第一个调用是以左值传入，会执行一次复制构造；第二个调用是以右值传入，在万能引用的加持下，实际执行了 <code>string</code> 的移动构造；第三个是字面量，编译器事实上会利用 <code>emplace</code> 的移动构造，直接在 <code>names</code> 的内存位置构造一个 <code>string</code>，省去了在 <code>func</code> 中的一次 <code>string</code> 形参实例化。</p><p>然而，当我们有一个新的需求，需要添加一个重载版本的 <code>func</code>，问题就出现了：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 另一个同名的重载函数，形参为 int </span></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">func</span><span class="params">(<span class="type">int</span> idx)</span> </span>&#123; </span><br><span class="line">  names.<span class="built_in">emplace_back</span>(<span class="built_in">find_name</span>(idx)); </span><br><span class="line">&#125; </span><br><span class="line"></span><br><span class="line"><span class="comment">// 调用代码 </span></span><br><span class="line"><span class="built_in">func</span>(<span class="number">1</span>); <span class="comment">// 调用到了 func(int) 这个重载版本，没有问题 </span></span><br><span class="line"><span class="type">short</span> idx = <span class="number">1</span>; </span><br><span class="line"><span class="built_in">func</span>(idx); <span class="comment">// 编译器报错！</span></span><br></pre></td></tr></table></figure><p>讨论为什么第二个调用，没有按预期调用到 <code>func(int)</code> 的重载版本，而是报错，便是本条款要引出的问题。</p><p>实际很容易理解，第一个调用，常量 1 会被编译器当作 <code>int</code> 类型，直接匹配到 <code>func(int)</code>，而传入 <code>short idx</code>，在决策使用哪个重载版本时，却使用了万能引用所在的模板函数的实例化，即：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 万能引用的模板函数，以 short 实例化后的结果 </span></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">func</span><span class="params">(<span class="type">short</span> name)</span> </span>&#123; </span><br><span class="line">  names.<span class="built_in">emplace_back</span>(name); </span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>当编译器发现，以万能引用作为参数的模板函数，实例化后的版本，能够完美匹配时，相比于另一个 <code>func(int)</code> 版本还需要类型转换，自然使用万能引用模板函数的实例化更合理。而我们也发现，<code>names</code> 的类型，是无法接收 <code>short</code> 作为构造函数参数的（<code>string</code> 类型不能通过 <code>short</code> 构造），所以编译器就报错了。</p><p>实际上的编译器报错可能会输出一大串似乎没有什么用的信息，这是 C++ 模板出现问题时，不人性化的体现。于是，当编译报错出现时，很可能不太容易发现是这里的问题。</p><p>依据 C++ 规范，编译器会认为，模板实例化后的版本，没有任何的额外成本，所以这么选择是合理的，自然不能去怪编译器。所以，也就是本条款要提的主题，避免为万能引用作为形参的函数，提供重载版本。形参为万能引用的函数，是重载版本中最贪婪的。</p><p>之所以第一个版本：<code>func(1)</code> 没有问题，是因为 C++ 也规定了，如果模板实例化后的版本和另一个重载的普通函数是一样的，那么优先使用普通函数的版本。</p><blockquote><p><strong>陷阱</strong>：盲目使用万能引用而不顾及其他，可能会带来潜在问题。</p></blockquote><p>当发现一个问题时，最好能举一反三。书中便进一步继续这个话题。当万能引用的版本作为类的构造函数，又会发生什么现象。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Person</span> &#123; </span><br><span class="line"><span class="keyword">public</span>: </span><br><span class="line">  <span class="keyword">template</span> &lt;<span class="keyword">typename</span> T&gt; <span class="built_in">Person</span>(T&amp;&amp; n) : <span class="built_in">name</span>(n) &#123;&#125; </span><br><span class="line">  <span class="built_in">Person</span>(<span class="type">int</span> i) : <span class="built_in">name</span>(<span class="built_in">find_name</span>(i)) &#123;&#125; </span><br><span class="line"><span class="keyword">private</span>:   </span><br><span class="line">  std::string name; </span><br><span class="line">&#125;; </span><br><span class="line"></span><br><span class="line"><span class="comment">// 调用代码 </span></span><br><span class="line"><span class="function">Person <span class="title">p1</span><span class="params">(<span class="string">&quot;name&quot;</span>)</span></span>; </span><br><span class="line"><span class="function"><span class="keyword">auto</span> <span class="title">p2</span><span class="params">(p1)</span></span>; <span class="comment">// 编译报错！</span></span><br></pre></td></tr></table></figure><p>这里第一眼看上去，报错很匪夷所思，我们希望调用到 <code>Person</code> 的复制构造函数，为什么就出错呢？<br>原因还是在于万能引用，我们提供的万能引用版本的构造函数，实际上可以实例化出来一个类似复制构造函数的版本：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 实例化为 </span></span><br><span class="line">Person::<span class="built_in">Person</span>(Person&amp; p) : <span class="built_in">name</span>(p) &#123;&#125;</span><br></pre></td></tr></table></figure><p>万万没想到，本来只是打算通过万能引用的构造函数，来接收一个 <code>name</code> 作为参数，结果却把 <code>Person</code> 对象也实例化出来了。</p><p>而我们也知道，编译器生成的默认复制构造函数，其参数是带有 <code>const</code> 属性的：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 编译器隐式生成的复制构造函数 </span></span><br><span class="line">Person::<span class="built_in">Person</span>(<span class="type">const</span> Person&amp; p) : <span class="built_in">name</span>() &#123;&#125;</span><br></pre></td></tr></table></figure><p>编译器在实例化万能引用的模板构造函数后，经过权衡，发现使用模板实例化的版本，不需要额外添加 <code>const</code> 属性，所以选择了这个模板实例化的版本。然后，我们便看到了将 <code>name</code> 这个 <code>string</code> 类型的对象，使用 <code>Person</code> 对象（形参）来构造，自然无法实施，导致报错。</p><p>事实上，如果调用代码中，采用以下实现：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">const</span> Person <span class="title">p3</span><span class="params">(<span class="string">&quot;name&quot;</span>)</span></span>; </span><br><span class="line"><span class="function"><span class="keyword">auto</span> <span class="title">p4</span><span class="params">(p3)</span></span>; <span class="comment">// 正常，调用复制构造函数</span></span><br></pre></td></tr></table></figure><p>问题便不存在了，原因和之前非类内的阐述是一样的。</p><p>当这个话题出现在继承结构中，也会出现类似的问题。假设有个类继承自 <code>Person</code>：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">SpecialPerson</span> : <span class="keyword">public</span> Person &#123; </span><br><span class="line"><span class="keyword">public</span>: </span><br><span class="line">  <span class="built_in">SpecialPerson</span>(<span class="type">const</span> SpecialPerson&amp; rhs) : <span class="built_in">Person</span>(rhs) &#123; ... &#125; </span><br><span class="line">  <span class="built_in">SpecialPerson</span>(SpecialPerson&amp;&amp; rhs) : <span class="built_in">Person</span>(std::<span class="built_in">move</span>(rhs)) &#123; ... &#125; </span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>代码中，对 <code>SpecialPerson</code> 的复制构造函数和移动构造函数中，对 <code>Person</code> 的构造，实际上都调用了基类 <code>Person</code> 的完美转发构造函数，并都会编译失败。</p><p>在这一条款中得到的经验就是，尽可能不要去把万能引用参数的函数去做重载。在下一条款，我们会深入介绍几个技术，来迎着困难而上。</p><blockquote><p><strong>陷阱</strong>：小心不要把万能引用参数的函数去做函数重载，很可能在调用时，会得到错误的重载版本。</p></blockquote><h2 id="条款-27：重载中出现万能引用时的替代方案"><a href="#条款-27：重载中出现万能引用时的替代方案" class="headerlink" title="条款 27：重载中出现万能引用时的替代方案"></a>条款 27：重载中出现万能引用时的替代方案</h2><p>针对 <strong>条款 26</strong>，本条款提供了一些可以解决前述问题的替代方案。</p><h3 id="方案-1：放弃重载"><a href="#方案-1：放弃重载" class="headerlink" title="方案 1：放弃重载"></a>方案 1：放弃重载</h3><p>当出现万能引用时，不要使用重载，而是用不同的函数名称来实现功能。</p><p>这是一种逃避策略，但逃避不一定意味着不好。事实上当你看完整个条款后，再回过头，会发现这种方案反而是最直接也是最推荐的做法，至少在大多数项目中，与其设计复杂的、高端的编程技巧，远不如设计简单的、易维护的代码。</p><h3 id="方案-2：将万能引用改为左值引用"><a href="#方案-2：将万能引用改为左值引用" class="headerlink" title="方案 2：将万能引用改为左值引用"></a>方案 2：将万能引用改为左值引用</h3><p>既然两个特性冲突时容易有问题，除了放弃重载，自然也可以放弃万能引用。将万能引用类型全部改为常量左值引用。<br>这种做法失去了万能引用的性能优势，但如果放弃性能能带来代码的简洁性和安全性，也不失为一种可斟酌的方案。</p><h3 id="方案-3：将引用改为传值"><a href="#方案-3：将引用改为传值" class="headerlink" title="方案 3：将引用改为传值"></a>方案 3：将引用改为传值</h3><p>虽然我们使用引用是为了改进性能，但有些时候，即便使用传值，也并不是带来额外的性能开销。在 <strong>条款 41</strong> 中会详细阐述这个问题。</p><p>实践经验中，当知道什么情况下引用类型可以改进性能时，也就有能力去判断什么情况下，传值也能带来类似的效果，反而还规避了之前的问题。</p><h3 id="方案-4：标签分派"><a href="#方案-4：标签分派" class="headerlink" title="方案 4：标签分派"></a>方案 4：标签分派</h3><p>前边 3 个方案都可以认为是逃避式的方案，不过确实有一些直接解决问题的方案。</p><p>重载决议时，编译器会考察所有重载版本的形参和传入的实参，匹配全局的最优函数。既然万能引用很强大，总是能优先吸引编译器选择自己所在的重载版本，那么我们可以给重载函数多加一个参数，用额外的这个参数来制约万能引用形参的吸引力。</p><p>我们重新修改 <strong>条款 26</strong> 中的重载函数，为了保证对外接口不变，将添加额外参数的函数设置为其内部的子函数，也将重载这个内部子函数：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line">std::vector&lt;std::string&gt; names; </span><br><span class="line"></span><br><span class="line"><span class="comment">// 万能引用版本的函数 </span></span><br><span class="line"><span class="keyword">template</span> &lt;<span class="keyword">typename</span> T&gt; </span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">func</span><span class="params">(T&amp;&amp; name)</span> </span>&#123; </span><br><span class="line">  <span class="built_in">funcImpl</span>(std::forward&lt;T&gt;(name),             </span><br><span class="line">           std::is_integral&lt;<span class="keyword">typename</span> std::remove_reference&lt;T&gt;::type&gt;()); <span class="comment">// 注意这里 </span></span><br><span class="line">  <span class="comment">// 在 C++14 中，写法可以更简单 </span></span><br><span class="line">  <span class="comment">// funcImpl(std::forward&lt;T&gt;(name), </span></span><br><span class="line">  <span class="comment">//          std::is_integral&lt;std::remove_reference_t&lt;T&gt;()); </span></span><br><span class="line">&#125; </span><br><span class="line"></span><br><span class="line"><span class="comment">// 内部的子函数，添加 tag 控制重载决议 </span></span><br><span class="line"><span class="keyword">template</span> &lt;<span class="keyword">typename</span> T&gt; </span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">funcImpl</span><span class="params">(T&amp;&amp; name, std::false_type)</span> </span>&#123; </span><br><span class="line">  names.<span class="built_in">emplace_back</span>(std::forward&lt;T&gt;(name)); </span><br><span class="line">&#125; </span><br><span class="line"><span class="keyword">template</span> &lt;<span class="keyword">typename</span> T&gt; </span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">funcImpl</span><span class="params">(<span class="type">int</span> idx, std::true_type)</span> </span>&#123; </span><br><span class="line">  names.<span class="built_in">emplace_back</span>(<span class="built_in">find_name</span>(idx)); </span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这个额外的形参 <code>std::false_type</code> 和 <code>std::true_type</code> 被称为 tag，基于这种方式的重载决议也被叫做 <strong>标签分派（tag dispatch）</strong>。</p><p>代码中 <code>std::is_integral&lt;T&gt;</code> 和 <code>std::remove_reference&lt;T&gt;</code> 是类型特征（type traits），之前条款中已经出现过，C++ 的类型特征很多，它们用于对类型做处理或者判断。本例子中，首先注意到第二个 tag 参数返回的是一个 “是否是整形类型” 的判断。因为类型特征是一个编译期行为，所以判断的结果，也应该是一个静态的状态，所以 C++ 标准库中提供了名为 <code>std::false_type</code> 和 <code>std::true_type</code> 这样的静态类型。注意到，这些 tag 并没有形参的名字，所以它们在运行期不会起作用，编译器会在生成程序时，去除掉这种形参（即使不去掉也不影响我们理解代码）。</p><p>继续观察 <code>std::is_integral&lt;T&gt;</code> 中的 <code>T</code>，为了保证无论传入的类型是 <code>T</code> 还是 <code>T&amp;</code> 或 <code>T&amp;&amp;</code>，都应该去处理，使用了 <code>std::remove_reference&lt;T&gt;</code> 来移除引用属性。</p><p>最终，在重载决议时，如果类型 <code>T</code> 是一个整形，那么 tag 实参的判断就是 <code>true_type</code>，会选中第二个（第一个形参为 <code>int</code>）重载版本；否则，tag 判断是 <code>false_type</code>，会选中第一个（第一个形参为 <code>T&amp;&amp;</code>）重载版本。将 tag 作为重载函数参数，掩盖了万能引用影响重载决议的吸引力，最终达到我们的目的。</p><p>说句题外话，C++ 的很多复杂性的演化动力，就来自于这种不断对现有技术中的缺陷进行更多设计和改善的需求中。换句话说，不断地用一个更复杂的补丁，弥补之前的不足。</p><h3 id="方案-5：处理类构造函数中存在万能引用的问题"><a href="#方案-5：处理类构造函数中存在万能引用的问题" class="headerlink" title="方案 5：处理类构造函数中存在万能引用的问题"></a>方案 5：处理类构造函数中存在万能引用的问题</h3><p>在 <strong>条款 26</strong> 中，还提到了如果类构造函数中使用了万能引用，调用构造函数时，本欲调用复制构造函数，却实际调用到了万能引用版本的构造函数。方案 4 无法解决这个问题，因为复制构造函数有可能是编译器自己生成的，所以没办法用方案 4 中，实现一个子函数来做重载。</p><p>万能的 C++ 当然考虑到了这一点，实际上，标准库中很多代码都面临这个问题，学习这块知识有助于我们去阅读学习标准库中的代码，也有助于理解和读懂编译报错时的信息。这部分会比较复杂，但也依然是类型特征的范畴。</p><p>由于没办法自己生成带有标签分派的重构函数，所以只能另寻他法，这里引出一个新的东西：<code>std::enable_if</code>。</p><p>如果你看过一些标准库或复杂的 C++ 项目，就应该已经熟悉它，它可以指定编译器处理一个模板实例化时的条件。默认的模板总是使能的，但如果 <code>enable_if</code> 中的条件不满足，那么模板将会被禁用，有关于完整的原理这里不展开（可以在网上查一下 SFINAE）。针对我们的问题，实际上，我希望在带有万能引用形参的模板中，加入这个功能，控制仅当满足特定条件时才启用该模板。下边，我们将讨论指定什么条件可以达成我们的目的。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Person</span> &#123; </span><br><span class="line"><span class="keyword">public</span>: </span><br><span class="line">  <span class="keyword">template</span> &lt;<span class="keyword">typename</span> T,</span><br><span class="line">            <span class="keyword">typename</span> = <span class="keyword">typename</span> std::enable_if&lt; </span><br><span class="line">                         !std::is_same&lt;Person,</span><br><span class="line">                                       <span class="keyword">typename</span> std::decay&lt;T&gt;::type</span><br><span class="line">                                       &gt;::value</span><br><span class="line">                         &gt;::type</span><br><span class="line">             &gt;</span><br><span class="line">  <span class="built_in">Person</span>(T&amp;&amp; name); </span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>这段代码中的条件很复杂，如果之前没看过这个代码，也没关系，现在一起来看一下。代码按关键位置做了换行，以方便阅读。</p><p>简单来说，模板参数的第二项：<code>typename = typename std::enable_if&lt;...&gt;::type</code> 是对 <code>enable_if</code> 的使用，当满足 <code>...</code> 中的条件时，该模板被启用；</p><p>展开里边的结构：<code>!std::is_same&lt;...&gt;::value</code> 是启用条件，也是一个类型特征，它的语义为，当 <code>...</code> 中指定的两个类型不同时，模板被启用；</p><p>再继续展开：<code>Person, typename std::decay&lt;T&gt;::type</code>，指定了两个要比较的类型，第一个是 <code>Person</code> 类，第二个是另一个类型特征，<code>std::decay&lt;T&gt;::type</code> 的语义是，将类型 <code>T</code> 的引用属性、CV 特性（const 和 volatile）、数组和函数等类型，简化为最简单的形式。换句话说，比如 <code>T&amp;</code> 或 <code>T&amp;&amp;</code>，<code>const T&amp;</code> 或 <code>const T&amp;&amp;</code> 等等类型，经过该类型特征，输出都是 <code>T</code>。</p><p>总结起来，这个代码设定的就是，当某个类型 <code>T</code>，将其简化后（经过 <code>decay</code>），和 <code>Person</code> 不同时，该万能引用作为形参的构造函数，将被启用；否则，不会被启用。那么，针对上一个条款中的问题，当遇到：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="function">Person <span class="title">p1</span><span class="params">(<span class="string">&quot;name&quot;</span>)</span></span>; </span><br><span class="line"><span class="function"><span class="keyword">auto</span> <span class="title">p2</span><span class="params">(p1)</span></span>;</span><br></pre></td></tr></table></figure><p>便可正常编译，因为 <code>p1</code> 被推断为 <code>Person</code> 类型，万能引用模板构造函数被禁用了，编译器只能找到复制构造函数。</p><p>以上我们已经开始逐步地接触模板元编程（TMP），感兴趣可以到其他地方学习。类型特征是模板元编程中一块重要的内容，可以理解为是类型的类型，在 Rust 等编程语言中，被称为特征 trait。要想熟练模板元编程，对类型特征的集合要有一定的熟悉度，这在大多数 C++ 教材中，都不会大篇幅介绍。</p><h3 id="方案-6：处理继承结构中遇到的问题"><a href="#方案-6：处理继承结构中遇到的问题" class="headerlink" title="方案 6：处理继承结构中遇到的问题"></a>方案 6：处理继承结构中遇到的问题</h3><p>在继承结构中，比如 <strong>条款 26</strong> 中最后的 <code>SpecialPerson</code> 示例，目前使用方案 5 还是无法解决。因为 <code>decay</code> 无法将一个派生类型简化为其基类类型，导致 <code>enable_if</code> 通过。</p><p>C++ 标准当然为我们考虑到了这一点，使用 <code>std::is_base_of</code> 便可以处理派生类和基类之间比较的问题：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Person</span> &#123;</span><br><span class="line"><span class="keyword">public</span>: </span><br><span class="line">  <span class="keyword">template</span> &lt;<span class="keyword">typename</span> T, </span><br><span class="line">            <span class="keyword">typename</span> = <span class="keyword">typename</span> std::enable_if&lt; </span><br><span class="line">                         !std::is_base_of&lt;Person,</span><br><span class="line">                                          <span class="keyword">typename</span> std::decay&lt;T&gt;::type</span><br><span class="line">                                           &gt;::value</span><br><span class="line">                         &gt;::type</span><br><span class="line">             &gt;</span><br><span class="line">  <span class="built_in">Person</span>(T&amp;&amp; name); </span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>使用 <code>is_base_of</code> 取代 <code>is_same</code> 即可。当两个类型相同的类型使用 <code>is_base_of</code>，如 <code>std::is_base_of&lt;Person, Person&gt;</code>，结果依然是成立的，所以可以放心大胆地取代 <code>is_same</code>。</p><p>对了，以上代码使用 C++14 均可以更简洁一些，结果是一样的：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Person</span> &#123; </span><br><span class="line"><span class="keyword">public</span>: </span><br><span class="line">  <span class="keyword">template</span> &lt;<span class="keyword">typename</span> T,</span><br><span class="line">            <span class="keyword">typename</span> = std::<span class="type">enable_if_t</span>&lt; </span><br><span class="line">                         !std::is_base_of&lt;Person,</span><br><span class="line">                                          std::<span class="type">decay_t</span>&lt;T&gt; &gt;::value</span><br><span class="line">                         &gt;</span><br><span class="line">            &gt; </span><br><span class="line">  <span class="built_in">Person</span>(T&amp;&amp; name); </span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><h3 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h3><p>被绕了很久，已经逐渐忘记最初的目标是什么了，做一个简单的回顾。</p><p>在本章节开始时，我们介绍了万能引用的作用，而在 <strong>条款 26</strong> 中，却提出了当万能引用函数和重载、或者和构造函数一起出现时，会遇到的棘手问题。在本条款中，提出了几种简单的和复杂的方案，去解决这些问题。</p><p>最后的结论依然是，如果可以用前边几个简单的方案来解决这些问题，那么就去用便好了，在工程实践中，追求技术的专业和高效并不是最优的，代码的稳定性和可维护性才是更重要的事情。如果你是在编写库代码、做技术研究，或者单纯地就是想追寻极致的代码之美，试试模板元编程也并不为过。</p><p>但需要注意，C++ 一个极其让人诟病的地方就是它的报错很可能非常复杂，问题点夹杂在冗长的编译报错消息中，难以检查，即使你不去自己使用这些高深的技术，只要使用了标准库，也很可能会体会到，或已经深受其害。如果去使用这些技术，那么就要做好充分的心理准备，去面对恐怖的报错清单（C++20 的 Concept 一定程度上缓解了这里的烦恼）。</p><p>唯一值得安慰的是，当你掌握这些技术之后，你就已经超过了绝大多数 C++ 开发者，面对相同问题时，就更可能快速定位问题。</p><h2 id="条款-28：理解引用折叠"><a href="#条款-28：理解引用折叠" class="headerlink" title="条款 28：理解引用折叠"></a>条款 28：理解引用折叠</h2><p>到这里，我们终于可以展开说一下什么是万能引用。在这之前，需要先引入一个概念叫 <strong>引用折叠（reference collapse）</strong>。</p><p>C++ 中，不允许出现引用的引用，如果你这么写了，编译器会报错。但编译器自己却允许在内部推导时，出现引用的引用，它会将其合并起来，这就是引用折叠。引用折叠的规则是：</p><ol><li>如果其中存在左值引用，那么折叠后是左值引用。比如 <code>T&amp; &amp; =&gt; T&amp;</code>，<code>T&amp; &amp;&amp; =&gt; T&amp;</code> 以及 <code>T&amp;&amp; &amp; =&gt; T&amp;</code>；</li><li>如果都是右值引用，那么折叠后是右值引用。比如 <code>T&amp;&amp; &amp;&amp; =&gt; T&amp;&amp;</code>；</li></ol><p>注意，这些都是编译器内的行为，不能写出这种代码。</p><p>C++ 中使用引用折叠的场合有 4 个：</p><ol><li>模板实例化（万能引用就是其中的一种实践）；</li><li>auto 类型推导（本质上和模板实例化一样）；</li><li>typedef 类型定义；</li><li>decltype 表达式类型推导；</li></ol><p>接下来我们要使用引用折叠规则了。回到我们的万能引用中，对于一个典型示例：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">template</span> &lt;<span class="keyword">typename</span> T&gt; </span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">f</span><span class="params">(T&amp;&amp; arg)</span> </span>&#123; </span><br><span class="line">  <span class="built_in">g</span>(std::forward&lt;T&gt;(arg)); </span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>其中 <code>arg</code> 是一个万能引用，所以我们知道，传入 <code>f</code> 的实参是左值还是右值，会被编码到形参类型 <code>T</code> 中。<strong>万能引用能做到的是，当实参是一个左值时，<code>T</code></strong> <strong>的结果是一个左值引用，当实参是一个右值时，<code>T</code></strong> <strong>的结果是一个非引用类型</strong>（注意和引用折叠做区分，这里并不是一个右值引用，这是模板推导规则之一，可见<strong>条款 1</strong>）。</p><p>我们再将 <code>std::forward</code> 的定义写出来：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">template</span> &lt;<span class="keyword">typename</span> T&gt; </span><br><span class="line"><span class="function">T&amp;&amp; <span class="title">forward</span><span class="params">(<span class="keyword">typename</span> remove_reference&lt;T&gt;::type&amp; param)</span> </span>&#123; </span><br><span class="line">  <span class="keyword">return</span> <span class="built_in">static_cast</span>&lt;T&amp;&amp;&gt;(param); </span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>依次考虑一下。首先，当函数 <code>f</code> 的实参是一个左值时，由于万能引用的推导，<code>f</code> 的模板类型 <code>T</code> 是一个左值引用：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 推导后： </span></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">f</span><span class="params">(Widget&amp; &amp;&amp; arg)</span> </span>&#123; </span><br><span class="line">  <span class="built_in">g</span>(std::forward&lt;Widget&amp;&gt;(arg)) </span><br><span class="line">&#125; </span><br><span class="line"></span><br><span class="line"><span class="comment">// 引用折叠后： </span></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">f</span><span class="params">(Widget&amp; arg)</span> </span>&#123;   <span class="comment">// 折叠为左值引用 </span></span><br><span class="line">  <span class="built_in">g</span>(std::forward&lt;Widget&amp;&gt;(arg)) </span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>对 <code>forward</code> 的实例化结果为：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 推导后： </span></span><br><span class="line"><span class="function">Widget&amp; &amp;&amp; <span class="title">forward</span><span class="params">(<span class="keyword">typename</span> remove_reference&lt;Widget&amp;&gt;::type&amp; param)</span> </span>&#123; </span><br><span class="line">  <span class="keyword">return</span> <span class="built_in">static_cast</span>&lt;Widget&amp; &amp;&amp;&gt;(param); </span><br><span class="line">&#125; </span><br><span class="line"></span><br><span class="line"><span class="comment">// 引用折叠后： </span></span><br><span class="line"><span class="function">Widget&amp; <span class="title">forward</span><span class="params">(Widget&amp; param)</span> </span>&#123; </span><br><span class="line">  <span class="keyword">return</span> <span class="built_in">static_cast</span>&lt;Widget&amp;&gt;(param); </span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>forward</code> 函数实际上什么也没做，返回了一个左值引用。<br>整体上来看，当给万能引用模板函数 <code>f</code> 传入一个左值时，传递给函数 <code>g</code> 的实参也是一个左值引用，符合完美转发的设定。</p><p>考虑第二种情况，当函数 <code>f</code> 的实参是一个右值时，由于万能引用的推导，<code>f</code> 模板类型 <code>T</code> 是一个右值（非引用类型）：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 推导后： </span></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">f</span><span class="params">(Widget arg)</span> </span>&#123; </span><br><span class="line">  <span class="built_in">g</span>(std::forward&lt;Widget&gt;(arg)); </span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>对 <code>forward</code> 的实例化结果为：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="function">Widget&amp;&amp; <span class="title">forward</span><span class="params">(Widget&amp; param)</span> </span>&#123; </span><br><span class="line">  <span class="keyword">return</span> <span class="built_in">static</span>&lt;Widget&amp;&amp;&gt;(param); </span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>可以看到，<code>forward</code> 将一个左值引用作为输入，返回了一个类型转换后的右值引用。</p><p>整体上来看，当给万能引用模板函数 <code>f</code> 传入一个右值时，传递给函数 <code>g</code> 的实参，也是一个右值引用，同样符合完美转发的设定。</p><p>最后，对万能引用做一个定义。万能引用并非一种新的引用类型，它其实就是满足以下语境的右值引用：</p><ol><li>类型推导过程会区分左值和右值；</li><li>会发生引用折叠；</li></ol><h2 id="条款-29：假定移动操作不会发生或成本更高"><a href="#条款-29：假定移动操作不会发生或成本更高" class="headerlink" title="条款 29：假定移动操作不会发生或成本更高"></a>条款 29：假定移动操作不会发生或成本更高</h2><p>如果你已经很顺利地看完了前边的内容，那么这一条很容易理解。</p><p>C++ 的移动操作并不保证一定会发生移动操作，他仅仅只是把一个类型转换为右值，所以虽然用到了 <code>std::move</code>，移动也不一定总会发生；或者虽然没有显式使用移动语义，但编译器可能会调用移动构造和移动运算符的场景，移动也不总是会发生。</p><p>可能的一些原因有：</p><ol><li>对象没有提供移动操作，或者编译器无法自动为对象添加移动操作；</li><li>如果对象有移动操作，但移动操作可能并不会比复制操作更快；</li><li>如果移动操作本可以发生，但如果对象要求强异常安全保证，而移动没有添加 <code>noexcept</code>，那么移动操作也不会调用；</li></ol><p>对于其中的第 2 点，展开说一下。移动操作本质上没有什么神奇的，移动比复制快的原因，是因为移动是浅拷贝，也就是只复制了指向数据的指针，而没有复制数据本身。如果一些类型，它的数据本身就是其自身的一部分，比如 <code>std::array</code>，或者小数据量下的 <code>std::vector</code> 和 <code>std::string</code>，当对它们做移动操作时，和复制操作没有效率上的优势。</p><h2 id="条款-30：完美转发失败的情况"><a href="#条款-30：完美转发失败的情况" class="headerlink" title="条款 30：完美转发失败的情况"></a>条款 30：完美转发失败的情况</h2><p>最后，我们来看一下，完美转发不适用的场景。事实上，大多数使用完美转发的情形都是符合规范的，本条款中提到的一些情况，都是很罕见的用法。</p><p>首先来定义一个问题，我们要讨论的示例代码：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">template</span> &lt;<span class="keyword">typename</span> T&gt; </span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">f</span><span class="params">(T&amp;&amp; param)</span> </span>&#123; </span><br><span class="line">  <span class="built_in">g</span>(std::forward&lt;T&gt;(param)); </span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>如果以下两种用法，产生的效果是等价的，那么就可以认为完美转发成功了，否则，完美转发失败了：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">g</span>( expression ); <span class="comment">// 直接调用内部函数 </span></span><br><span class="line"><span class="built_in">f</span>( expression ); <span class="comment">// 调用外部函数，通过完美转发参数，间接执行内部函数</span></span><br></pre></td></tr></table></figure><p>我们来看一下哪些场景不适用完美转发。</p><h3 id="情况-1：大括号初始化"><a href="#情况-1：大括号初始化" class="headerlink" title="情况 1：大括号初始化"></a>情况 1：大括号初始化</h3><p>在第一章 <strong>条款 1</strong> 的讨论中，我们将大括号初始化来做模板类型推导时，就发现这种推导是无法执行的，所以，很显然，如果函数 <code>f</code> 传入的是一个大括号初始化，那么就不可能通过编译。<br>比如，函数 <code>g</code> 的定义为：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">void</span> <span class="title">g</span><span class="params">(<span class="type">const</span> std::vector&lt;<span class="type">int</span>&gt;&amp; v)</span></span>;</span><br></pre></td></tr></table></figure><p>以下代码：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">g</span>(&#123;<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>&#125;); <span class="comment">// 可以通过编译，编译器隐式转换大括号初始化为 std::vector </span></span><br><span class="line"><span class="built_in">f</span>(&#123;<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>&#125;); <span class="comment">// 编译失败</span></span><br></pre></td></tr></table></figure><p>有意思的是，在 <strong>条款 2</strong> 的讨论中，我们知道，<code>auto</code> 类型是可以接受大括号初始化类型推导的，所以一种变通的修改方案是：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">auto</span> temp = &#123;<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>&#125;; <span class="comment">// 先通过 auto 将大括号初始化转换为 std::initializer_list&lt;int&gt; </span></span><br><span class="line"><span class="built_in">f</span>(temp); <span class="comment">// 模板参数被推导为 std::initializer_list&lt;int&gt;</span></span><br></pre></td></tr></table></figure><h3 id="情况-2：0-和-NULL-用作空指针"><a href="#情况-2：0-和-NULL-用作空指针" class="headerlink" title="情况 2：0 和 NULL 用作空指针"></a>情况 2：0 和 NULL 用作空指针</h3><p>读到这里的读者，应该不再会愿意使用 0 和 <code>NULL</code> 来指定空指针了。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">f</span>(<span class="number">0</span>);       <span class="comment">// 预期想推导为 void*，结果推导为 int </span></span><br><span class="line"><span class="built_in">f</span>(<span class="literal">NULL</span>);    <span class="comment">// 同理 </span></span><br><span class="line"><span class="built_in">f</span>(<span class="literal">nullptr</span>); <span class="comment">// 被推导为 nullptr_t，指针类型</span></span><br></pre></td></tr></table></figure><p>由于函数 <code>f</code> 的形参需要一个万能引用，而常量因为没有地址，无法被引用，所以编译会失败。</p><h3 id="情况-3：声明为-static-const-的成员变量"><a href="#情况-3：声明为-static-const-的成员变量" class="headerlink" title="情况 3：声明为 static const 的成员变量"></a>情况 3：声明为 static const 的成员变量</h3><p>在 C++ 中有这样一个规定，如果类的成员函数中，声明了 <code>static const</code> 的成员，由于这样一个成员实际上可以被编译器当作常量来对待，所以编译器不会要求必须给这个成员做定义（只需要做声明）。</p><p>而如果这样一个成员变量，没有定义而只有声明，编译器编译不会报错，但对于将他作为 <code>f</code> 的参数绑定到万能引用类型形参，就同样遇到了无法被引用的错误。它会在链接时报错，找不到名称的定义。</p><p>修改的方案就是，为它提供定义。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Widget</span> &#123; </span><br><span class="line"><span class="keyword">public</span>: </span><br><span class="line">  <span class="type">static</span> <span class="type">const</span> std::<span class="type">size_t</span> Val = <span class="number">18</span>; <span class="comment">// 声明 </span></span><br><span class="line">&#125;; </span><br><span class="line"><span class="comment">// static const std::size_t Val;  // 定义 </span></span><br><span class="line"></span><br><span class="line"><span class="built_in">g</span>(Val); <span class="comment">// 正确，等价于 g(18); </span></span><br><span class="line"><span class="built_in">f</span>(Val); <span class="comment">// 若 Val 未定义，链接报错</span></span><br></pre></td></tr></table></figure><h3 id="情况-4：重载函数和模板函数"><a href="#情况-4：重载函数和模板函数" class="headerlink" title="情况 4：重载函数和模板函数"></a>情况 4：重载函数和模板函数</h3><p>对于重载函数和模板函数，都存在着没有地方指导万能引用确定引用哪个版本的重载函数或模板实例化的问题，从而导致编译失败。</p><p>看下重载函数的例子：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">int</span> <span class="title">processVal</span><span class="params">(<span class="type">int</span> val)</span></span>; </span><br><span class="line"><span class="function"><span class="type">int</span> <span class="title">processVal</span><span class="params">(<span class="type">int</span> val, <span class="type">int</span> priority)</span></span>; </span><br><span class="line"></span><br><span class="line"><span class="comment">// 若 g 的定义为： </span></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">g</span><span class="params">(<span class="type">int</span> (*pf)(<span class="type">int</span>))</span></span>; </span><br><span class="line"><span class="built_in">g</span>(processVal); <span class="comment">// 一切正常，会将第一个重载函数的地址传入函数 </span></span><br><span class="line"><span class="built_in">f</span>(processVal); <span class="comment">// 编译报错，万能引用并不知道要引用哪个重载函数</span></span><br></pre></td></tr></table></figure><p>然后再来看模板函数：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">template</span> &lt;<span class="keyword">typename</span> T&gt; </span><br><span class="line"><span class="function">T <span class="title">processValTemp</span><span class="params">(T param)</span> </span>&#123; ... &#125; </span><br><span class="line"></span><br><span class="line"><span class="comment">// 若 g 的定义为： </span></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">g</span><span class="params">(<span class="type">int</span> (*pf)(<span class="type">int</span>))</span></span>; </span><br><span class="line"><span class="built_in">g</span>(processValTemp); <span class="comment">// 一切正常，会被引用到模板参数为 int 类型的模板实例化函数 </span></span><br><span class="line"><span class="built_in">f</span>(processValTemp); <span class="comment">// 编译报错</span></span><br></pre></td></tr></table></figure><p>调整方案都是一样的，使用一个额外的对象，先为输入的实参做好类型签名，再传递给 <code>f</code>：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">int</span> (*)(<span class="type">int</span>) processValPtr = processVal; </span><br><span class="line"><span class="built_in">f</span>(processValPtr); <span class="comment">// 正确 </span></span><br><span class="line"><span class="built_in">f</span>(<span class="built_in">static_cast</span>&lt;<span class="built_in">int</span> (*)(<span class="type">int</span>)&gt;(processValTemp)); <span class="comment">// 正确</span></span><br></pre></td></tr></table></figure><h3 id="情况-5：位域"><a href="#情况-5：位域" class="headerlink" title="情况 5：位域"></a>情况 5：位域</h3><p>从前边几个情况，可以发现一个普适的规律，万能引用是一种引用，所以它需要绑定的对象，一定是要能找到地址的（可引用的）。位域是另一种无法被直接引用的类型。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">struct</span> <span class="title class_">IP</span> &#123;   </span><br><span class="line">  std::<span class="type">uint32_t</span> version:<span class="number">4</span>,</span><br><span class="line">                IHL:<span class="number">4</span>,</span><br><span class="line">                DSCP:<span class="number">6</span>,</span><br><span class="line">                ECN:<span class="number">2</span>,</span><br><span class="line">                totalLength:<span class="number">16</span>; </span><br><span class="line">&#125;; </span><br><span class="line"></span><br><span class="line"><span class="comment">// 如果 g 的定义是： </span></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">g</span><span class="params">(std::<span class="type">uint32_t</span> sz)</span></span>;  </span><br><span class="line">IP ip; </span><br><span class="line"><span class="built_in">g</span>(ip.totalLength); <span class="comment">// 正确 </span></span><br><span class="line"><span class="built_in">f</span>(ip.totalLength); <span class="comment">// 错误</span></span><br></pre></td></tr></table></figure><p>由于位域是由机器字上的若干任意部分组成的，所以无法为其取地址，从而就意味着无法引用。</p><hr><p>如果你看到这里，还比较自在，说明你对 C++ 的掌握能力，已经很不错了。有关于万能引用和模板实例化的高级技术，上文中都没有展开，但那些东西是通往高级 C++ 工程师的必经之路，谦虚地说，我还没有完全入门。</p><p>如果你到此依然充满热情，那就继续准备开下一章节吧，后半部书中，会讨论一些有不同风味的知识。</p><p>本系列的其他文章：</p><ol class="series-items"><li><a href="/posts/9bb75fe1.html" title="Effective Modern C++ 读书笔记：类型推导">Effective Modern C++ 读书笔记：类型推导</a></li><li><a href="/posts/f3206605.html" title="Effective Modern C++ 读书笔记：auto">Effective Modern C++ 读书笔记：auto</a></li><li><a href="/posts/57a898cb.html" title="Effective Modern C++ 读书笔记：转向现代C++">Effective Modern C++ 读书笔记：转向现代C++</a></li><li><a href="/posts/ce1aec80.html" title="Effective Modern C++ 读书笔记：智能指针">Effective Modern C++ 读书笔记：智能指针</a></li><li><a href="/posts/410ab8fb.html" title="Effective Modern C++：右值引用、移动语义和完美转发">Effective Modern C++：右值引用、移动语义和完美转发</a></li><li><a href="/posts/cd605d5c.html" title="Effective Modern C++：lambda 表达式">Effective Modern C++：lambda 表达式</a></li><li><a href="/posts/745d2232.html" title="Effective Modern C++：并发 API">Effective Modern C++：并发 API</a></li><li><a href="/posts/49749d08.html" title="Effective Modern C++：微调">Effective Modern C++：微调</a></li></ol><hr><div class="note info flat"><p>本文同步发布在知乎账号下：<a href="https://zhuanlan.zhihu.com/p/1949164417171306465">https://zhuanlan.zhihu.com/p/1949164417171306465</a></p></div>]]></content>
    
    
      
      
    <summary type="html">&lt;link rel=&quot;stylesheet&quot; class=&quot;aplayer-secondary-style-marker&quot; href=&quot;/assets/css/APlayer.min.css&quot;&gt;&lt;script src=&quot;/assets/js/APlayer.min.js&quot; cla</summary>
      
    
    
    
    <category term="软件开发" scheme="https://p2tree.top/categories/%E8%BD%AF%E4%BB%B6%E5%BC%80%E5%8F%91/"/>
    
    
    <category term="CPP" scheme="https://p2tree.top/tags/CPP/"/>
    
  </entry>
  
  <entry>
    <title>Effective Modern C++ 读书笔记：智能指针</title>
    <link href="https://p2tree.top/posts/ce1aec80.html"/>
    <id>https://p2tree.top/posts/ce1aec80.html</id>
    <published>2025-08-26T22:31:06.000Z</published>
    <updated>2025-08-26T14:44:07.000Z</updated>
    
    <content type="html"><![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/assets/css/APlayer.min.css"><script src="/assets/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><p>智能指针是现代 C++ 中的一个重要内容，以至于当使用现代 C++ 编程时，智能指针可以完全取代裸指针。</p><p>使用 C 风格裸指针，存在很多已经被人诟病已久的问题，比如：</p><ol><li>裸指针从声明中，无法看出它指向的是对象还是数组；</li><li>无法通过裸指针本身来判断，是否需要析构它的资源；</li><li>就算知道需要析构资源，也不知道应该怎么析构，是直接调用 <code>delete</code> 还是调用某个用于释放资源的函数；</li><li>另外，同第1条，也不可能知道应该调用 <code>delete</code> 还是 <code>delete[]</code>，这都带来了很多风险；</li><li>在使用裸指针的工程中，时刻都需要考虑这些资源在不同路径下的状态，除了常规代码逻辑外，还需要考虑发生异常时的路径。一旦有所疏忽，便会出现资源泄漏或未定义行为；</li><li>如果资源释放时，没有将裸指针置为 0，那么将来再次意外解引用时，就会遇到悬挂指针问题；</li></ol><p>为了解决这些问题，现代 C++ 提出了几种智能指针，它的本质是依靠 C++ 的 RAII 设计理念，将资源管理和对象生命周期绑定在一起，从而避免让程序员主动去操作资源释放的行为。<code>unique_ptr</code> 和 <code>shared_ptr</code> 是两种最常见的智能指针，另外还有 <code>weak_ptr</code> 用于解决 <code>shared_ptr</code> 的循环引用问题，在后续内容中会逐个展开讨论。<code>auto_ptr</code> 在 C++11 中被 <code>unique_ptr</code> 取代，所以不要再使用。</p><h2 id="条款-18：优先使用-unique-ptr"><a href="#条款-18：优先使用-unique-ptr" class="headerlink" title="条款 18：优先使用 unique_ptr"></a>条款 18：优先使用 unique_ptr</h2><p>当需要使用指针来引用一块资源时，考虑使用智能指针；当需要使用智能指针时，优先考虑 <code>unique_ptr</code>。这里的原则是，大多数情况下，我们分配的资源，是交给“一个”目标使用的，所以专属所有权通常就足够了。</p><p><code>unique_ptr</code> 使用移动语义来实现指针的转移，它没有复制操作，所以才能实现“唯一性”。</p><p>它最常见的用处是作为工厂函数的返回值。一个工厂函数，通常是在函数内部创建一个位于堆上的对象，返回之后，需要交接资源的所有权，这就是 <code>unique_ptr</code> 的用武之地。交接之后，资源的责任人变成了调用工厂函数的一方。</p><h3 id="自定义析构器"><a href="#自定义析构器" class="headerlink" title="自定义析构器"></a>自定义析构器</h3><p><code>unique_ptr</code> 具有自定义析构函数。一个自定义析构函数，是指当智能指针被析构时，选择调用的函数。比如，我们希望在析构智能指针时，打印一些日志，那么就可以使用自定义析构函数。</p><p>自定义析构函数可以通过智能指针的第二个模板参数来指定，它可以是函数指针，函数对象或 lambda 表达式等可调用类型，它接受一个参数，类型为原始资源的裸指针。比如：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">auto</span> delFunc = [](Object* obj) &#123; </span><br><span class="line">  <span class="comment">// do some other thing  </span></span><br><span class="line">  <span class="keyword">delete</span> obj; </span><br><span class="line">&#125; </span><br><span class="line"></span><br><span class="line"><span class="comment">// 下边是一个工厂函数，返回 Object 对象的 unique_ptr，并包含自定义析构函数 delFunc </span></span><br><span class="line"><span class="function"><span class="keyword">template</span>&lt;<span class="keyword">typename</span>... Ts&gt; </span></span><br><span class="line"><span class="function">std::unique_ptr&lt;Object*, <span class="title">decltype</span><span class="params">(delFunc)</span>&gt; <span class="title">makeObject</span><span class="params">(Ts&amp;&amp;... args)</span></span>;</span><br></pre></td></tr></table></figure><p>示例中，使用 lambda 表达式作为智能指针的自定义析构函数。</p><p>通常，我们知道，<code>unique_ptr</code> 对象的大小，和裸指针的大小是一样的。不过，当引入智能指针时，问题需要进一步讨论。</p><p>当析构器是函数指针时，这个函数指针需要保存在 <code>unique_ptr</code> 的对象内，所以通常 <code>unique_ptr</code> 的大小会增加几个字节（指针大小）；当析构器是函数对象时，析构器对 <code>unique_ptr</code> 空间的影响，则取决于函数对象本身占用的存储空间；当析构器使用 lambda 表达式指定时，因为我们知道，lambda 表达式的本质也是函数对象，对于空捕获列表的 lambda 表达式，不会对 <code>unique_ptr</code> 的空间产生额外需求，但当存在捕获列表时，占用空间则与捕获列表中对象的空间占用相同。</p><p>最后，虽然 <code>unique_ptr</code> 可以用来保存一个数组资源，也就是 <code>std::unique_ptr&lt;T[]&gt;</code>，但绝大多数场景下没有这个必要，请使用 <code>vector</code>，<code>array</code> 等线性容器来替代它。</p><p>同时，当需要将 <code>unique_ptr</code> 转换为 <code>shared_ptr</code> 时，不要尝试获取它的裸指针后，初始化新的 <code>shared_ptr</code>，这样将很可能带来资源被重复释放的问题。C++ 提供了从 <code>unique_ptr</code> 向 <code>shared_ptr</code> 的类型转换：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 将工厂函数返回的 unique_ptr 直接转换为 shared_ptr，也是允许的 </span></span><br><span class="line">std::shared_ptr&lt;Object&gt; sp = <span class="built_in">makeObject</span>(arguments);</span><br></pre></td></tr></table></figure><h2 id="条款-19：谨慎使用-shared-ptr"><a href="#条款-19：谨慎使用-shared-ptr" class="headerlink" title="条款 19：谨慎使用 shared_ptr"></a>条款 19：谨慎使用 shared_ptr</h2><p>和 <code>unique_ptr</code> 不同，<code>shared_ptr</code> 中需要注意的使用问题更多一些，我们按书中的思路依次展开。</p><h3 id="组成结构"><a href="#组成结构" class="headerlink" title="组成结构"></a>组成结构</h3><p><code>shared_ptr</code> 自身也是指针，但它是共享所有权的智能指针，这意味着多个<code>shared_ptr</code> 可以指向同一块资源，而只有当资源被一个 <code>shared_ptr</code> 指向时，这个 <code>shared_ptr</code> 析构时才负责释放资源。所以，<code>shared_ptr</code> 需要一个计数器，或者严谨点来说，是资源需要一个共享智能指针的计数器。</p><p>每个 <code>shared_ptr</code> 的实例由两个指针组成，第一个指针指向负责管理的资源位置，第二个指针指向一个<strong>资源控制块</strong>。如果有新的 <code>shared_ptr</code> 也指向这个资源，那么它的控制块指针也指向相同的资源控制块。如上提到的计数器就保存在控制块中，另外，控制块中也包括了如自定义的析构器、资源分配器等内容。</p><p>虽然 <code>shared_ptr</code> 占用了两个指针的大小，然而它的操作带来的性能开销却远大于 <code>unique_ptr</code>（<code>unique_ptr</code> 和裸指针基本一致）。这由于以下几个原因：</p><ul><li>第一个创建一块资源的 <code>shared_ptr</code> 对象，需要负责分配控制块内存；</li><li>增加或减少对资源的共享引用时，需要增减计数器，而为了避免并发问题，增减操作必须是原子操作，这就带来了额外的开销。考虑缓存和机器行为，原子操作可能带来性能的不稳定性；</li></ul><p>正因为原子操作计数器的原因，移动构造一个 <code>shared_ptr</code>（计数器不增加），要比复制构造一个 <code>shared_ptr</code>（计数器+1）要更快。</p><p>插一句，Rust 中的引用计数，直接提供了两个实现，<code>Rc&lt;T&gt;</code> 和 <code>Arc&lt;T&gt;</code>，分别用于非并发程序下的共享所有权和并发程序下的共享所有权，前者没有使用原子操作实现引用计数，这样就避免了非并发程序时引用计数的性能开销。<strong>并发导致的性能损失，应该只有在真正需要时才需要承担。</strong></p><p>除此之外，我们还需要注意在使用 <code>shared_ptr</code> 的几个问题。</p><h3 id="自定义析构器-1"><a href="#自定义析构器-1" class="headerlink" title="自定义析构器"></a>自定义析构器</h3><p>和 <code>unique_ptr</code> 不同，<code>shared_ptr</code> 的析构器并不是智能指针对象的一部分，因为它保存在控制块中，所以，不会涉及到上一节，不同析构器占用空间的讨论。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">auto</span> loggingDel = [](Widget *pw) &#123;  </span><br><span class="line">  <span class="built_in">makeLogEntry</span>(pw);  </span><br><span class="line">  <span class="keyword">delete</span> pw; </span><br><span class="line">&#125;;  </span><br><span class="line"><span class="function">std::unique_ptr&lt;Widget*, <span class="title">decltype</span><span class="params">(loggingDel)</span>&gt; <span class="title">unique_pointer</span><span class="params">( </span></span></span><br><span class="line"><span class="params"><span class="function">  <span class="keyword">new</span> Widget, </span></span></span><br><span class="line"><span class="params"><span class="function">  loggingDel )</span></span>; </span><br><span class="line"><span class="function">std::shared_ptr&lt;Widget*&gt; <span class="title">shared_pointer</span><span class="params">(  <span class="comment">// 类型中模板参数没有析构器参数</span></span></span></span><br><span class="line"><span class="params"><span class="function">  <span class="keyword">new</span> Widget, </span></span></span><br><span class="line"><span class="params"><span class="function">  loggingDel )</span></span>;</span><br></pre></td></tr></table></figure><p>正因为如此，指向相同资源的不同 <code>shared_ptr</code> 可以在初始化时，接受不同的析构器，然后，只有最后一个 shared_ptr 离开作用域时，会调用当前的析构器。注意，同一个资源，只对应一个析构器，看代码：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">auto</span> deleter1 = [](<span class="type">int</span> *p) &#123; <span class="keyword">delete</span> p; std::cout &lt;&lt; <span class="string">&quot;Deleter1\n&quot;</span>; &#125;;</span><br><span class="line"><span class="keyword">auto</span> deleter2 = [](<span class="type">int</span> *p) &#123; <span class="keyword">delete</span> p; std::cout &lt;&lt; <span class="string">&quot;Deleter2\n&quot;</span>; &#125;;  </span><br><span class="line"><span class="function">std::shared_ptr&lt;<span class="type">int</span>&gt; <span class="title">ptr1</span><span class="params">(<span class="keyword">new</span> <span class="type">int</span>(<span class="number">10</span>), deleter1)</span></span>; </span><br><span class="line"><span class="function">std::shared_ptr&lt;<span class="type">int</span>&gt; <span class="title">ptr2</span><span class="params">(<span class="keyword">new</span> <span class="type">int</span>(<span class="number">20</span>), deleter2)</span></span>;  </span><br><span class="line">ptr1 = ptr2;  <span class="comment">// a. 这里打印出 Deleter1 </span></span><br><span class="line">ptr<span class="number">1.</span><span class="built_in">reset</span>(); <span class="comment">// b. 这里什么也不打印 </span></span><br><span class="line">ptr<span class="number">2.</span><span class="built_in">reset</span>(); <span class="comment">// c. 这里打印出 Deleter2</span></span><br></pre></td></tr></table></figure><p>代码中有两个资源，10 和 20，他们的控制块中，自定义析构器分别指向了 <code>deleter1</code> 和 <code>deleter2</code>，初始化时，计数器的值都为 1。在 a 处，<code>ptr1</code> 被 <code>ptr2</code> 赋值为指向资源 20，那么资源 10 的计数器值为 0，触发资源 10 的析构器 <code>deleter1</code>，而此时资源 20 的计数器值为 2；在 b 处，<code>ptr1</code> 释放，但资源 20 的计数器值变为 1，不用调用资源析构；在 c 处，<code>ptr2</code> 释放，资源 20 的计数器值变为 0，调用资源 20 的析构器 <code>deleter2</code>。</p><p>因为 <code>shared_ptr</code> 本身不带有析构器类型，所以即使拥有不同的析构器，它们也可以放在同一个容器中：<code>std::vector&lt;std::shared_ptr&lt;int&gt;&gt; vp &#123;ptr1, ptr2&#125;;</code>。</p><h3 id="创建智能指针"><a href="#创建智能指针" class="headerlink" title="创建智能指针"></a>创建智能指针</h3><p>有几种方法可以创建 <code>shared_ptr</code>：</p><ul><li>使用 <code>std::make_shared</code> 接口函数。它总会分配一个新的对象，同时创建一个新的控制块，并返回一个新的 <code>shared_ptr</code>，但它不能指定自定义析构器。</li><li>从 <code>unique_ptr</code> 出发构造。由于 <code>unique_ptr</code> 并没有控制块，所以使用它创建 <code>shared_ptr</code> 时，编译器会创建一个控制块出来。这个过程，会让 <code>unique_ptr</code> 失去对资源的所有权。</li><li>从一个裸指针来构造。这是最灵活也是最危险的一种方式，使用这种方法会创建一个控制块，如上边已经用到的代码。<code>shared_ptr</code> 构造函数可以接受一个可调用对象作为自定义析构器，这也是唯一一种可以自定义析构器的方式。</li></ul><p>对第三种方式展开讨论，它存在一个很容易犯的使用错误，编译器无法报告错误。看以下代码：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">auto</span> pw = <span class="keyword">new</span> Widget; </span><br><span class="line"><span class="function">std::shared_ptr&lt;Widget&gt; <span class="title">spw1</span><span class="params">(pw, deleter1)</span></span>; </span><br><span class="line"><span class="function">std::shared_ptr&lt;Widget&gt; <span class="title">spw2</span><span class="params">(pw, deleter2)</span></span>;  </span><br><span class="line">spw<span class="number">1.</span><span class="built_in">reset</span>(); <span class="comment">// a. 打印 Deleter1 </span></span><br><span class="line">spw<span class="number">2.</span><span class="built_in">reset</span>(); <span class="comment">// b. 报错：资源被二次析构</span></span><br></pre></td></tr></table></figure><p>我们已经知道，这种创建智能指针的方式，会创建一个控制块，那么，构造 <code>spw1</code> 和 <code>spw2</code> 时，就会分别创建一个控制块，其内部计数器的值均为 1，那么，当 a 处 <code>spw1</code> 释放时，资源 <code>pw</code> 就会被析构掉，而在 b 处，<code>spw2</code> 释放时，就会重复释放资源 <code>pw</code>。</p><p>我们应该尽量避免使用这种方式来构造智能指针，除非你真的想利用这里边的灵活性。如果一定要做，那么确保多个析构器中，只有一个析构器会去析构资源，而它作为主析构器，必须在最后被初始化，比如上例中，<code>deleter2</code> 作为主析构器，去析构资源。更合理的建议是，不要用一个裸指针对象去初始化智能指针，而是在初始化智能指针时，同时分配资源：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="function">std::shared_ptr&lt;Widget&gt; <span class="title">spw1</span><span class="params">(<span class="keyword">new</span> Widget, deleter2)</span></span>; </span><br><span class="line">std::shared_ptr&lt;Widget&gt; spw2 = spw1; <span class="comment">// spw2 和 spw1 指向同一个控制块，计数器为 2</span></span><br></pre></td></tr></table></figure><p><em>陷阱：如无必要，不要使用裸指针对象来初始化带有自定义析构器的</em> <em><code>shared_ptr</code>。</em></p><h3 id="衍生话题"><a href="#衍生话题" class="headerlink" title="衍生话题"></a>衍生话题</h3><p>看书中给出的这个示例代码：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">std::vector&lt;std::shared_ptr&lt;Widget&gt;&gt; processedWidgets; </span><br><span class="line"><span class="keyword">class</span> <span class="title class_">Widget</span> &#123; <span class="keyword">public</span>: <span class="function"><span class="type">void</span> <span class="title">process</span><span class="params">()</span></span>; &#125;</span><br></pre></td></tr></table></figure><p><code>process()</code> 成员函数需要做一个事情，把当前对象以 <code>shared_ptr</code> 方式保存到 <code>processedWidgets</code> 中，最容易想到的实现是：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">void</span> <span class="title">Widget::process</span><span class="params">()</span> </span>&#123; </span><br><span class="line">  processedWidgets.<span class="built_in">emplace_back</span>(<span class="keyword">this</span>); <span class="comment">// 使用 this 裸指针来初始化 shared_ptr 并存入容器 </span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>结合上一小节的知识，就会意识到这里存在问题，每一个 Widget 实例对象，在 <code>process</code> 之后，都会生成自己的智能指针，并拥有自己的控制块，所以智能指针析构时，这里就会发生重复释放资源的问题！（对象自己的析构函数和智能指针管理资源的析构器都会去析构同一个资源）</p><p>C++ 委员会已经意识到这个问题，也为我们提供了解决方案，也就是使用 <code>enable_shared_from_this</code>：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Widget</span>: <span class="keyword">public</span> std::enable_shared_from_this&lt;Widget&gt; &#123;</span><br><span class="line"><span class="keyword">public</span>: </span><br><span class="line">  <span class="function"><span class="type">void</span> <span class="title">process</span><span class="params">()</span></span>; </span><br><span class="line">&#125; </span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">Widget::process</span><span class="params">()</span> </span>&#123; </span><br><span class="line">  processedWidget.<span class="built_in">emplace_back</span>(<span class="built_in">shared_from_this</span>());</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>将 <code>Widget</code> 对象继承自 <code>enable_shared_from_this&lt;Widget&gt;</code> 类型，它会在内部维护一个 <code>weak_ptr</code> 对象（拥有共享所有权但不会重复创建控制块），通过一个成员函数 <code>shared_from_this</code>来使用 <code>weak_ptr</code> 获取 <code>shared_ptr</code>，从而帮我们解决这个问题。</p><p>这里还有一个最佳实践，因为 <code>enable_shared_from_this</code> 使用的一个前提是，<code>Widget</code> 需要有一个控制块，也就意味着它需要有一个 <code>shared_ptr</code> 已经指向它（控制块存在），才可以正常调用 <code>shared_from_this</code>，所以，需要避免对象被多种方式管理，比如还在栈上或裸指针指向。</p><p>这个最佳实践是实现一个工厂函数，返回 <code>shared_ptr</code> 来管理对象，并禁止调用构造函数：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Widget</span>: <span class="keyword">public</span> std::enable_shared_from_this&lt;Widget&gt; &#123; </span><br><span class="line"><span class="keyword">public</span>: </span><br><span class="line">  <span class="function"><span class="type">void</span> <span class="title">process</span><span class="params">()</span></span>; </span><br><span class="line">  <span class="function"><span class="type">static</span> std::shared_ptr&lt;Widget&gt; <span class="title">create</span><span class="params">()</span></span>; </span><br><span class="line"></span><br><span class="line"><span class="keyword">private</span>: </span><br><span class="line">  <span class="built_in">Widget</span>() = <span class="keyword">default</span>; </span><br><span class="line">&#125;; </span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">static</span> std::shared_ptr&lt;Widget&gt; <span class="title">Widget::create</span><span class="params">()</span> </span>&#123; </span><br><span class="line">  <span class="keyword">return</span> std::<span class="built_in">shared_ptr</span>&lt;Widget&gt;(<span class="keyword">new</span> Widget); <span class="comment">// 或直接 make_shared </span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>工厂函数可以确保对象只能被 <code>shared_ptr</code> 管理，且在调用 <code>process</code> 之前（或者说调用 <code>shared_from_this</code> 之前），一定已经存在一个 <code>shared_ptr</code> 指向它。</p><p><em>技巧：使用</em> <em><code>enable_shared_from_this</code></em> <em>和对外部隐藏构造函数的方式，来安全地实现在类的成员函数内，获取指向该对象的</em> <em><code>shared_ptr</code>。</em></p><h3 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h3><p>如果你担心这些 <code>shared_ptr</code> 带来的性能问题和潜在陷阱，那么就应该考虑下代码中是否真的需要共享所有权。如果 <code>unique_ptr</code> 可以完成任务，还是要优先使用 <code>unique_ptr</code>。</p><p>从 <code>unique_ptr</code> 创建一个 <code>shared_ptr</code> 很容易，只需要分配一个控制块就行，C++ 已经提供了这种能力。但反过来则不行，我们永远无法将一个 <code>shared_ptr</code> 转变为一个 <code>unique_ptr</code>，即使它的计数器为 1。这也就意味着，如果不有所克制，你的工程中，一定会面临 <code>shared_ptr</code> 泛滥的问题，这意味着代码已经开始变味。</p><h2 id="条款-20：使用-weak-ptr-作为-shared-ptr-的补充"><a href="#条款-20：使用-weak-ptr-作为-shared-ptr-的补充" class="headerlink" title="条款 20：使用 weak_ptr 作为 shared_ptr 的补充"></a>条款 20：使用 weak_ptr 作为 shared_ptr 的补充</h2><p><code>weak_ptr</code> 不是一种新的智能指针，它是一种特殊的 <code>shared_ptr</code>。它使用和 <code>shared_ptr</code> 相同的控制块，所以也拥有两个指针。<code>weak_ptr</code> 使用 <code>shared_ptr</code> 创建：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">auto</span> shared_pointer = std::<span class="built_in">make_shared</span>&lt;<span class="type">int</span>&gt;(); </span><br><span class="line"><span class="function">std::weak_ptr&lt;<span class="type">int</span>&gt; <span class="title">weak_pointer</span><span class="params">(shared_pointer)</span></span>;  </span><br><span class="line">shared_pointer.<span class="built_in">reset</span>(); <span class="comment">// 之后，weak_pointer 变成悬挂智能指针</span></span><br></pre></td></tr></table></figure><p><code>weak_ptr</code> 不同于 <code>shared_ptr</code> 的地方在于，它不会影响 <code>shared_ptr</code> 的引用计数。但因为它依然拥有控制块，所以可以利用里边的信息，实现检查智能指针是否悬挂，这是和裸指针所不一样的地方。</p><p>因为检查是否悬挂之后，我们通常希望在没有悬挂时操作智能指针，那么“检查”和“操作”分开执行时，在并发程序中就会遇到竞争的问题，所以，它也提供了一种原子性的操作方式：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 若 weak_pointer 悬挂，shared_pointer == nullptr </span></span><br><span class="line">std::shared_ptr&lt;<span class="type">int</span>&gt; shared_pointer = weak_pointer.<span class="built_in">lock</span>(); </span><br><span class="line"></span><br><span class="line"><span class="comment">// 若 weak_pointer 悬挂，抛出异常 </span></span><br><span class="line"><span class="function">std::shared_ptr&lt;<span class="type">int</span>&gt; <span class="title">shared_pointer2</span><span class="params">(weak_pointer)</span></span>;</span><br></pre></td></tr></table></figure><p><code>weak_ptr</code> 最常用的地方是解决 <code>shared_ptr</code> 的环形引用导致资源泄漏的问题，想必是学习智能指针一定会接触的问题。书中还提到了另一种 <code>weak_ptr</code> 的用途，即用它来实现检查资源是否失效的需求。</p><p>如果我们想设计一个缓存机制，使用容器保存一些智能指针，它们指向一些已分配的资源。如果容器中存储的是 <code>shared_ptr</code>，那么当所有权被转移到外部时，资源就会被析构，如果想要缓存资源而不是直接析构，就用 <code>weak_ptr</code> 所取代，并在必要时，比如重新加载资源到缓存时，通过 <code>weak_ptr</code> 的悬挂检测来决定。</p><p>我个人认为不是很常用的做法，也比较取巧，就不列出代码了。</p><p>最后提一下，<code>weak_ptr</code> 只是不修改 <code>shared_ptr</code> 的引用计数，但不代表着它没有性能开销，控制块中还有关于 <code>weak_ptr</code> 的计数器，它依然会原子地修改这个计数器。</p><p><em>谬误：<code>weak_ptr</code></em> <em>并没有比</em> <em><code>shared_ptr</code></em> <em>操作效率高。</em></p><h2 id="条款-21：除非有充分的理由，否则应该使用-make-函数创建智能指针对象"><a href="#条款-21：除非有充分的理由，否则应该使用-make-函数创建智能指针对象" class="headerlink" title="条款 21：除非有充分的理由，否则应该使用 make 函数创建智能指针对象"></a>条款 21：除非有充分的理由，否则应该使用 make 函数创建智能指针对象</h2><p>C++ 有三个 make 函数，它们分别是 <code>std::make_unique</code>，<code>std::make_shared</code> 和 <code>std::allocate_shared</code>，其中，<code>std::make_unique</code> 是在 C++14 中引入的，另外两个是在 C++11 中引入的。</p><p>从前边的讨论中我们知道，可以使用 <code>new</code> 来初始化一个智能指针，而不需要考虑 <code>delete</code> 的事情，不过，还是优先推荐使用 make 函数来初始化智能指针。</p><h3 id="使用-make-函数的优点"><a href="#使用-make-函数的优点" class="headerlink" title="使用 make 函数的优点"></a>使用 make 函数的优点</h3><p>C++ 中有一种很常见的异常问题，被程序员们广泛讨论，见代码：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 有一个函数，接受两个参数，一个智能指针，一个值 </span></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">func</span><span class="params">(std::shared_ptr&lt;Widget&gt;, <span class="type">int</span>)</span></span>; </span><br></pre></td></tr></table></figure><p>如果，我们通过 new 来初始化智能指针，再调用一个函数来求第二个值，并把这些放在一条语句中，C++ 是允许的：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">func</span>(std::<span class="built_in">shared_ptr</span>&lt;Widget&gt;(<span class="keyword">new</span> Widget), <span class="built_in">compute</span>());</span><br></pre></td></tr></table></figure><p>但这种写法会带来一个异常安全问题。</p><p>因为 C++ 并不规定同一个函数参数的参数列表中，各表达式的求值顺序，所以，这个参数列表中，三个操作：<code>new Widget</code>，<code>compute()</code> 和 <code>shared_ptr&lt;Widget&gt;()</code> 将会以随机的方式执行（严谨一些说，<code>new Widget</code> 会在智能指针构造前完成，但 <code>compute()</code> 会在任意位置完成）。如果，<code>compute()</code> 刚好在 <code>new Widget</code> 和智能指针构造之间执行，而它发生了异常，那么，<code>new Widget</code> 的资源便会发生资源泄漏，永远无法被释放。<br>有几种不同的方案可以避免这个问题：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 使用 make 函数 </span></span><br><span class="line"><span class="built_in">func</span>(std::<span class="built_in">make_shared</span>&lt;Widget&gt;(), <span class="built_in">compute</span>()); </span><br><span class="line"></span><br><span class="line"><span class="comment">// 将 compute 计算放在之前完成 </span></span><br><span class="line"><span class="type">int</span> val = <span class="built_in">compute</span>(); </span><br><span class="line"><span class="built_in">func</span>(std::<span class="built_in">make_shared</span>&lt;Widget&gt;(), val); </span><br><span class="line"></span><br><span class="line"><span class="comment">// 或先创建智能指针</span></span><br><span class="line"><span class="keyword">auto</span> ptr = std::<span class="built_in">make_shared</span>&lt;Widget&gt;(); </span><br><span class="line"><span class="built_in">func</span>(ptr, <span class="built_in">compute</span>());</span><br></pre></td></tr></table></figure><p>对于第三种做法，存在一个小问题。因为 <code>ptr</code> 是左值，所以默认按复制传入形参，而复制操作，会让 <code>shared_ptr</code> 的引用计数增加，带来了额外的性能开销。第二种写法，因为第一个实参是右值，按移动传入，不会带来性能开销。第三种方案可以适当改进一下：<code>func(std::move(ptr), compute());</code>。</p><p><em>陷阱：参数列表中多个表达式有存在异常时，将导致手动分配的内存出现内存泄漏，这是 C++ 很常见的一个陷阱。</em></p><p>另一个使用 make 的优点是，它的性能更好。对于 <code>shared_ptr</code> 来说，因为它除了被指向的资源外，还有一块控制块，也位于堆内存中，如果调用 <code>make_shared</code> 操作，那么只需要进行一次内存分配，同时用于资源数据使用和控制块使用。</p><p>而如果使用 <code>new</code> 的方式，将先后进行两次内存分配。向操作系统的一次内存申请，其性能将优于两次申请。这同样适用于 <code>std::allocate_shared</code> 操作。</p><h3 id="使用-make-函数的不足"><a href="#使用-make-函数的不足" class="headerlink" title="使用 make 函数的不足"></a>使用 make 函数的不足</h3><p>和之前的一些条款一样，使用 make 函数并不是一个完美替代 new 操作来创建智能指针的方案，它也存在一些无法被使用的场景。</p><p>第一个场景是自定义析构器。从前边条款中得知，make 函数不能指定自定义析构器，而只能通过 <code>new</code> 关键字来实现：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="function">std::unique_ptr&lt;Widget, <span class="title">decltype</span><span class="params">(Deleter)</span>&gt; <span class="title">upw</span><span class="params">(<span class="keyword">new</span> Widget, Deleter)</span></span>; </span><br><span class="line"><span class="function">std::shared_ptr&lt;Widget&gt; <span class="title">spw</span><span class="params">(<span class="keyword">new</span> Widget, Deleter)</span></span>;</span><br></pre></td></tr></table></figure><p>第二个场景是，当希望通过初始化列表作为参数，来调用被指向对象的特殊构造函数时，make 函数会将其按小括号初始化来对待，这在我们之前讨论初始化列表时，也提到过。这里再重复啰嗦一次，见下边代码：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">auto</span> ptr = std::make_unique&lt;std::vector&lt;<span class="type">int</span>&gt;&gt;(<span class="number">10</span>, <span class="number">20</span>);</span><br></pre></td></tr></table></figure><p>这样一条语句，它生成的是一个指向 10 个值为 20 的 <code>vector</code> 的智能指针，还是指向包含 2 个元素，值分别为 10 和 20 的 <code>vector</code> 的智能指针？想必你也比较熟悉，答案是前者。</p><p>make 函数做不到通过这一条语句实现后者，这是因为 make 函数内部的实现，在处理对形参的完美转发时，使用的是圆括号，而不是大括号。</p><p>如果一定要使用初始化列表来初始化这个资源，可以分开来写：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">auto</span> args = &#123;<span class="number">10</span>, <span class="number">20</span>&#125;; </span><br><span class="line"><span class="keyword">auto</span> ptr = std::make_unique&lt;std::vector&lt;<span class="type">int</span>&gt;&gt;(args); <span class="comment">// 生成指向 [10, 20] 的智能指针</span></span><br></pre></td></tr></table></figure><p>第三个场景，前边提到，使用 make 函数可以让对资源内存的分配和对控制块内存的分配，合并在一起申请，从而提高性能，然而，当讨论 <code>weak_ptr</code> 时，这个问题变的复杂。</p><p>在控制块中，除了保存引用计数外，还保存着其他信息，比如弱引用计数（注意，<code>weak_ptr</code> 用引用计数来判断自己是否失效，而它构造和析构改变的是弱引用计数）。当引用计数为 0 但弱引用计数不为 0 时（<code>shared_ptr</code> 没有了，但 <code>weak_ptr</code> 还存在一些），我们认为资源应该被释放，这没问题，这里的问题是，如果使用 make 函数统一将资源和控制块分配在一块内存块上，因为弱引用计数还存在，所以控制块不能析构，导致位于同一块内存块的资源内存，也不能被尽早析构（虽然它已经没有用了）。只有当控制块的弱引用计数也为 0 后，整个控制块应该被析构时，操作系统才开始考虑对整个内存块（包括资源内存和控制块内存）进行析构操作。</p><p>如果我们的资源内存占用比较大，而 <code>weak_ptr</code> 长时间没有释放，那么这块内存就带来了长期驻留内存的资源占用问题。</p><p>使用 <code>new</code> 操作来初始化智能指针，反而可以避免这个问题。虽然这不是一个很急迫的问题，但讨论一下，也是蛮有意思的。技术权衡的正反面，只有了解细节才能在选型时找到最优解。</p><h2 id="条款-22：使用-Pimpl-用法时要注意的地方"><a href="#条款-22：使用-Pimpl-用法时要注意的地方" class="headerlink" title="条款 22：使用 Pimpl 用法时要注意的地方"></a>条款 22：使用 Pimpl 用法时要注意的地方</h2><p>Pimpl 是 C++ 编程中的一种习惯用法，它全称是 Pointer to implementation。具体来说，比如我们实现一个自定义类型：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Widget.h </span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&quot;Object.h&quot;</span> </span></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">Widget</span> &#123; </span><br><span class="line"><span class="keyword">public</span>: </span><br><span class="line">  <span class="built_in">Widget</span>(); </span><br><span class="line"></span><br><span class="line"><span class="keyword">private</span>:   </span><br><span class="line">  Object obj; <span class="comment">// Object 是另一个自定义类型，位于其他文件中 </span></span><br><span class="line">&#125;; </span><br><span class="line"></span><br><span class="line"><span class="comment">// Widget.cpp </span></span><br><span class="line">Widget::<span class="built_in">Widget</span>() &#123;&#125;;  <span class="comment">// 构造函数定义</span></span><br></pre></td></tr></table></figure><p>通常会这么写代码。但这里有一个编译性能的问题。如果我们的 <code>Widget.h</code> 文件没有变，但 <code>Object.h</code> 文件发生了变化，那么对于引用 <code>Widget.h</code> 的用户代码来说，编译时，<code>Widget.h</code> 文件也会被重新编译一遍。当工程代码中间的此类依赖很多很复杂时，编译时间会受到很大的影响。</p><p>Pimpl 就可以解决这个问题。它的重构做法是：</p><ul><li>在 <code>Widget.h</code> 头文件中，创建一个类内定义的对象，假设名为 <code>Impl</code>，它的实现放在 <code>Widget.cpp</code> 中。</li><li>再将和 <code>Object</code> 相关的数据都放到 <code>Impl</code> 类型的实现中，而在 <code>Widget.h</code> 的类声明中，只放置一个指向 <code>Impl</code> 对象的指针。</li></ul><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Widget.h </span></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">Widget</span> &#123; </span><br><span class="line"><span class="keyword">public</span>: </span><br><span class="line">  <span class="built_in">Widget</span>(); </span><br><span class="line">  ~<span class="built_in">Widget</span>(); <span class="comment">// 需要添加析构函数 </span></span><br><span class="line"></span><br><span class="line"><span class="keyword">private</span>: </span><br><span class="line">  <span class="keyword">struct</span> <span class="title class_">Impl</span>;   </span><br><span class="line">  Impl *pImpl; </span><br><span class="line">&#125;; </span><br><span class="line"></span><br><span class="line"><span class="comment">// Widget.cpp </span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&quot;Object.h&quot;</span> </span></span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">Widget</span>::Impl &#123; </span><br><span class="line">  Object obj; </span><br><span class="line">&#125;; </span><br><span class="line"></span><br><span class="line">Widget::<span class="built_in">Widget</span>() : <span class="built_in">pImpl</span>(<span class="keyword">new</span> Impl) &#123;&#125;; </span><br><span class="line">Widget::~<span class="built_in">Widget</span>() &#123; <span class="keyword">delete</span> pImpl; &#125;</span><br></pre></td></tr></table></figure><p>这样，在 <code>Widget.h</code> 中，就没有了对 <code>Object.h</code> 的引入，从而，修改 <code>Object.cpp</code> 时，引用 <code>Widget.h</code> 头文件的用户代码，就不会再重复编译。</p><h3 id="使用智能指针来重新实现"><a href="#使用智能指针来重新实现" class="headerlink" title="使用智能指针来重新实现"></a>使用智能指针来重新实现</h3><p>在现代 C++ 中，我们希望能尽可能避免显式使用 <code>new</code> 和 <code>delete</code> 关键字，而是把资源管理交给 RAII 技术。所以很容易想到使用智能指针来替代裸指针。因为 <code>pImpl</code> 是被 <code>Widget</code> 对象独占所有权的，所以使用 <code>unique_ptr</code> 是顺理成章的事情。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Widget.h </span></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">Widget</span> &#123; </span><br><span class="line"><span class="keyword">public</span>: </span><br><span class="line">  <span class="built_in">Widget</span>(); <span class="comment">// 看似不用写出析构函数了，实际这里是问题所在，后边介绍 </span></span><br><span class="line"></span><br><span class="line"><span class="keyword">private</span>: </span><br><span class="line">  <span class="keyword">struct</span> <span class="title class_">Impl</span>; </span><br><span class="line">  std::unique_ptr&lt;Impl&gt; *pImpl; </span><br><span class="line">&#125;; </span><br><span class="line"></span><br><span class="line"><span class="comment">// Widget.cpp </span></span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">Impl</span> &#123; </span><br><span class="line">  Object obj; </span><br><span class="line">&#125;; </span><br><span class="line"></span><br><span class="line">Widget::<span class="built_in">Widget</span>() : <span class="built_in">pImpl</span>(std::<span class="built_in">make_unique</span>&lt;Impl&gt;()) &#123;&#125;</span><br></pre></td></tr></table></figure><p>这个代码看似没问题，并且代码自身编译也没问题，但在调用 <code>Widget</code> 的用户代码那边，编译报错了。报错是，无法找到一个对 <code>Object</code> 的完整实例化。<code>unique_ptr</code> 作为指针，并不需要关心其指向的对象的实例化情况，所以这里看起来很奇怪。</p><p>原因是，编译器在为我们创造默认析构函数时，当看到 <code>unique_ptr</code>，会做一个 <code>static_assert</code>，检查 <code>unique_ptr</code> 是否能找到其指向对象的 <code>sizeof</code> 和 <code>delete</code> 运算符定义。这么做是考虑到生成更高效的析构代码。</p><p>为了强行解决这个问题，我们可以手动实现析构函数，但只需要生成默认版本的析构函数即可。析构函数放在 <code>Widget.cpp</code> 的末尾，这样，当编译器想要调用析构函数时，一定会先看到 <code>Impl</code> 的定义，也就能知道智能指针指向对象的信息了。另外，因为自定义析构函数后，编译器不会为我们生成默认移动构造函数和移动运算符函数，所以这两个函数也得手动实现一趟，当然，编译器默认生成的复制和移动版本是浅拷贝，就算不是这个原因，大概率也得手动生成一份深拷贝版本。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Widget.h </span></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">Widget</span> &#123; </span><br><span class="line"><span class="keyword">public</span>: </span><br><span class="line">  <span class="built_in">Widget</span>(); </span><br><span class="line">  ~<span class="built_in">Widget</span>(); </span><br><span class="line">  <span class="built_in">Widget</span>(<span class="type">const</span> Widget&amp;&amp;); </span><br><span class="line">  Widget&amp; <span class="keyword">operator</span>=(<span class="type">const</span> Widget&amp;&amp;); </span><br><span class="line">  <span class="built_in">Widget</span>(<span class="type">const</span> Widget&amp;); </span><br><span class="line">  Widget&amp; <span class="keyword">operator</span>=(<span class="type">const</span> Widget&amp;); </span><br><span class="line"></span><br><span class="line"><span class="keyword">private</span>: </span><br><span class="line">  <span class="keyword">struct</span> <span class="title class_">Impl</span>; </span><br><span class="line">  std::unique_ptr&lt;Impl&gt; *pImpl;</span><br><span class="line">&#125;; </span><br><span class="line"></span><br><span class="line"><span class="comment">// Widget.cpp </span></span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">Impl</span> &#123; </span><br><span class="line">  Object obj; </span><br><span class="line">&#125;; </span><br><span class="line"></span><br><span class="line">Widget::<span class="built_in">Widget</span>() : <span class="built_in">pImpl</span>(std::<span class="built_in">make_unique</span>&lt;Impl&gt;()) &#123;&#125; </span><br><span class="line">Widget::~<span class="built_in">Widget</span>() = <span class="keyword">default</span>; <span class="comment">// 可以用 default，但要放在后边 </span></span><br><span class="line"><span class="comment">// 复制和移动函数与运算符重载函数的定义这里省略</span></span><br></pre></td></tr></table></figure><p>言归正传，虽然 <code>unique_ptr</code> 作为独占所有权指针，用在这里再合适不可，但如果想使用 <code>shared_ptr</code>，就会发现，后者并没有前述的这些问题，这是因为 <code>unique_ptr</code> 的析构器是其类型的一部分，编译器会努力生成最优的代码来让 <code>unique_ptr</code> 做到裸指针的性能，而 <code>shared_ptr</code> 的析构器却不是类型的一部分，所以它不关心析构性能，也就没有在析构时去做完整实例化对象的检查。</p><hr><p>智能指针是现代 C++ 中很重要的一部分内容，值得仔细了解。但事实上，编写 demo 时，即使知道裸指针和 <code>unique_ptr</code> 的性能相同，但大多数人还是喜欢先使用裸指针，我仔细想过，唯一的原因可能就是语法差异，语法 <code>*</code> 和写几个单词（<code>auto</code> 和 <code>unique_ptr</code>，<code>make_unique</code>）还是有差距的。</p><p>适应新技术需要一个过程，但习惯之后，便会收获便捷和安全。</p><p>本系列的其他文章：</p><ol class="series-items"><li><a href="/posts/9bb75fe1.html" title="Effective Modern C++ 读书笔记：类型推导">Effective Modern C++ 读书笔记：类型推导</a></li><li><a href="/posts/f3206605.html" title="Effective Modern C++ 读书笔记：auto">Effective Modern C++ 读书笔记：auto</a></li><li><a href="/posts/57a898cb.html" title="Effective Modern C++ 读书笔记：转向现代C++">Effective Modern C++ 读书笔记：转向现代C++</a></li><li><a href="/posts/ce1aec80.html" title="Effective Modern C++ 读书笔记：智能指针">Effective Modern C++ 读书笔记：智能指针</a></li><li><a href="/posts/410ab8fb.html" title="Effective Modern C++：右值引用、移动语义和完美转发">Effective Modern C++：右值引用、移动语义和完美转发</a></li><li><a href="/posts/cd605d5c.html" title="Effective Modern C++：lambda 表达式">Effective Modern C++：lambda 表达式</a></li><li><a href="/posts/745d2232.html" title="Effective Modern C++：并发 API">Effective Modern C++：并发 API</a></li><li><a href="/posts/49749d08.html" title="Effective Modern C++：微调">Effective Modern C++：微调</a></li></ol><hr><div class="note info flat"><p>本文同步发布在知乎账号下：<a href="https://zhuanlan.zhihu.com/p/1943341648118547139">https://zhuanlan.zhihu.com/p/1943341648118547139</a></p></div>]]></content>
    
    
      
      
    <summary type="html">&lt;link rel=&quot;stylesheet&quot; class=&quot;aplayer-secondary-style-marker&quot; href=&quot;/assets/css/APlayer.min.css&quot;&gt;&lt;script src=&quot;/assets/js/APlayer.min.js&quot; cla</summary>
      
    
    
    
    <category term="软件开发" scheme="https://p2tree.top/categories/%E8%BD%AF%E4%BB%B6%E5%BC%80%E5%8F%91/"/>
    
    
    <category term="CPP" scheme="https://p2tree.top/tags/CPP/"/>
    
  </entry>
  
  <entry>
    <title>Effective Modern C++ 读书笔记：转向现代C++</title>
    <link href="https://p2tree.top/posts/57a898cb.html"/>
    <id>https://p2tree.top/posts/57a898cb.html</id>
    <published>2025-08-15T22:36:09.000Z</published>
    <updated>2025-08-15T14:43:12.000Z</updated>
    
    <content type="html"><![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/assets/css/APlayer.min.css"><script src="/assets/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><h2 id="条款-7：在创建对象时区分-和"><a href="#条款-7：在创建对象时区分-和" class="headerlink" title="条款 7：在创建对象时区分 () 和 {}"></a>条款 7：在创建对象时区分 () 和 {}</h2><p>这是一个令很多人头痛的问题，如果没有完全搞懂这里边的门道，是不会愿意使用 C++ 提供的大括号初始化语法的。或者，大多数人多少也有踩过坑的过去。</p><h3 id="大括号初始化语法的优点"><a href="#大括号初始化语法的优点" class="headerlink" title="大括号初始化语法的优点"></a>大括号初始化语法的优点</h3><p>讨论以下代码：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">int</span> <span class="title">x</span><span class="params">(<span class="number">0</span>)</span></span>;  <span class="comment">// 使用小括号来初始化 x 值为 0 </span></span><br><span class="line"><span class="type">int</span> y = <span class="number">0</span>; <span class="comment">// 使用等号来初始化 y 值为 0 </span></span><br><span class="line"><span class="type">int</span> z&#123;<span class="number">0</span>&#125;;  <span class="comment">// 使用大括号来初始化 z 值为 0 </span></span><br><span class="line"><span class="type">int</span> r = &#123;<span class="number">0</span>&#125;; <span class="comment">// 使用等号和大括号来初始化 r 值为 0</span></span><br></pre></td></tr></table></figure><p>以上这几种写法，本质上有什么区别？这些初始化语法都做到了相同的目的，但作为一个高级语言，这种混乱多变的用法，事实上并不讨喜。于是 C++ 11 引入了统一初始化语法，或者书中被称为大括号初始化语法。</p><p>大括号初始化除了能初始化一个单独的值，还可以直接初始化容器：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">std::vector&lt;<span class="type">int</span>&gt; v&#123;<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>&#125;; <span class="comment">// 初始化 v 中存放 3 个元素：1，2，3</span></span><br></pre></td></tr></table></figure><p>小括号初始化和等号初始化，在一些场合不能使用，但都可以替换成大括号初始化语法，大括号初始化语法适用于所有初始化的场景。</p><p>另外，大括号初始化的方式还会禁止不同类型的隐式窄化类型转换，比如，以下代码无法通过编译：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">double</span> x, y; </span><br><span class="line"><span class="type">int</span> s&#123;x + y&#125;; <span class="comment">// 编译失败 </span></span><br><span class="line"><span class="type">int</span> s = x + y; <span class="comment">// 编译成功</span></span><br></pre></td></tr></table></figure><p>这避免了一些潜在的精度损失问题。</p><p>最后，大括号初始化语法解决了 C++ 的解析语法问题。<br>讲一下这个问题。解析语法的一个示例为：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="function">Widget <span class="title">w</span><span class="params">()</span></span>;</span><br></pre></td></tr></table></figure><p>单纯看这个代码，你（和编译器）都不能判断出 <code>w</code> 到底是一个 <code>Widget</code> 的对象（调用默认构造函数），还是一个返回 <code>Widget</code> 的函数声明。编译器优先会将其解析为一个函数声明，具体要取决于上下文。</p><p>大括号初始化语法就直接将两者区分开来了。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Widget w&#123;&#125;; <span class="comment">// 一定是初始化 Widget 对象，而不是函数声明</span></span><br></pre></td></tr></table></figure><h3 id="大括号初始化的缺点"><a href="#大括号初始化的缺点" class="headerlink" title="大括号初始化的缺点"></a>大括号初始化的缺点</h3><p>然而，好消息总会伴随着坏消息。大括号初始化语法也有一些不适合使用的场景。</p><p>一个常见的问题就是它和初始化列表之间的纠葛。</p><p>如果一个类型的构造函数中，没有提供任何传入初始化列表作为参数的版本，那么一切都是正常的，但反之，选择调用构造函数时，编译器就会非常强烈地选择使用传入初始化列表作为参数的构造函数版本。如下代码：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Widget</span> &#123; </span><br><span class="line"><span class="keyword">public</span>: </span><br><span class="line">  <span class="built_in">Widget</span>(<span class="type">int</span> i, <span class="type">bool</span> b); </span><br><span class="line">  <span class="built_in">Widget</span>(std::initializer_list&lt;<span class="type">double</span>&gt; d); <span class="comment">// 这是那个很敏感的版本 </span></span><br><span class="line">  <span class="function"><span class="keyword">operator</span> <span class="title">float</span><span class="params">()</span> <span class="type">const</span></span>; <span class="comment">// 提供强制转换为 float 的行为 </span></span><br><span class="line">&#125;;  </span><br><span class="line"></span><br><span class="line"><span class="function">Widget <span class="title">w1</span><span class="params">(<span class="number">10</span>, <span class="literal">true</span>)</span></span>; <span class="comment">// 如果使用小括号来初始化，自然调用了第一个构造函数 </span></span><br><span class="line">Widget w2&#123;<span class="number">10</span>, <span class="literal">true</span>&#125;; <span class="comment">// 如果使用大括号来初始化，则一定调用第二个构造函数</span></span><br></pre></td></tr></table></figure><p>如代码中 <code>w2</code> 的初始化，编译器会强制使用初始化列表作为参数的构造函数，即使它需要额外做将 <code>int</code> 和 <code>bool</code> 转换为 <code>double</code> 的动作。这就是为什么说会 “非常强烈地”。</p><p>即使是编译器默认提供的构造函数，比如上例中的默认复制构造函数，也难逃意外。</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">Widget w0; </span><br><span class="line"><span class="function">Widget <span class="title">w1</span><span class="params">(w0)</span></span>; <span class="comment">// 如果使用小括号来初始化，调用了默认复制构造函数 </span></span><br><span class="line">Widget w2&#123;w0&#125;; <span class="comment">// 如果使用大括号来初始化，依然调用的是那个敏感的版本！</span></span><br></pre></td></tr></table></figure><p>编译器还是会对 <code>w2</code> 调用带有初始化列表作为参数的构造函数，它会将 <code>w0</code> 这个 <code>Widget</code> 类型转换为<code>float</code>，之后转换为 <code>double</code>，最后使用那个敏感的版本来构造 <code>w2</code>。</p><p>那么，在上边这些问题之后，以下代码声明，调用的是什么构造函数呢？</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">Widget w1;   <span class="comment">// 调用无参构造函数 </span></span><br><span class="line">Widget w2&#123;&#125;; <span class="comment">// 虽然使用了大括号，但实际上调用了无参默认构造函数 </span></span><br><span class="line"><span class="function">Widget <span class="title">w3</span><span class="params">()</span></span>; <span class="comment">// 又是解析语法问题，这是函数声明 </span></span><br><span class="line"><span class="function">Widget <span class="title">w4</span><span class="params">(&#123;&#125;)</span></span>; <span class="comment">// 调用了带初始化列表作为参数的构造函数，初始化列表为空 </span></span><br><span class="line">Widget w5&#123;&#123;&#125;&#125;; <span class="comment">// 和 w4 一样</span></span><br></pre></td></tr></table></figure><p>上边的内容可能比较太 “语言律师” 了，但它在实际工程中，确确实实会带来一些潜在的意外。比如：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="function">std::vector&lt;<span class="type">int</span>&gt; <span class="title">v1</span><span class="params">(<span class="number">10</span>, <span class="number">20</span>)</span></span>; <span class="comment">// 调用了 vector 的一个构造函数，创建了包含有 10 个元素，元素值都是 20 的 vector 对象 </span></span><br><span class="line">std::vector&lt;<span class="type">int</span>&gt; v2&#123;<span class="number">10</span>, <span class="number">20</span>&#125;; <span class="comment">// 调用了 vector 中带有初始化列表作为参数的构造函数，创建了包含 2 个元素，值分别是 10 和 20 的 vector 对象</span></span><br></pre></td></tr></table></figure><p>事实上我就在这里犯过错。</p><p>作为类的实现者，最好能避免用户犯这种错误，让用户使用类型时，不用在意它应该怎么写，才能调用到哪个构造函数。C++ STL 中的这种设计，事实上是个败笔，当然，根源还是 C++ 语法的问题。</p><p><em>陷阱：区分一个类型使用小括号来传入多个值，和用大括号来传入多个值的区别，尤其是标准库类型。</em></p><p>事实上，在工程实践中，很难快速意识到这种问题，比如说，最一开始的实现中，没有添加带有初始化列表的构造函数，之后也在很远的地方，正常使用大括号初始化语法来初始化类的对象（它会调用到构造函数），很久之后，我们又需要给类中添加带有初始化列表的构造函数版本，添加之后，发现测试用例挂了（感谢你提前编写了测试用例吧）。</p><p><em>陷阱：在为自定义类添加带有初始化列表作为参数的构造函数时，一定要小心。</em></p><p>现在，关于现代 C++ 中，是否建议使用大括号语法来初始化对象，依然是激烈争论的议题。两者各有优缺点。对于我们，<strong>最重要的是，理解上述的这些问题，并在使用大括号初始化语法和带有初始化列表的构造函数时，多留个心眼</strong>。</p><p>对于模板的实现者来说，这个问题更为头疼，因为模板的实现者无法决定用户会使用小括号还是大括号，从而便无法决定模板中的两种不同实例化，应该怎么做到统一实现。</p><h2 id="条款-8：优先选用-nullptr"><a href="#条款-8：优先选用-nullptr" class="headerlink" title="条款 8：优先选用 nullptr"></a>条款 8：优先选用 nullptr</h2><p>一条耳熟能详的 C++11 建议，使用 <code>nullptr</code> 替代 <code>NULL</code> 或 0 来作为空指针类型。</p><p>原因是 0 可能被解析为 <code>int</code>，而 <code>NULL</code> 在大多数库中的实现也只是宏定义到 0。<code>nullptr</code> 不具备整形类型，当然，它也不是某一种类型的指针，它的类型是 <code>std::nullptr_t</code>，它的能力是提供了可以转换为任意指针类型的类型转换操作。</p><p>这一条没有什么副作用，所以大多数程序员都已经无缝切换。</p><h2 id="条款-9：优先选用别名声明"><a href="#条款-9：优先选用别名声明" class="headerlink" title="条款 9：优先选用别名声明"></a>条款 9：优先选用别名声明</h2><p>C++ 11 标准中另一个实用的改进，允许我们使用 <code>using</code> 关键字来声明一些自定义类型，也就是类型别名，替代传统 C&#x2F;C++ 中的 <code>typedef</code> 语法。</p><p>替换并不只是让代码更清晰，毕竟并没有那么多场合需要声明很复杂的类型别名，但有一些是 <code>typedef</code> 做不到的。</p><p>一个例子是类型别名可以模板化。举例来说：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 这是一个存放多个类型及类型带有的自定义分配器的列表 </span></span><br><span class="line"><span class="keyword">template</span>&lt;<span class="keyword">typename</span> T&gt; <span class="keyword">using</span> MyAllocList = std::list&lt;T, MyAlloc&lt;T&gt;&gt;; </span><br><span class="line"></span><br><span class="line"><span class="comment">// 应用代码，声明一个 lw 的对象 </span></span><br><span class="line">MyAllocList&lt;Widget&gt; lw;</span><br></pre></td></tr></table></figure><p>如果使用 <code>typedef</code>，因为不能声明模板别名，只能编写一个自定义结构：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">template</span>&lt;<span class="keyword">typename</span> T&gt; </span><br><span class="line">sturct MyAllocList &#123; </span><br><span class="line">  <span class="keyword">typedef</span> std::list&lt;T, MyAlloc&lt;T&gt;&gt; type;</span><br><span class="line">&#125;; </span><br><span class="line"></span><br><span class="line"><span class="comment">// 应用代码 a </span></span><br><span class="line">MyALlocList&lt;Widget1&gt;::type lw; <span class="comment">// 不能省略 ::type </span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 应用代码 b </span></span><br><span class="line"><span class="comment">// 如果想在模板类型定义中使用这个自定义类型 </span></span><br><span class="line"><span class="keyword">template</span>&lt;<span class="keyword">typename</span> T&gt; </span><br><span class="line"><span class="keyword">class</span> <span class="title class_">Widget2</span> &#123; </span><br><span class="line">  <span class="keyword">typename</span> MyAllocList&lt;T&gt;::type list; <span class="comment">// 不能省略 typename 和 ::type </span></span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>编写的代码就要复杂一些，应用代码 b 中的用法，<code>typename</code> 不能省略，因为编译器需要知道后边的 <code>MyAllocList&lt;T&gt;::type</code> 是一个类型，而不是其他什么东西（因为从语法上来看，它仅仅只是对一个模板类内静态成员的引用）。</p><h3 id="类型特征相关的应用"><a href="#类型特征相关的应用" class="headerlink" title="类型特征相关的应用"></a>类型特征相关的应用</h3><p>也许你会认为，这种场景太少见了，然而，在库实现中，这种用法却到处都是，C++ 11 中的类型特征，就是采用这种原始的方式实现的，你会在各种地方看到诸如 <code>std::remove_const&lt;T&gt;::type</code> 这种以 <code>::type</code> 结尾的类型修饰词转换操作。如果这些场景下的应用代码都使用 <code>typedef</code>，那么将提高代码的复杂性。</p><p>当然，在 C++11 的库实现中，还是大量使用了 <code>typedef</code> 这种用法来定义类型特征，书中没有解释原因，不过，<code>using</code> 的好处在未来被人重视起来，C++14 开始，都添加了对应的类型别名版本，比如：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">std::remove_const&lt;T&gt;::type    <span class="comment">// C++11 中将 const T 类型转换为 T </span></span><br><span class="line">std::<span class="type">remove_const_t</span>&lt;T&gt;        <span class="comment">// C++14 中的版本，它们都会带有一个 _t 后缀来替代 ::type</span></span><br></pre></td></tr></table></figure><p>作者建议，无条件地优先使用 <code>using</code>，即使你使用的是 C++11，也去自己实现用类型别名来取代 <code>typedef</code> 实现类型特征：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">template</span> &lt;<span class="keyword">typename</span> T&gt; <span class="keyword">using</span> <span class="type">remove_const_t</span> = <span class="keyword">typename</span> remove_const&lt;T&gt;::type;</span><br></pre></td></tr></table></figure><h2 id="条款-10：优先选用枚举类"><a href="#条款-10：优先选用枚举类" class="headerlink" title="条款 10：优先选用枚举类"></a>条款 10：优先选用枚举类</h2><p>C++ 11 中的另一个新引入的语法特征，使用枚举类来取代枚举。在绝大多数场景下，你都应该这样做。下文把 <code>enum</code> 定义的类型叫做枚举，把 <code>enum class</code> 定义的类型叫做枚举类。</p><h3 id="这样替代的好处"><a href="#这样替代的好处" class="headerlink" title="这样替代的好处"></a>这样替代的好处</h3><p>我们知道，枚举是不限定作用域的，也就是说，通过枚举定义的类型，其作用域与定义枚举的作用域一致，而不是限定在枚举类型本身，这导致枚举类型的名字污染以及潜在的类型转换错误。而枚举类可以做到这一点。这带来很多好处，我相信不需要把书里的例子拿出来复述一遍了。</p><p>另外一个好处是，枚举类可以做前置声明（在表面上看）：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">enum</span> <span class="title class_">Color</span>; <span class="comment">// 预先前置声明枚举，后边再定义，编译会出错 </span></span><br><span class="line"><span class="keyword">enum class</span> <span class="title class_">Color</span>; <span class="comment">// 预先前置声明枚举类，可以编译</span></span><br></pre></td></tr></table></figure><p>究其原因，并不是语法上不允许，而是编译器需要在前置声明时，为枚举（或枚举类）选择一个默认的底层实现类型，而枚举没有默认的底层实现类型，所以编译器无法预先分配前置声明的类型。</p><p>所以，在 Pre-C++ 11，也可以通过手动指定枚举类型来实现将枚举做前置声明：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">enum</span> <span class="title class_">Color</span>: std::<span class="type">uint8_t</span>;</span><br></pre></td></tr></table></figure><p>枚举类不需要手动指定，就可以前置声明，因为枚举类有默认的底层实现类型：<code>int</code>。所以，这个好处是在不需要指定底层类型时，做前置声明。</p><h3 id="枚举仍然有用的地方"><a href="#枚举仍然有用的地方" class="headerlink" title="枚举仍然有用的地方"></a>枚举仍然有用的地方</h3><p>书中提到了一个枚举可能的用途。因为枚举可以做隐式类型转换，而枚举类不可以，所以在一些明确需要编译器完成隐式类型转换的场合，枚举就有用了。比如说 <code>std::tuple</code> 下的 <code>std::get&lt;&gt;</code> 操作，后者会按指定的静态下标来获取元组中的元素。</p><p>考虑以下代码：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">using</span> UserInfo = std::tuple&lt;std::string, std::string, std::<span class="type">size_t</span>&gt;; </span><br><span class="line">UserInfo info; </span><br><span class="line"></span><br><span class="line"><span class="comment">// 获取元素最常见的写法是 </span></span><br><span class="line"><span class="keyword">auto</span> val = std::<span class="built_in">get</span>&lt;<span class="number">1</span>&gt;(info); <span class="comment">// 取下标 1 的元素</span></span><br></pre></td></tr></table></figure><p>但这种写法，从调用方来看，很难直观地看到获取的 <code>val</code> 是什么东西。<br>所以我们习惯用枚举来作为下标，因为枚举定义的默认值是从 0 开始的整形，刚好可以用来做下标值：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">enum</span> <span class="title class_">UserInfoFields</span> &#123; uiName, uiEmail, uiReputation &#125;; </span><br><span class="line">UserInfo info; </span><br><span class="line"></span><br><span class="line"><span class="comment">// 使用枚举作为下标获取元素 </span></span><br><span class="line"><span class="keyword">auto</span> val = std::<span class="built_in">get</span>&lt;uiEmail&gt;(info); <span class="comment">// 明显看出是获取 Email 域</span></span><br></pre></td></tr></table></figure><p>然而，如果使用的是枚举类，则代码会冗长一些：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">enum class</span> <span class="title class_">UserInfoFields</span> &#123; uiName, uiEmail, uiReputation &#125;; </span><br><span class="line">UserInfo info; </span><br><span class="line"></span><br><span class="line"><span class="comment">// 使用枚举类作为下标获取元素 </span></span><br><span class="line"><span class="comment">// 因为枚举类不能做隐式类型转换，所以需要显式完成 </span></span><br><span class="line"><span class="keyword">auto</span> val = std::get&lt;<span class="built_in">static_cast</span>&lt;std::<span class="type">size_t</span>&gt;(UserInfoFields::uiEmail)&gt;(info);</span><br></pre></td></tr></table></figure><p>其他办法也不会比它更简单。</p><p>当然，在我看来，这个遗留问题，更应该拷问下，是不是 <code>std::get&lt;&gt;</code> 的设计有问题，如果 C++ 可以提供 <code>info.uiEmail</code> 这种更便捷的语法，这里的问题便不攻自破了。</p><h2 id="条款-11：优先选用-delete-来删除函数"><a href="#条款-11：优先选用-delete-来删除函数" class="headerlink" title="条款 11：优先选用 delete 来删除函数"></a>条款 11：优先选用 delete 来删除函数</h2><p>在 C++11 之前，如果我们需要删除类内的一些类成员函数，比如编译器自动生成的那些构造函数，做法是将这些函数显式声明出来，并放在 <code>private</code> 区中，同时不去实现它。</p><p>这样，如果类外部的对象调用这些被删除的函数，则编译器会因为这些函数位于 <code>private</code> 而阻止调用，如果类内调用这些被删除的函数，则编译器会因为这些函数没有被定义而报错。</p><p>在 C++11 中，无条件将这种做法替换为使用 <code>delete</code> 关键字来标记，也就是在类成员函数声明的末尾添加 <code>= delete;</code> 语法。编译器会保证这些函数不允许被实现和调用。</p><p><code>delete</code> 的第一个优点便是简单，比之前的做法少写一点代码。但其另外一个无法被取代的优点是，它可以用来修饰一个普通函数（类外定义的函数）。举例来说：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 我们有一个函数 </span></span><br><span class="line"><span class="function"><span class="type">bool</span> <span class="title">isLucky</span><span class="params">(<span class="type">int</span> number)</span></span>; </span><br><span class="line"></span><br><span class="line"><span class="comment">// 因为 C++ 会对类型做隐式变换，所以以下调用，都可以通过编译 </span></span><br><span class="line"><span class="built_in">isLucky</span>(<span class="string">&#x27;a&#x27;</span>); </span><br><span class="line"><span class="built_in">isLucky</span>(<span class="literal">true</span>); </span><br><span class="line"><span class="built_in">isLucky</span>(<span class="number">3.5</span>);</span><br></pre></td></tr></table></figure><p>如果我们不允许一些特殊的类型转换，就需要利用 <code>delete</code> 来删除一些 “重载版本”：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">bool</span> <span class="title">isLucky</span><span class="params">(<span class="type">char</span>)</span> </span>= <span class="keyword">delete</span>; </span><br><span class="line"><span class="function"><span class="type">bool</span> <span class="title">isLucky</span><span class="params">(<span class="type">bool</span>)</span> </span>= <span class="keyword">delete</span>; </span><br><span class="line"><span class="function"><span class="type">bool</span> <span class="title">isLucky</span><span class="params">(<span class="type">double</span>)</span> </span>= <span class="keyword">delete</span>; </span><br><span class="line"></span><br><span class="line"><span class="comment">// 以下调用会报错 </span></span><br><span class="line"><span class="built_in">isLucky</span>(<span class="string">&#x27;a&#x27;</span>);  <span class="comment">// 错误 </span></span><br><span class="line"><span class="built_in">isLucky</span>(<span class="literal">true</span>); <span class="comment">// 错误 </span></span><br><span class="line"><span class="built_in">isLucky</span>(<span class="number">3.5</span>);  <span class="comment">// 错误</span></span><br></pre></td></tr></table></figure><p>另外，<code>delete</code> 还可以删除那些不希望被模板实例化的模板类型：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">template</span>&lt;<span class="keyword">typename</span> T&gt; <span class="type">void</span> <span class="title">processPointer</span><span class="params">(T* ptr)</span></span>; </span><br><span class="line"></span><br><span class="line"><span class="comment">// 下边这种实例化被删除 </span></span><br><span class="line"><span class="keyword">template</span>&lt;&gt; <span class="type">void</span> <span class="built_in">processPointer</span>&lt;<span class="type">void</span> *&gt;(<span class="type">void</span>*) = <span class="keyword">delete</span>;</span><br></pre></td></tr></table></figure><p>值得一提的是，如果模板函数是类内的成员函数，则无法通过改到 <code>private</code> 中来删除实例化，只能通过 <code>delete</code> 来删除。所以，<code>private</code> 的方案也并不总是能应用在类内成员函数的删除。</p><h2 id="条款-12：留意使用-override-来声明需要改写的成员函数"><a href="#条款-12：留意使用-override-来声明需要改写的成员函数" class="headerlink" title="条款 12：留意使用 override 来声明需要改写的成员函数"></a>条款 12：留意使用 override 来声明需要改写的成员函数</h2><p>首先需要明确函数的 <strong>改写</strong> 和 <strong>重载</strong> 的区别。重载可以实现多个函数名称相同，但函数参数类型不同的一系列函数，从而方便调用使用；而改写是派生类对象中，实现和基类中 <strong>相同</strong> 的虚函数，从而可以让多态行为生效（有时也叫做 <strong>覆写</strong> 或 <strong>重写</strong>）。</p><p>上文中我高亮了 <strong>相同</strong> 两个字，在 C++ 标准中，需要满足一定的约束，才能实现改写的目的。<br>在 C++98 中，已经提出的约束有：</p><ul><li>基类中的函数必须是虚函数。</li><li>基类和派生类中的函数必须同名（析构函数除外）。</li><li>基类和派生类中的函数参数类型必须完全一样。</li><li>基类和派生类中的函数常量性必须完全一样。</li><li>基类和派生类中的函数返回值类型和异常规格必须能够兼容。</li></ul><p>在 C++11 中，新增了一条：</p><ul><li>基类和派生类中的函数需要使用相同的引用修饰词。</li></ul><p><em>陷阱：C++11 中，需要改写的成员函数，需要保证具有相同的引用性质。</em></p><p>有关于引用修饰词，书中也在该小节予以大篇幅的介绍。引用修饰词是对成员函数的修饰，来标记该成员函数应该在对象是左值引用还是右值引用时被调用，因为有些场景中，我们希望调用左值引用对象的成员函数与调用右值引用对象的成员函数时，采用不同的实现策略。</p><p>我个人认为这部分内容和条款本身没有直接联系，所以不再做展开。</p><p>回到条款中，如果希望改写类成员函数，则无条件建议在派生类中，对要改写的函数使用 <code>override</code> 来修饰。编译器会对这种修饰词做检查，排查出任何与成员函数改写相关的意外、漏写或设计错误。</p><p>另外，C++11 还提供了另一个关键字 <code>final</code>，和 <code>override</code> 刚好相反，被修饰为 <code>final</code> 的成员函数，将拒绝被其后边继承的派生类中改写。推荐在编写类层次结构时，总是留意去使用这两个关键字，它们可能会帮你大忙。</p><h2 id="条款-13：优先选用-const-iterator-代替-iterator"><a href="#条款-13：优先选用-const-iterator-代替-iterator" class="headerlink" title="条款 13：优先选用 const_iterator 代替 iterator"></a>条款 13：优先选用 const_iterator 代替 iterator</h2><p><code>const_iterator</code> 用于指定指向带有 <code>const</code> 属性的 <code>iterator</code> 类型，所以它所指向的值，内容不可修改。建议在可能的情况下，使用 <code>const_iterator</code> 代替 <code>iterator</code>。</p><p>C++98 中 <code>const_iterator</code> 的实现不完整，所以若想在 C++98 中使用，需要做一些额外的约束。C++ 98 中没有提供 <code>cbegin()</code> 和 <code>cend()</code> 等操作，所以需要这样使用：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">std::vector&lt;<span class="type">int</span>&gt; values; </span><br><span class="line"><span class="keyword">auto</span> ConstIterator = </span><br><span class="line">  std::<span class="built_in">find</span>( <span class="keyword">static_cast</span>&lt;std::vector&lt;<span class="type">int</span>&gt;::const_iterator&gt;(values.<span class="built_in">begin</span>()), </span><br><span class="line">             <span class="keyword">static_cast</span>&lt;std::vector&lt;<span class="type">int</span>&gt;::const_iterator&gt;(values.<span class="built_in">end</span>()), </span><br><span class="line">             <span class="number">100</span>);</span><br></pre></td></tr></table></figure><p>不需要特别了解这种用法，现在使用 C++98 的场合并不多了，即使有那种环境，使用迭代器而不是指针的场合就更少了。</p><p>在 C++11 中，标准的写法变成了调用 <code>values.cbegin()</code> 和 <code>values.cend()</code>，不需要我多举例子了。</p><p>讨论另一个话题。现代 C++ 中，除了为大多数标准库容器提供 <code>cbegin()</code> 和 <code>cend()</code> 接口之外，还提供了非成员函数版本的 <code>cbegin()</code> 和 <code>cend()</code>，那么，什么时候使用成员函数版本，什么时候使用非成员函数版本？或者说，非成员函数版本的意义是什么？</p><p>答案是，在一些容器中，或类似容器的数据结构，如数组中，是没有成员函数版本的迭代器获取接口的，如果在泛型的实现中，调用成员函数版本的迭代器获取，便会导致编译错误，而使用非成员函数的版本，则可以充分兼容这种情况。</p><p>举例来说：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 我们实现一个泛型的容器操作，可以接受各种不同的容器类型 </span></span><br><span class="line"><span class="keyword">template</span>&lt;<span class="keyword">typename</span> C, <span class="keyword">typename</span> V&gt; <span class="comment">// C 是容器类型，V 是容器中元素类型 </span></span><br><span class="line"><span class="function"><span class="type">void</span> <span class="title">findAndInsert</span><span class="params">(C&amp; container, <span class="type">const</span> V&amp; targetVal, <span class="type">const</span> V&amp; insertVal)</span> </span>&#123; </span><br><span class="line">  <span class="keyword">using</span> std::cbegin; </span><br><span class="line">  <span class="keyword">using</span> std::cend; <span class="comment">// 将非成员函数版本的 cbegin 和 cend 导入命名空间 </span></span><br><span class="line"></span><br><span class="line">  <span class="keyword">auto</span> it = std::<span class="built_in">find</span>(<span class="built_in">cbegin</span>(container), <span class="built_in">cend</span>(container), targetVal);   </span><br><span class="line">  container.<span class="built_in">insert</span>(it, insertVal); </span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>很明显可以看出来，如果容器 <code>C</code> 没有内置的成员函数版本 <code>cbegin()</code> 和 <code>cend()</code>，这个实现也是可以工作的。</p><p><em>谬误：非成员函数版本的</em> <em><code>cbegin()</code></em> <em>和</em> <em><code>cend()</code></em> <em>与成员函数版本的</em> <em><code>cbegin()</code></em> <em>和</em> <em><code>cend()</code>，在大多数情况下都一样，但建议在可能的情况下，还是使用非成员函数版本。</em></p><p><em>陷阱：不过需要注意，非成员函数版本的</em> <em><code>cbegin()</code></em> <em>和</em> <em><code>cend()</code></em> <em>在 C++14 中才提供，而在 C++11 中不存在。所以，将你的项目中构建参数里用</em> <em><code>--std=c++14</code></em> <em>代替</em> <em><code>--std=c++11</code></em> <em>吧。</em></p><p>如果你的项目中不得不使用 C++11，那么实现一个非成员函数版本的 <code>cbegin</code> 也非常方便：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">template</span>&lt;<span class="keyword">typename</span> C&gt; </span></span><br><span class="line"><span class="function"><span class="keyword">auto</span> <span class="title">cbegin</span><span class="params">(<span class="type">const</span> C&amp; container)</span> -&gt; <span class="title">decltype</span><span class="params">(std::begin(container))</span> </span>&#123; </span><br><span class="line">  <span class="keyword">return</span> std::<span class="built_in">begin</span>(container); </span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>实现方案非常取巧，直觉上，在实现体中，应该会写成：<code>return container.cbegin();</code>，但因为有一些容器，它并没有成员函数版本的 <code>cbegin()</code>，所以这样不可行。上边的实现策略，依据是：如果非成员函数版本的 <code>begin()</code> 中传入的是一个 <code>const</code> 类型容器，那么它返回的是 <code>const_iterator</code>，所以这个实现的重点在模板函数的参数声明上。这种实现，对数组也一样适用。</p><h2 id="条款-14：只要函数不发射异常，就使用-noexcept-声明"><a href="#条款-14：只要函数不发射异常，就使用-noexcept-声明" class="headerlink" title="条款 14：只要函数不发射异常，就使用 noexcept 声明"></a>条款 14：只要函数不发射异常，就使用 noexcept 声明</h2><p>这一条聊一下异常，C++ 中，异常的使用这部分话题，是高频被讨论到的话题。然而本条款更关注的是，如果在使用异常时，让编译器尽可能简化对异常代码的处理。</p><p>在实现接口函数时，如果明知道一个函数不会抛出异常，但没有使用 <code>noexcept</code> 声明，那这就是接口设计缺陷。使用 <code>noexcept</code>，可以让函数以及调用函数的其他函数，能够生成更好的代码。</p><p>在 C++11 之前，我们使用 <code>throw()</code> 来修饰一个函数会抛出异常，但这种修饰，编译器能做的优化并不多，所以不建议再使用。</p><p>使用 <code>noexcept</code> 修饰后，编译器就会做一些更激进的优化。讨论一个复杂的例子。C++11 中的移动语义，由于需要考虑异常，所以，只有在被 <code>noexcept</code> 修饰后，才会说明移动的过程不会发生异常（也就是说，不会因为中途出现异常，导致了数据被破坏），编译器才能妥善放心地使用移动操作。</p><p><code>swap()</code> 函数是 C++ 标准库中一个很常见的操作，然而，它有多种不同的实现，最容易理解的就是，我们是应该采用复制操作来交换数据，还是用移动操作来交换数据，移动操作显然更高效，但其前提便是，<code>swap(a, b)</code> 中的两个输入，都必须是 <code>noexcept</code> 的。<br>一个典型的 <code>swap</code> 函数声明为：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">void</span> <span class="title">swap</span><span class="params">(pair &amp;p)</span> <span class="title">noexcept</span><span class="params">(<span class="keyword">noexcept</span>(swap(first, p.first)) &amp;&amp; <span class="keyword">noexcept</span>(swap(second, p.second)))</span></span>;</span><br></pre></td></tr></table></figure><p>末尾的声明称为 <strong>条件式 noexcept 声明</strong>，满足 <code>swap</code> 是否可以为 <code>noexcept</code> 的前提是，传入的每个输入，都必须是 <code>noexcept</code> 的。</p><p>所以可以看得出来，如果想保证函数调用时，尽可能会通过 <code>noexcept</code> 修饰，就必须让其相关函数都是 <code>noexcept</code> 的。</p><p>然而，大多数函数其实是中立的，虽然自身不抛出异常，但其内部调用的函数（和更深的调用层次中的函数），可能会抛出异常，所以它也不能使用 <code>noexcept</code> 修饰。</p><p>当然，也不能刻意为了 <code>noexcept</code> 而扭曲了函数实现，那就是主次不分了。如果已经为接口函数添加了 <code>noexcept</code> 修饰，但是在将来又删除这个修饰，那么很有可能，在很多调用该函数的其他场景中，造成很多编译错误。</p><p>在 C++11 中，所有的析构函数是默认带有 <code>noexcept</code> 属性的，不需要手动添加。</p><h2 id="条款-15：尽可能地使用-constexpr"><a href="#条款-15：尽可能地使用-constexpr" class="headerlink" title="条款 15：尽可能地使用 constexpr"></a>条款 15：尽可能地使用 constexpr</h2><p>C++11 中引入了一个新的关键字 <code>constexpr</code>，它和 <code>const</code> 的区别用一句话就可以清晰的说明。<code>const</code> 是表示对象的不可变性（后续无法修改该对象），<code>constexpr</code> 是表示对象的常量性（编译期便可求值）。过去，我们对 <code>const</code> 等价于 <strong>常量</strong> 的错误观念，应该被纠正。</p><p><em>谬误：<code>const</code></em> <em>表示的是不可变性，其反义词是</em> <em><code>mutable</code>，并不表示常量。<code>constexpr</code></em> <em>才表示常量。至于关键字的取名，属于 C++ 特色了，取名总是很难与其实际意义相契合。</em></p><p>所以，<code>constexpr</code> 的对象，一定是 <code>const</code> 的（常量是不可变的），但反过来不成立（不可变的变量可能是受约束的变量）。</p><p>由于 <code>constexpr</code> 修饰的是常量，所以编译器可以在编译期对这种对象做求值，体现出来的便是，会把运行时的一些开销，移动到编译时完成。</p><p>如果使用 <code>constexpr</code> 修饰函数，并不意味着这个函数一定可以在编译期求值，它只是表示，当函数的参数都是常量时，编译器才会对这个函数做编译期求值，否则则和正常函数没有区别。</p><p><em>谬误：使用</em> <em><code>constexpr</code></em> <em>修饰的函数，只有当其参数都是常量时，才会做编译期求值。</em></p><p>在 C++11 中，<code>constexpr</code> 修饰函数还有一些受限，比如只能有一个 <code>return</code>，也不能存在条件语句，这是为了让编译期能更好的完成求值计算。不过，在 C++14 中，这种没必要的约束被解除了。所以，有条件时，尽量用 C++14 代替 C++11。</p><h3 id="使用-constexpr-修饰类"><a href="#使用-constexpr-修饰类" class="headerlink" title="使用 constexpr 修饰类"></a>使用 <code>constexpr</code> 修饰类</h3><p>如果用 <code>constexpr</code> 修饰了类的构造函数，那么很可能会让类定义的对象，也成为一个常量。这很神奇，但确实存在，如书中示例：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Point</span> &#123; </span><br><span class="line"><span class="keyword">public</span>: </span><br><span class="line">  <span class="function"><span class="keyword">constexpr</span> <span class="title">Point</span><span class="params">(<span class="type">double</span> xVal = <span class="number">0</span>, <span class="type">double</span> yVal = <span class="number">0</span>)</span> <span class="keyword">noexcept</span> : x(xVal), y(yVal) &#123;</span>&#125; </span><br><span class="line">  <span class="function"><span class="keyword">constexpr</span> <span class="type">double</span> <span class="title">xValue</span><span class="params">()</span> <span class="type">const</span> <span class="keyword">noexcept</span> </span>&#123; <span class="keyword">return</span> x; &#125; </span><br><span class="line">  <span class="function"><span class="keyword">constexpr</span> <span class="type">double</span> <span class="title">yValue</span><span class="params">()</span> <span class="type">const</span> <span class="keyword">noexcept</span> </span>&#123; <span class="keyword">return</span> y; &#125; </span><br><span class="line"></span><br><span class="line"><span class="keyword">private</span>: </span><br><span class="line">  <span class="type">double</span> x, y; </span><br><span class="line">&#125;; </span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">constexpr</span> Point <span class="title">p1</span><span class="params">(<span class="number">10.1</span>, <span class="number">20.2</span>)</span></span>; <span class="comment">// 编译期求值的常量对象，值放在只读内存中</span></span><br></pre></td></tr></table></figure><p>因为它是常量对象，所以也可以用在任何使用 <code>constexpr</code> 修饰的常量函数中：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">constexpr</span> Point <span class="title">midPoint</span><span class="params">(<span class="type">const</span> Point &amp;p1, <span class="type">const</span> Point &amp;p2)</span> <span class="keyword">noexcept</span> </span>&#123;</span><br><span class="line">   <span class="keyword">return</span> &#123; (p<span class="number">1.</span><span class="built_in">xValue</span>() + p<span class="number">2.</span><span class="built_in">xValue</span>()) / <span class="number">2</span>, (p<span class="number">1.</span><span class="built_in">yValue</span>() + p<span class="number">2.</span><span class="built_in">yValue</span>()) / <span class="number">2</span>&#125;; </span><br><span class="line">&#125; </span><br><span class="line"></span><br><span class="line"><span class="keyword">constexpr</span> <span class="keyword">auto</span> mid = <span class="built_in">midPoint</span>(p1, p2);</span><br></pre></td></tr></table></figure><p>这意味着，mid 这个对象，其构造过程涉及到了构造函数，访问器，以及非成员函数的调用，但它依然是一个常量，可以放在只读内存中。</p><p>不过，如果类中的成员函数修改了对象的成员数据，则在 C++11 中，不能修饰为 <code>constepxr</code>，因为我们理应认为，这种函数没有不可变性。不过，在 C++14 中，这种约束也被去掉了。所以，在 C++14 中，可以编写：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Point</span> &#123; </span><br><span class="line"><span class="keyword">public</span>: </span><br><span class="line">  <span class="function"><span class="keyword">constexpr</span> <span class="title">setX</span><span class="params">(<span class="type">double</span> newX)</span> <span class="keyword">noexcept</span> </span>&#123; x = newX; &#125; </span><br><span class="line">  <span class="function"><span class="keyword">constexpr</span> <span class="title">setY</span><span class="params">(<span class="type">double</span> newY)</span> <span class="keyword">noexcept</span> </span>&#123; y = newY; &#125; </span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>使用 <code>constexpr</code> 没有副作用，所以在可以使用它的时候，还是尽量去使用它。但如果你是在编写接口函数，在添加 <code>constexpr</code> 修饰时，一定想清楚，不要在将来又想要删除它，那时候，可能和 <code>noexcept</code> 一样，会遇到很多编译错误。</p><h2 id="条款-16：小心-const-成员函数的线程安全性"><a href="#条款-16：小心-const-成员函数的线程安全性" class="headerlink" title="条款 16：小心 const 成员函数的线程安全性"></a>条款 16：小心 const 成员函数的线程安全性</h2><p>从条款标题上看，似乎 const 修饰的成员函数，应该不可能存在线程安全性，因为 const 修饰后，它们是只读操作，不会对外部数据产生修改和破坏性的影响。</p><p>然而，C++ 还提供了 <code>mutable</code> 这个关键字，它自相矛盾地设计了这样一种机制：在类的 const 成员函数中，允许修改被 <code>mutable</code> 修饰的成员变量。比如：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Widget</span> &#123; </span><br><span class="line"><span class="keyword">public</span>: </span><br><span class="line">  <span class="function"><span class="type">void</span> <span class="title">count</span><span class="params">()</span> <span class="type">const</span> </span>&#123; </span><br><span class="line">    <span class="comment">// val1++;    // 非法，编译器报错，const 函数中无法修改     </span></span><br><span class="line">    val2++;       <span class="comment">// 合法 </span></span><br><span class="line">  &#125; </span><br><span class="line"><span class="keyword">private</span>: </span><br><span class="line">  <span class="type">int</span> val1; <span class="keyword">mutable</span> <span class="type">int</span> val2; </span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>看起来很不合理，对吧？</p><p>因为这个机制，所以 const 成员函数，并不完全可以认为是只读的，从而，在这种场合下，如果这个成员函数刚好位于多线程上下文中，就很有可能出现常见的并发问题，就如同那些普通的并发场景下的问题一样的问题。</p><p><em>陷阱：const 成员函数，不一定是完全不会对外界产生影响的函数。留意那些 mutable 修饰的成员变量。</em></p><p>解决方案，书中列举了很多，但我认为没必要展开，因为它脱离了本条款的重点，加锁、原子操作等常见的方案都可以处理，只要把那个看起来是 const 的成员函数，当作普通函数处理就好了。</p><h3 id="为什么-C-要有这种自相矛盾的设计"><a href="#为什么-C-要有这种自相矛盾的设计" class="headerlink" title="为什么 C++ 要有这种自相矛盾的设计"></a>为什么 C++ 要有这种自相矛盾的设计</h3><p><code>mutable</code> 很早就有了，事实上它不比 <code>const</code> 来的更晚。这种设计的场合，通常发生在对接口函数的维护中。</p><p>假设你有一个已经公开的接口类，其中的 const 成员函数接口已经被很多地方引用，如果此时，因为一些与 const 本身要保护的主体不太相关的需求，需要破坏该接口的 const 属性，应该怎么办？使用 <code>mutable</code> 便是一个办法。</p><p>举个具体的例子，如果有一个控制数据库的类，其中有一个 const 成员函数是查询数据库信息。但现在有一个新需求，要求在每次查询数据库信息时，将查询日志写入一个位置，在不删除 const 属性的前提下，一种解决办法，便是把日志存放对象修饰为 <code>mutable</code>，这样就可以在 const 成员函数中写入日志了。写入日志本身是非 const 的操作，但它不违背这个成员函数添加 const 的初衷，即保证该函数不会改变数据库数据。</p><p>所以，在这些场合下，如果引入多线程，便可能产生奇怪的问题，这些问题非常难定位。这就是这一条款要表达的内容。</p><h2 id="条款-17：注意特殊成员函数的隐式生成规则"><a href="#条款-17：注意特殊成员函数的隐式生成规则" class="headerlink" title="条款 17：注意特殊成员函数的隐式生成规则"></a>条款 17：注意特殊成员函数的隐式生成规则</h2><p>特殊成员函数包括默认构造函数、析构函数、复制构造函数和复制赋值运算符函数，在 C++11 之后，又增加了移动构造函数和移动赋值运算符函数。编译器可能为一些类自动生成其中部分或全部特殊成员函数。本条规则需要留意，它们的隐式行为可能导致程序错误或性能衰退。</p><p>编译器隐式生成特殊构造函数的规则是：</p><ul><li>默认构造函数：当类中不包含任何用户声明的构造函数时才生成。</li><li>析构函数：当类中不包含用户声明的析构函数时才生成。如果基类中的析构函数是 <code>virtual</code> 的，那么此时生成的析构函数，也是 <code>virtual</code> 的。C++11 引入了 <code>noexcept</code> 关键字，生成的析构函数也默认带有 <code>noexcept</code> 属性。</li><li>复制构造函数：只有当类中不包含用户自定义的复制构造函数、移动构造函数和移动复制运算符时，才会自动生成。<strong>注意，如果类中包含了用户自定义的复制赋值运算符，复制构造函数仍然会自动生成，这一点和移动操作函数不同。</strong></li><li>复制赋值运算符：和复制构造函数相同。</li><li>移动构造函数：只有当类中不包含用户自定义的复制操作、移动操作和析构函数时，才会自动生成。<strong>注意，和复制操作不同，移动构造函数和移动赋值运算符是互斥的，其中一个存在，另一个不会自动生成，而复制操作没有这个规则。</strong></li><li>移动赋值运算符：和移动构造函数相同。</li></ul><p>C++ 本有意将两者统一起来，避免上述加粗内容的歧义，但考虑到代码兼容的问题，此条只能成为建议，而不是规则。书中称为 “大三律（Rule of Three）”：如果类中声明了复制构造函数、复制赋值运算符或析构函数中的任何一个，就应该同时声明这三个。</p><p><em>陷阱：复制构造函数和复制赋值运算符的隐式生成是独立的；移动构造函数和移动赋值运算符的隐式生成是互斥的。</em></p><p><em>建议：大三律规则，如果类中声明了复制构造函数、复制赋值运算符或析构函数中的任何一个，那么就应该同时声明这三个。</em></p><p>大三律的理由是，如果一个类中涉及到了这三种特殊成员函数中的任何一个，说明这个类会做资源管理，如果它会做资源管理，那么这三个就应该同时存在。进一步地，如果用户自定义了这三个之中任何一个成员函数，那么其他成员函数就也应该交由用户自定义，而不是自动生成，但 C++98 中没有对此做约束。</p><p>在 C++11 中，成功让大三律能自动推广到移动操作，使之成为规则而不仅仅是建议，即其中之一存在自定义版本时，就不会自动生成其他几个成员函数。</p><p>需要留意，移动构造函数和移动赋值运算符函数，不一定真的会让对象以移动的方式构造或赋值，能否移动取决于这个对象的所有组成部分是否可以移动。如果不能，移动构造函数和移动赋值运算符函数会转而以复制的方式来执行。</p><p>原理上来说，它们使用了 <code>std::move()</code> 操作，而这个操作，实际做的只是尝试性把左值引用类型转换为右值引用。第五章的条款会展开这个话题。</p><p>使用 <code>=default</code> 可以强行让编译器生成默认特殊成员函数，这样将隐式生成改为显式生成，让代码更容易理解，也可以避免意外地隐式生成规则生效或失效导致的一系列问题。</p><p>比如说下边的示例：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">StringTable</span> &#123; </span><br><span class="line"><span class="keyword">public</span>: </span><br><span class="line">  <span class="built_in">StringTable</span>() &#123;&#125; ... <span class="comment">// 没有实现其他复制、移动和析构函数 </span></span><br><span class="line"></span><br><span class="line"><span class="keyword">private</span>:   </span><br><span class="line">  std::map&lt;<span class="type">int</span>, std::string&gt; values; <span class="comment">// 沉重的负载 </span></span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>如果我们在未来的某一天，或者是其他开发人员，对此代码做了一些修改。比如想要在资源申请和释放时，添加日志记录，这需求非常常见。为了释放时记录日志，我们不得不自定义析构函数，但我们忘记了大三律规则：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">StringTable</span> &#123; </span><br><span class="line"><span class="keyword">public</span>: </span><br><span class="line">  <span class="built_in">StringTable</span>() &#123; <span class="built_in">makeLog</span>(<span class="string">&quot;Create&quot;</span>); &#125; ... <span class="comment">// 没有实现其他复制、移动操作 </span></span><br><span class="line">  ~<span class="built_in">StringTable</span>() &#123; <span class="built_in">makeLog</span>(<span class="string">&quot;Destroy&quot;</span>); &#125; <span class="comment">// 增加了自定义的析构函数 </span></span><br><span class="line"></span><br><span class="line"><span class="keyword">private</span>:   </span><br><span class="line">  std::map&lt;<span class="type">int</span>, std::string&gt; values;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>这种改动似乎合理，但却引入了很严重的性能问题。添加自定义析构函数后，会阻止移动操作的隐式生成，但不会阻止复制操作的隐式生成。所以，第一个版本中，对象可以通过移动操作来高效移动，但添加日志后，我们发现对象只能用复制操作了，因为负载很大，所以引入了严重的性能下降。</p><p>是不是很 amazing 呀！一个简单的改进：</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">StringTable</span> &#123; </span><br><span class="line"><span class="keyword">public</span>: </span><br><span class="line">  <span class="built_in">StringTable</span>() &#123; <span class="built_in">makeLog</span>(<span class="string">&quot;Create&quot;</span>); &#125; </span><br><span class="line">  <span class="built_in">StringTable</span>(<span class="type">const</span> StringTable &amp;) = <span class="keyword">default</span>; <span class="comment">// 显式要求编译器生成默认版本   </span></span><br><span class="line">  StringTable &amp;<span class="keyword">operator</span>=(<span class="type">const</span> StringTable &amp;) = <span class="keyword">default</span>; </span><br><span class="line">  <span class="built_in">StringTable</span>(<span class="type">const</span> StringTable &amp;&amp;) = <span class="keyword">default</span>;   </span><br><span class="line">  StringTable &amp;<span class="keyword">operator</span>=(<span class="type">const</span> StringTable &amp;&amp;) = <span class="keyword">default</span>; </span><br><span class="line">  ~<span class="built_in">StringTable</span>() &#123; <span class="built_in">makeLog</span>(<span class="string">&quot;Destroy&quot;</span>); &#125; </span><br><span class="line"></span><br><span class="line"><span class="keyword">private</span>:   </span><br><span class="line">  std::map&lt;<span class="type">int</span>, std::string&gt; values;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><hr><p>C++ 的进化和 C++ 工程师的成长，就是在这样一遍又一遍的摸索和爬坑的过程中完成的。</p><p>本系列的其他文章：</p><ol class="series-items"><li><a href="/posts/9bb75fe1.html" title="Effective Modern C++ 读书笔记：类型推导">Effective Modern C++ 读书笔记：类型推导</a></li><li><a href="/posts/f3206605.html" title="Effective Modern C++ 读书笔记：auto">Effective Modern C++ 读书笔记：auto</a></li><li><a href="/posts/57a898cb.html" title="Effective Modern C++ 读书笔记：转向现代C++">Effective Modern C++ 读书笔记：转向现代C++</a></li><li><a href="/posts/ce1aec80.html" title="Effective Modern C++ 读书笔记：智能指针">Effective Modern C++ 读书笔记：智能指针</a></li><li><a href="/posts/410ab8fb.html" title="Effective Modern C++：右值引用、移动语义和完美转发">Effective Modern C++：右值引用、移动语义和完美转发</a></li><li><a href="/posts/cd605d5c.html" title="Effective Modern C++：lambda 表达式">Effective Modern C++：lambda 表达式</a></li><li><a href="/posts/745d2232.html" title="Effective Modern C++：并发 API">Effective Modern C++：并发 API</a></li><li><a href="/posts/49749d08.html" title="Effective Modern C++：微调">Effective Modern C++：微调</a></li></ol><hr><div class="note info flat"><p>本文同步发布在知乎账号下：<a href="https://zhuanlan.zhihu.com/p/1939768367813727100">https://zhuanlan.zhihu.com/p/1939768367813727100</a></p></div>]]></content>
    
    
      
      
    <summary type="html">&lt;link rel=&quot;stylesheet&quot; class=&quot;aplayer-secondary-style-marker&quot; href=&quot;/assets/css/APlayer.min.css&quot;&gt;&lt;script src=&quot;/assets/js/APlayer.min.js&quot; cla</summary>
      
    
    
    
    <category term="软件开发" scheme="https://p2tree.top/categories/%E8%BD%AF%E4%BB%B6%E5%BC%80%E5%8F%91/"/>
    
    
    <category term="CPP" scheme="https://p2tree.top/tags/CPP/"/>
    
  </entry>
  
  <entry>
    <title>Chapter.316</title>
    <link href="https://p2tree.top/posts/ad7e4660.html"/>
    <id>https://p2tree.top/posts/ad7e4660.html</id>
    <published>2025-08-12T23:08:24.000Z</published>
    <updated>2025-12-23T14:19:41.000Z</updated>
    
    <content type="html"><![CDATA[<link rel="stylesheet" class="aplayer-secondary-style-marker" href="/assets/css/APlayer.min.css"><script src="/assets/js/APlayer.min.js" class="aplayer-secondary-script-marker"></script><p>人的一生可以划分成多个不同的阶段，每个阶段有不同的任务、目标和心境。我试着按每 9 年一个阶段来划分，有这样一个总结（末尾引号中一句话是对这个阶段的赠语）。</p><p><strong>0 到 9 岁</strong>：完全依赖他人生活，好奇心旺盛，学习生存技能，认识世界，满足作为一个人的基本能力。”世界是一本打开的书，每一天都是新的一页“</p><p><strong>9 到 18 岁</strong>：开始形成自我认知，开始学习专业能力，掌握和传承那些前辈留下的知识，渴望独立但又依赖他人。”现在的困惑是未来智慧的种子“</p><p><strong>18 到 27 岁</strong>：自由地规划自己的人生目标，坚实基础，追求真理，探索自己，尝试开始反哺社会，充满希望，也常感焦虑。”勇敢不是不害怕，而是害怕时仍毅然前行“</p><p><strong>27 到 36 岁</strong>：找到同路人一起前行，养育孩子，事业家庭双重责任，理想和现实冲突不断。”比较是偷走幸福的元凶，走出自己的节奏“</p><p><strong>36 到 45 岁</strong>：重新认识世界，理解很多过去的事情，与自己和解，事业小有所成或寻找新的机会，压抑但坚韧。”照顾好别人之前，先照顾好自己“</p><p><strong>45 到 54 岁</strong>：对生活的期待发生变化，子女独立，父母年迈，心态回归家庭，培养更稳定更包容的情绪。”人生可以从不同的地方多次闪光，不要太早放弃“</p><p><strong>54 到 63 岁</strong>：体力下降但心智更成熟，从给社会创造财富的岗位上退出，就像曾经年轻的自己，再次寻找自己人生的新目标。”接受变化，接受新思想，永远保持好奇心“</p><p><strong>63 到 72 岁</strong>：开始抵抗疾病的侵袭，自己过去固有的生活习惯和观念受到冲击，更努力的适应新环境。“乐于改变的人找到了自己新的生活目标”</p><p><strong>72 到 81 岁</strong>：认识的人逐渐离开自己，对人的一生有了新的看法，并尽可能把这些东西留下来，积极面对未来。“时光会带走很多，但不会带有爱和记忆”</p><p><strong>81 到 90 岁</strong>：依赖他人照顾生活，但精神上却高度自由，将自己的经历留给后人，指导晚辈成长。“你人生中写下的每一个标点，都有其意义”</p><p>虽然这只是人生的几个阶段，但如果将它们看作是不同次的生命，我们就会更加珍惜眼前的生活，理解自己。试着去体验不同的人生，扮演不同的角色，经历各具特色的故事。</p><hr><div class="note primary flat"><p>封面图片来自豆包 AI。</p><p>转载自我自己的<a href="https://mp.weixin.qq.com/s/GCSvn0YcAb47_R96ENx2YQ">微信公众号</a>，欢迎关注。</p></div>]]></content>
    
    
    <summary type="html">你的一生，可以是十次生命</summary>
    
    
    
    <category term="生活感悟" scheme="https://p2tree.top/categories/%E7%94%9F%E6%B4%BB%E6%84%9F%E6%82%9F/"/>
    
    
    <category term="人生" scheme="https://p2tree.top/tags/%E4%BA%BA%E7%94%9F/"/>
    
    <category term="哲学" scheme="https://p2tree.top/tags/%E5%93%B2%E5%AD%A6/"/>
    
  </entry>
  
</feed>
