如何编写Claude Code Skills

Claude Code 的 Skills 是一种强大的扩展机制,允许开发者创建可复用的指令集,通过斜杠命令快速调用。本文将详细介绍 Skills 的概念、结构和编写技巧。

1. 什么是 Skill

Skill(技能)是 Claude Code 中的一种扩展机制,它本质上是一组预定义的指令集,可以让 AI 代理执行特定的任务。通过 Skills,你可以:

  • 封装常用的工作流程
  • 标准化代码生成模板
  • 自动化复杂的开发任务
  • 团队共享最佳实践

调用方式:

1
2
3
4
5
# 通过斜杠命令调用
/skill-name

# 带参数调用
/skill-name arg1 arg2

2. Skill 包含哪些部分

一个完整的 Skill 由以下部分组成:

2.1 目录结构

1
2
3
4
5
6
7
8
my-skill/
├── SKILL.md # 必需:指令 + 元数据
├── README.md # 可选:人类可读描述
├── metadata.json # 可选:发布用扩展元数据
├── references/ # 可选:文档、指南、API 参考
├── examples/ # 可选:示例输出
├── scripts/ # 可选:可执行代码
└── assets/ # 可选:模板、图片、数据文件

2.2 核心文件 - SKILL.md

SKILL.md 是 Skill 的核心,包含两部分:

Frontmatter(元数据):

1
2
3
4
---
name: my-skill
description: "What this skill does and when to use it."
---

指令内容: 具体的执行步骤和要求。

2.3 元数据字段说明

字段 类型 必需 说明
name string 1-64字符,小写字母+连字符,必须匹配文件夹名
description string 1-1024字符,描述功能和使用场景
license string 许可证信息
compatibility string 环境要求
metadata object 附加元数据(author, version等)

高级字段(Claude Code 特有):

字段 说明
disable-model-invocation 设为 true 时仅用户可调用,agent 不能自动调用
user-invocable 设为 false 时从菜单隐藏,仅 agent 可加载
allowed-tools 预批准的工具列表,如 Read Grep Glob
context 设为 fork 在子 agent 上下文中运行
agent 子 agent 类型:ExplorePlan

3. 如何创建一个初始 Skill

3.1 基本步骤

  1. 创建文件夹:.claude/skills/ 目录下创建以 skill 名称命名的文件夹
1
mkdir -p .claude/skills/hexo-deploy
  1. 创建 SKILL.md: 编写元数据和指令
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
---
name: hexo-deploy
description: "Deploy Hexo blog to GitHub Pages. Covers build and deploy workflow."
---

# Hexo Deploy Skill

部署 Hexo 博客到 GitHub Pages。

## 步骤

1. 清理旧文件:
```bash
hexo clean
```

2. 生成静态文件:
```bash
hexo generate
```

3. 部署到 GitHub:
```bash
hexo deploy
```

## 注意事项

- 确保 `_config.yml` 中的 deploy 配置正确
- 部署前建议先本地预览
  1. 测试 Skill: 在 Claude Code 中输入 /hexo-deploy 测试

3.2 完整示例

以下是一个代码审查 Skill 的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
---
name: code-review
description: "Review code for best practices and potential issues."
license: MIT
compatibility: Requires git
allowed-tools: Read Grep Glob
metadata:
author: your-name
version: "1.0.0"
---

# Code Review Skill

## 审查内容

1. **代码风格**
- 命名规范
- 代码格式
- 注释质量

2. **潜在问题**
- 安全漏洞
- 性能问题
- 错误处理

3. **最佳实践**
- 设计模式
- 单一职责
- 代码复用

## 输出格式

```markdown
## 审查结果

### 优点
- [列出优点]

### 问题
- [列出问题及建议]

### 建议
- [改进建议]
```

4. 怎样写出一个好的 Skill

4.1 Description 编写技巧

公式:

1
[产品] [核心功能]. Covers [关键主题]. Keywords: [关键词].

好的示例:

1
description: "Hexo blog deployment. Covers build, deploy, and preview. Keywords: Hexo, GitHub Pages, deployment."

差的示例:

1
2
description: "Helps with deployment."  # 太模糊
description: "A powerful deployment tool." # 营销语言

规则:

  • 目标:80-150 字符
  • 包含触发关键词
  • 避免营销词汇(”powerful”, “comprehensive”)
  • 避免填充词(”this skill”, “use this for”)

4.2 保持 SKILL.md 简洁

关键规则: SKILL.md 不要超过 500 行。

解决方案:使用 Router 模式

对于复杂的 Skill,将详细内容放在 references/ 目录:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
---
name: complex-skill
description: "Complex skill with multiple topics."
---

# Complex Skill

本文件是路由文件,请根据需要阅读对应文档。

## 入门

- 新手?阅读:`references/getting-started.md`
- 快速设置?阅读:`references/quickstart.md`

## 按场景选择

### 功能 A
- 详细说明:`references/feature-a.md`

### 功能 B
- 详细说明:`references/feature-b.md`

4.3 使用变量和动态注入

变量替换:

1
2
3
4
5
6
7
8
9
10
---
name: pr-review
description: "Review pull request."
---

# PR Review

审查 PR: $ARGUMENTS

请分析代码变更并提供反馈。

动态上下文注入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
---
name: pr-review
description: "Review pull request with diff context."
---

# PR Review

## PR 上下文

- PR diff: !`gh pr diff`
- 变更文件: !`gh pr diff --name-only`

## 任务

审查这个 PR 的代码变更...

4.4 工具限制

合理限制 Skill 可使用的工具,提高安全性:

1
2
3
4
5
6
7
8
# 只读分析
allowed-tools: Read Grep Glob

# Git 操作
allowed-tools: Bash(git:*)

# 特定命令
allowed-tools: Bash(npm:*) Bash(pnpm:*)

4.5 调用控制

根据使用场景设置调用权限:

场景 配置
用户和 agent 都可调用 默认,无需配置
仅用户手动调用 disable-model-invocation: true
仅 agent 后台调用 user-invocable: false

4.6 最佳实践清单

  • Name 使用小写字母和连字符,匹配文件夹名
  • Description 包含关键词,80-150 字符
  • SKILL.md 控制在 500 行以内
  • 复杂内容放入 references/ 目录
  • 合理使用变量和动态注入
  • 设置适当的工具限制
  • 添加示例和说明文档
  • 测试 Skill 的各种调用方式

5. 实战案例

案例 1:Hexo 文章发布 Skill

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
---
name: hexo-post
description: "Create and publish Hexo blog post. Covers frontmatter, tags, categories."
argument-hint: [title]
allowed-tools: Read Write Bash(hexo:*)
metadata:
author: your-name
version: "1.0.0"
---

# Hexo Post Skill

创建新的 Hexo 博客文章。

## 参数

- `$ARGUMENTS`: 文章标题

## 步骤

1. 创建新文章:
```bash
hexo new "$ARGUMENTS"
```

2. 编辑 frontmatter:
```yaml
---
title: $ARGUMENTS
date: YYYY-MM-DD HH:MM:SS
tags: [tag1, tag2]
categories: [category]
---
```

3. 添加内容...

## 注意事项

- 标签使用小写
- 分类保持简洁
- 日期格式:YYYY-MM-DD HH:MM:SS

案例 2:代码重构 Skill

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
---
name: refactor-code
description: "Refactor code following best practices. Covers naming, structure, patterns."
allowed-tools: Read Write Edit Grep Glob
context: fork
agent: Explore
---

# Code Refactoring Skill

重构代码,遵循最佳实践。

## 重构内容

1. **命名优化**
- 变量名清晰表达意图
- 函数名体现功能
- 类名使用名词

2. **结构调整**
- 函数职责单一
- 减少嵌套层级
- 提取重复代码

3. **设计模式**
- 识别可应用的模式
- 保持简单优先

## 输出

```markdown
## 重构结果

### 变更列表
- [文件]: [变更说明]

### 改进点
- [改进说明]
```

总结

编写高质量的 Claude Code Skill 需要注意:

  1. 清晰的命名和描述 - 让用户和 agent 快速理解用途
  2. 合理的结构 - 核心指令简洁,详细内容分离
  3. 安全的工具限制 - 只授予必要的权限
  4. 良好的文档 - 示例和说明帮助理解
  5. 持续优化 - 根据使用反馈改进

通过遵循这些原则,你可以创建出强大、易用、安全的 Skills,提升开发效率。

参考资料

OpenCode 是一个开源的 AI 编程助手,支持在终端、IDE 或桌面环境中使用。它拥有 60K+ GitHub stars,每月有超过 650,000 名开发者使用。本文将详细介绍如何快速搭建 OpenCode 开发环境。

1. 官方地址与 GitHub 仓库

官方网站https://opencode.ai/

GitHub 仓库

文档地址https://opencode.ai/docs/

OpenCode 支持 75+ LLM 提供商,包括 Claude、GPT、Gemini 等主流模型,同时也支持本地模型运行。它具有以下核心特性:

  • LSP 支持:自动为 LLM 加载合适的语言服务器
  • 多会话:可在同一项目中并行启动多个代理
  • 分享链接:可分享任何会话链接用于参考或调试
  • GitHub Copilot 集成:登录 GitHub 即可使用 Copilot 账户
  • ChatGPT Plus/Pro 集成:登录 OpenAI 即可使用 ChatGPT 账户
  • 任意编辑器:支持终端界面、桌面应用和 IDE 扩展

2. 安装方式

OpenCode 提供多种安装方式,可根据你的操作系统和包管理器选择合适的方法。

通用安装脚本(推荐)

1
curl -fsSL https://opencode.ai/install | bash

各平台包管理器安装

macOS(Homebrew)

1
brew install opencode

Node.js(npm)

1
npm install -g opencode

Bun

1
bun install -g opencode

Arch Linux(paru)

1
paru -S opencode

桌面应用

OpenCode 还提供桌面应用,支持 macOS、Windows 和 Linux,可访问 https://opencode.ai/download 下载。

3. 集成 Oh-My-OpenCode 插件

Oh-My-OpenCode 是一个强大的插件,为 OpenCode 提供了丰富的功能扩展,包括 20+ 内置工作流自动化钩子、专业化的 AI 代理、Model Context Protocol 集成等。

GitHub 仓库https://github.com/code-yeongyu/oh-my-opencode

官方网站https://ohmyopencode.com/

安装 Oh-My-OpenCode

推荐方式(使用 bunx)

1
bunx oh-my-opencode install

备选方式(npm)

1
npm install -g oh-my-opencode

备选方式(bun)

1
bun install -g oh-my-opencode

备选方式(yarn)

1
yarn global add oh-my-opencode

Oh-My-OpenCode 核心功能

  • Agents 配置:配置智能代理及其设置
  • Hooks 配置:启用或禁用工作流自动化钩子(20+ 内置钩子)
  • MCPs 配置:Model Context Protocol 集成
  • LSP 配置:添加和配置语言服务器协议
  • 实验性功能:配置前沿的实验性选项

配置文件位置

Oh-My-OpenCode 的配置文件位于以下位置(按优先级排序):

  1. .opencode/oh-my-opencode.json - 项目级配置(最高优先级)
  2. ~/.config/opencode/oh-my-opencode.json - 用户级配置

项目级配置会覆盖用户级配置,允许你在不同项目中使用不同设置。

4. 添加与配置 API Key

配置好 OpenCode 后,需要添加 LLM 提供商的 API Key 才能正常使用。

添加 API Key

使用 OpenCode 的 /connect 命令添加 API Key:

1
2
3
4
5
# 启动 OpenCode
opencode

# 在 OpenCode 中输入
/connect

按照提示选择 LLM 提供商并输入 API Key。添加的凭据会存储在 ~/.local/share/opencode/auth.json 文件中。

配置 LLM 提供商

在 OpenCode 配置文件(opencode.jsonopencode.jsonc)中配置提供商:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"$schema": "https://opencode.ai/config.json",
"theme": "opencode",
"model": "anthropic/claude-sonnet-4-5",
"autoupdate": true,
"providers": {
"anthropic": {
"apiKey": "your-api-key"
},
"openai": {
"apiKey": "your-api-key"
}
}
}

配置文件位置

OpenCode 的配置文件可在以下位置放置,按优先级合并:

  1. 项目目录下的 opencode.jsonopencode.jsonc
  2. ~/.config/opencode/opencode.json
  3. 其他系统级配置文件

配置文件会被合并而非替换,后续配置只会覆盖冲突的键值。

常用模型配置示例

Anthropic Claude

1
2
3
{
"model": "anthropic/claude-sonnet-4-5"
}

OpenAI GPT-4

1
2
3
{
"model": "openai/gpt-4"
}

Google Gemini

1
2
3
{
"model": "google/gemini-pro"
}

自定义 Base URL

如需使用代理服务或自定义端点,可配置 Base URL:

1
2
3
4
5
6
7
{
"providers": {
"anthropic": {
"baseURL": "https://your-proxy-service.com/v1"
}
}
}

总结

通过以上四个步骤,你就可以快速搭建 OpenCode 开发环境了:

  1. 访问 https://opencode.ai/ 了解官方信息
  2. 选择合适的安装方式完成 OpenCode 安装
  3. 使用 bunx oh-my-opencode install 集成 Oh-My-OpenCode 插件
  4. 通过 /connect 命令添加 API Key 并在配置文件中设置模型

搭建完成后,你就可以开始使用这个强大的 AI 编程助手来提升开发效率了!

参考资源

一、variant是什么

std::variant是C++17标准库引入的一种类型安全的联合体(union)实现,它位于头文件<variant>中。variant可以存储其模板参数列表中任意一种类型的值,类似于C语言中的union,但具有以下关键区别:variant会跟踪当前存储的值的类型,并且在使用时需要进行类型检查,这使得variant成为类型安全的异构数据容器。

传统的C语言union虽然可以存储不同类型的数据,但它存在严重的类型安全问题。程序员必须手动跟踪当前union中存储的是哪种类型,并在访问时进行正确的类型转换。如果类型不匹配或忘记进行类型检查,就会导致未定义行为。此外,union不能包含非平凡类型(non-trivial types),如包含析构函数的类型,这大大限制了它的使用范围。

std::variant解决了这些问题。variant可以包含任何可拷贝构造的类型,包括具有析构函数的类型。当variant的值被替换或variant本身被销毁时,会自动调用相应类型的析构函数。variant还提供了std::visit函数和std::variant_alternative等辅助工具,使得类型安全的访问成为可能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <variant>
#include <string>
#include <iostream>

// 定义一个可以存储int、double或std::string的variant
using MyVariant = std::variant<int, double, std::string>;

// 简单的variant使用示例
void basicExample() {
MyVariant v1 = 42; // 存储int
MyVariant v2 = 3.14; // 存储double
MyVariant v3 = "hello"; // 存储std::string (通过char数组构造)

std::cout << std::get<int>(v1) << std::endl; // 输出: 42
std::cout << std::get<double>(v2) << std::endl; // 输出: 3.14
std::cout << std::get<std::string>(v3) << std::endl; // 输出: hello
}

variant的核心设计理念是提供一种”类型安全的联合体”。每个variant实例在任何时候都恰好持有其类型列表中的一种类型的值(或者处于空状态,如果使用std::monostate作为第一个类型)。variant的大小是其最大类型的对齐要求加上一点开销,用于存储当前持有类型的索引。

variant使用std::visit进行访问,这要求所有被访问的类型都能处理相同的操作。这种设计模式被称为”双分派”(double dispatch),它允许我们根据variant中存储的实际类型来执行相应的操作。与传统的类型转换和switch语句相比,这种方式更加类型安全,也更容易维护。

二、variant有什么作用

std::variant在现代C++编程中扮演着重要的角色,它提供了一种优雅的解决方案来处理需要存储多种可能类型的数据场景。理解variant的作用对于编写健壮、可维护的C++代码至关重要。

第一个核心作用是实现类型安全的异构存储。与传统的union相比,variant在编译期和运行时都提供了类型安全保障。在编译期,variant会确保只有有效的类型才能被存储;在运行时,通过std::visitstd::get_if可以安全地访问variant中的值,如果类型不匹配会抛出异常或返回空指针,而不会导致未定义行为。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <variant>
#include <iostream>

void typeSafetyDemo() {
std::variant<int, double, std::string> v = 100;

// 正确的访问方式
std::cout << "Value: " << std::get<int>(v) << std::endl;

// 尝试获取错误的类型会抛出异常
try {
std::cout << std::get<double>(v) << std::endl;
} catch (const std::bad_variant_access& e) {
std::cout << "Exception: " << e.what() << std::endl;
}
}

第二个核心作用是消除原始指针和void*的使用。在C++11之前,实现类似功能通常需要使用void*配合类型信息存储,或者使用指向基类的指针和虚函数实现多态。这些方法都有各自的缺点:void*不安全且丢失了类型信息;虚函数需要预先定义类层次结构,不够灵活。variant提供了一种类型安全、零运行时开销的替代方案。

1
2
3
4
5
6
7
8
9
// 使用variant替代void*和类型标签
struct Message {
std::variant<int, std::string, double> payload;
int typeTag; // 不再需要手动维护类型标签
};

// 使用variant后的改进版本
using MessageV2 = std::variant<int, std::string, double>;
// 类型信息由variant自动维护,无需额外的typeTag

第三个核心作用是简化状态机的实现。状态机中的每个状态可能有不同的数据需要维护,使用variant可以自然地表达这种”每个状态一种数据结构”的需求。variant的std::visit机制非常适合实现状态转换逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
enum class State { Idle, Processing, Completed, Error };

// 使用variant表示每种状态的数据
struct ProcessingData {
int progress;
};

struct ErrorData {
std::string message;
};

using StateData = std::monostate; // Idle状态不需要额外数据
using StateData = std::variant<
std::monostate, // Idle
ProcessingData, // Processing
std::monostate, // Completed (不需要额外数据)
ErrorData // Error
>;

第四个核心作用是实现编译期多态。与运行时的虚函数多态不同,variant与std::visit结合可以实现编译期的分派,这通常带来更好的性能,因为虚函数调用的开销在编译期就被消除了。此外,编译期多态不需要类继承层次结构,使得代码更加扁平化和模块化。

第五个核心作用是作为std::expected等现代C++模式的基础。C++23引入的std::expected以及一些库中实现的Result类型,都可以使用variant作为其内部实现。这证明了variant作为一种基础构建块的通用性和重要性。

三、variant怎么使用

variant的使用涉及多个方面,包括基本操作、访问方式、值修改、状态查询等。下面将详细介绍这些使用技巧,这些内容是本文的重点部分。

3.1 variant的基本操作

创建variant非常简单,只需要指定它可以存储的类型列表,然后通过构造函数或赋值操作来初始化它。variant会自动使用列表中的第一个类型进行默认初始化(如果该类型支持默认构造),或者你可以通过其他类型进行初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <variant>
#include <string>
#include <iostream>

void basicOperations() {
// 创建variant,使用第一个类型的默认值初始化
std::variant<int, double, std::string> v1; // 值为0

// 使用其他类型的值初始化
std::variant<int, double, std::string> v2 = 42;
std::variant<int, double, std::string> v3 = 3.14;
std::variant<int, double, std::string> v4 = std::string("hello");

// 使用emplace避免不必要的拷贝
std::variant<int, double, std::string> v5;
v5.emplace<std::string>("world"); // 直接构造,避免临时对象

// 使用std::in_place_type指定类型
std::variant<int, double, std::string> v6{std::in_place_type<double>, 2.718};

// 使用std::in_place_index指定索引
std::variant<int, double, std::string> v7{std::in_place_index<2>, "variant"};
}

variant的赋值操作遵循”析构旧值,构造新值”的语义。如果新值的类型与当前存储的类型相同,则会调用该类型的拷贝赋值或移动赋值运算符;如果类型不同,则会先析构当前值,然后构造新值。

1
2
3
4
5
6
7
8
9
10
11
12
void assignmentDemo() {
std::variant<int, std::string> v = 10;

// 赋值相同类型
v = 20; // 调用int的赋值运算符

// 赋值不同类型
v = std::string("hello"); // 析构int,构造string

// 使用emplace进行原地构造
v.emplace<int>(100); // 析构string,构造int
}

3.2 访问variant中的值

访问variant中的值有多种方式,每种方式都有其适用场景。主要的访问方式包括std::getstd::get_if以及std::visit

std::get是最直接的访问方式,它要求调用者明确知道要获取哪种类型。如果variant当前存储的不是请求的类型,会抛出std::bad_variant_access异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <variant>
#include <iostream>
#include <string>

void getDemo() {
std::variant<int, double, std::string> v = "test";

// 使用类型访问
std::cout << std::get<std::string>(v) << std::endl;

// 使用索引访问
std::cout << std::get<2>(v) << std::endl;

// 如果类型不匹配,会抛出异常
try {
std::cout << std::get<int>(v) << std::endl;
} catch (const std::bad_variant_access& e) {
std::cerr << "Type mismatch: " << e.what() << std::endl;
}
}

std::get_if返回指向值的指针(如果类型匹配)或空指针(如果类型不匹配),这种方式更安全,适合在不确定variant当前类型的情况下进行访问。

1
2
3
4
5
6
7
8
9
10
11
12
void getIfDemo() {
std::variant<int, double, std::string> v = 42;

// 安全的类型检查和访问
if (auto* intPtr = std::get_if<int>(&v)) {
std::cout << "Got int: " << *intPtr << std::endl;
} else if (auto* doublePtr = std::get_if<double>(&v)) {
std::cout << "Got double: " << *doublePtr << std::endl;
} else if (auto* stringPtr = std::get_if<std::string>(&v)) {
std::cout << "Got string: " << *stringPtr << std::endl;
}
}

std::visit是访问variant最强大的方式,它允许我们对variant中存储的值应用一个可调用对象(函数、lambda等)。visitor必须能够处理variant中的所有可能类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <variant>
#include <iostream>
#include <string>
#include <functional>

void visitDemo() {
std::variant<int, double, std::string> v = "hello";

// 使用lambda作为visitor
std::visit([](auto&& arg) {
std::cout << "Value: " << arg << std::endl;
}, v);

// 多个variant一起访问
std::variant<int, double> v1 = 10;
std::variant<int, double> v2 = 20;

std::visit([](auto a, auto b) {
std::cout << "Sum: " << a + b << std::endl;
}, v1, v2);
}

3.3 在visitor中使用类型信息

有的时候,visitor需要根据当前值的具体类型来执行不同的操作。C++17提供了多种方式来获取这些类型信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <variant>
#include <iostream>
#include <type_traits>

void typeBasedVisitor() {
std::variant<int, double, std::string> v;

std::visit([](auto&& arg) {
using T = std::decay_t<decltype(arg)>;

if constexpr (std::is_same_v<T, int>) {
std::cout << "Processing int: " << arg << std::endl;
} else if constexpr (std::is_same_v<T, double>) {
std::cout << "Processing double: " << arg << std::endl;
} else if constexpr (std::is_same_v<T, std::string>) {
std::cout << "Processing string: " << arg << std::endl;
}
}, v);
}

std::visit返回visitor的返回值,这使得我们可以根据variant中的值计算结果:

1
2
3
auto result = std::visit([](auto&& arg) -> double {
return static_cast<double>(arg);
}, v);

3.4 variant的状态查询

variant提供了多个成员函数来查询其当前状态,这些函数对于调试和日志记录非常有用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <variant>
#include <iostream>

void stateQueries() {
std::variant<int, double, std::string> v = 42;

// 检查variant当前持有的是哪种类型
std::cout << "Index: " << v.index() << std::endl; // 输出: 0 (int的索引)

// 检查variant是否持有特定类型的值
std::cout << "holds_alternative<int>: " << std::holds_alternative<int>(v) << std::endl; // true
std::cout << "holds_alternative<double>: " << std::holds_alternative<double>(v) << std::endl; // false

// valueless_by_exception: variant是否处于有效状态
// 如果variant中的操作抛出异常,variant可能处于valueless状态
}

3.5 处理异常情况

当variant的赋值操作或其他修改操作抛出异常时,variant会进入一种特殊的状态。理解这种行为对于编写健壮的代码很重要。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <variant>
#include <iostream>
#include <stdexcept>

struct ThrowingType {
int value;
ThrowingType(int v) : value(v) {}
ThrowingType(const ThrowingType&) { throw std::runtime_error("copy error"); }
ThrowingType(ThrowingType&&) noexcept { throw std::runtime_error("move error"); }
};

void exceptionHandling() {
std::variant<int, ThrowingType> v = 10;

try {
v = ThrowingType(20); // 如果拷贝构造抛出异常
} catch (const std::exception& e) {
if (v.valueless_by_exception) {
std::cout << "Variant is now valueless" << std::endl;
}
// variant的状态取决于异常发生的时机
}
}

3.6 使用std::monostate处理”空”状态

有些情况下,我们可能需要一个”空”或”无效”状态来表示variant当前没有有效的值。C++17使用std::monostate类型来实现这个目的。std::monostate是一个空的、结构体类型,它本身不携带任何有意义的数据,只是作为一个占位符存在。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <variant>
#include <iostream>

void monostateDemo() {
// 使用std::monostate作为第一个类型来提供"空"状态
using OptionalInt = std::variant<std::monostate, int, double>;

OptionalInt v1; // 默认构造为空状态
OptionalInt v2 = std::monostate{}; // 显式设置为空状态

// 检查是否是空状态
if (std::holds_alternative<std::monostate>(v1)) {
std::cout << "v1 is empty" << std::endl;
}

// 在visit中处理空状态
std::visit([](auto&& arg) {
using T = std::decay_t<decltype(arg)>;
if constexpr (std::is_same_v<T, std::monostate>) {
std::cout << "Empty value" << std::endl;
} else {
std::cout << "Value: " << arg << std::endl;
}
}, v1);
}

四、variant有哪些使用场景

variant在实际编程中有广泛的应用场景,从简单的值替换到复杂的状态机实现,variant都提供了一种类型安全、表达力强的解决方案。理解这些使用场景有助于在实际项目中做出正确的设计决策。

第一个常见场景是实现可选值(Optional Values)。在C++17之前,我们通常使用指针或特殊值(如nullptr-1)来表示可选值。使用std::monostate作为第一个类型的variant可以更清晰地表达”可能没有值”的语义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
template<typename T>
using Optional = std::variant<std::monostate, T>;

Optional<std::string> findName(int id) {
if (id > 0 && id <= 100) {
return std::string("Name") + std::to_string(id);
}
return std::monostate{}; // 返回空状态
}

void useOptional() {
auto result = findName(50);
std::visit([](auto&& arg) {
if constexpr (std::is_same_v<std::decay_t<decltype(arg)>, std::monostate>) {
std::cout << "Not found" << std::endl;
} else {
std::cout << "Found: " << arg << std::endl;
}
}, result);
}

第二个常见场景是实现解析器和数据转换。在解析JSON、XML或其他格式的数据时,解析结果可能有多种类型(数字、字符串、布尔值、嵌套对象等),variant是表示这种异构数据的理想选择。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <variant>
#include <string>
#include <vector>
#include <unordered_map>

// JSON值的简单表示
using JsonValue = std::variant<
std::nullptr_t, // null
bool, // true/false
double, // number
std::string, // string
std::vector<JsonValue>, // array
std::unordered_map<std::string, JsonValue> // object
>;

// 构建JSON对象
JsonValue buildJson() {
std::unordered_map<std::string, JsonValue> object;
object["name"] = std::string("Alice");
object["age"] = 30;
object["active"] = true;

std::vector<JsonValue> skills = {
std::string("C++"),
std::string("Python")
};
object["skills"] = skills;

return object;
}

第三个常见场景是实现命令模式(Command Pattern)。不同的命令可能有不同的参数类型,使用variant可以统一表示不同类型的命令对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <variant>
#include <string>
#include <iostream>

// 定义不同类型的命令
struct CreateUser {
std::string name;
std::string email;
};

struct DeleteUser {
int userId;
};

struct UpdateUser {
int userId;
std::string field;
std::string newValue;
};

// 使用variant统一表示所有命令
using Command = std::variant<CreateUser, DeleteUser, UpdateUser>;

// 命令处理函数
void executeCommand(const Command& cmd) {
std::visit([](auto&& c) {
using T = std::decay_t<decltype(c)>;
if constexpr (std::is_same_v<T, CreateUser>) {
std::cout << "Creating user: " << c.name << std::endl;
} else if constexpr (std::is_same_v<T, DeleteUser>) {
std::cout << "Deleting user: " << c.userId << std::endl;
} else if constexpr (std::is_same_v<T, UpdateUser>) {
std::cout << "Updating user " << c.userId
<< ": " << c.field << " = " << c.newValue << std::endl;
}
}, cmd);
}

第四个常见场景是实现状态机。状态机中的每种状态通常有不同的关联数据,使用variant可以自然地表达这种关系。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#include <variant>
#include <string>
#include <iostream>

// 定义状态数据
struct IdleState {
// 空闲状态不需要额外数据
};

struct ConnectingState {
std::string server;
int port;
};

struct ConnectedState {
int connectionId;
double uptime;
};

struct ErrorState {
std::string message;
int errorCode;
};

// 使用variant表示所有可能的状态
using NetworkState = std::variant<
IdleState,
ConnectingState,
ConnectedState,
ErrorState
>;

// 状态机类
class NetworkConnection {
NetworkState state_;

public:
void connect(const std::string& server, int port) {
state_ = ConnectingState{server, port};
}

void disconnect() {
state_ = IdleState{};
}

void process() {
std::visit([](auto&& s) {
using T = std::decay_t<decltype(s)>;
if constexpr (std::is_same_v<T, IdleState>) {
std::cout << "Waiting for connection..." << std::endl;
} else if constexpr (std::is_same_v<T, ConnectingState>) {
std::cout << "Connecting to " << s.server << ":" << s.port << std::endl;
} else if constexpr (std::is_same_v<T, ConnectedState>) {
std::cout << "Connected (ID: " << s.connectionId << ")" << std::endl;
} else if constexpr (std::is_same_v<T, ErrorState>) {
std::cout << "Error: " << s.message << " (code: " << s.errorCode << ")" << std::endl;
}
}, state_);
}
};

第五个常见场景是实现事件处理系统。不同类型的事件可能有不同的负载数据,使用variant可以统一处理所有类型的事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include <variant>
#include <string>
#include <chrono>

// 事件类型定义
struct MouseClick {
int x, y;
int button;
};

struct KeyPress {
char key;
bool shift;
};

struct WindowResize {
int width, height;
};

struct TimerElapsed {
std::chrono::steady_clock::time_point timestamp;
};

// 统一的事件类型
using Event = std::variant<MouseClick, KeyPress, WindowResize, TimerElapsed>;

// 事件处理器
class EventHandler {
public:
void handleEvent(const Event& event) {
std::visit([this](auto&& e) {
processEvent(e);
}, event);
}

private:
void processEvent(const MouseClick& click) {
std::cout << "Mouse click at (" << click.x << ", " << click.y
<< ") button: " << click.button << std::endl;
}

void processEvent(const KeyPress& key) {
std::cout << "Key pressed: '" << key.key << "' (shift: " << key.shift << ")" << std::endl;
}

void processEvent(const WindowResize& resize) {
std::cout << "Window resized to " << resize.width << "x" << resize.height << std::endl;
}

void processEvent(const TimerElapsed& timer) {
std::cout << "Timer elapsed at timestamp" << std::endl;
}
};

第六个常见场景是实现函数的多返回类型。某些函数可能有不同类型的返回结果,使用variant可以优雅地处理这种情况,而不需要使用输出参数或异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <variant>
#include <string>
#include <iostream>

// 计算结果可能是数值或错误信息
using CalcResult = std::variant<double, std::string>;

CalcResult divide(double a, double b) {
if (b == 0) {
return std::string("Error: Division by zero");
}
return a / b;
}

void printResult(const CalcResult& result) {
std::visit([](auto&& arg) {
if constexpr (std::is_same_v<std::decay_t<decltype(arg)>, std::string>) {
std::cerr << arg << std::endl;
} else {
std::cout << "Result: " << arg << std::endl;
}
}, result);
}

五、variant与tuple的异同点

std::variantstd::tuple都是C++标准库中的异构容器,但它们有本质的区别。理解这些区别对于在实际编程中选择正确的工具至关重要。

5.1 相同点

首先,让我们看看variant和tuple的相同点:

两者都是异构容器,可以存储不同类型的数据。tuple可以存储任意数量的不同类型,variant可以存储任意一种类型(从给定的类型列表中选择)。

两者都支持编译期类型操作。C++标准库为两者都提供了类型访问的工具:std::tuple_elementstd::variant_alternative分别用于获取元素/候选类型。

两者都支持std::visit进行访问。对于tuple,std::visit会访问所有元素;对于variant,std::visit会根据当前存储的类型只访问一个元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <variant>
#include <tuple>
#include <iostream>

void commonFeatures() {
// 两者都支持异构类型
std::tuple<int, double, std::string> t{1, 2.0, "three"};
std::variant<int, double, std::string> v{1};

// 两者都支持std::visit
std::visit([](auto&& arg) {
std::cout << "Visit result: " << arg << std::endl;
}, v);

std::visit([](auto&&... args) {
((std::cout << args << " "), ...);
std::cout << std::endl;
}, t);
}

5.2 不同点

虽然variant和tuple都是异构容器,但它们在语义和使用场景上有根本的区别:

存储语义不同:tuple同时持有所有类型的值(以成员变量的形式),而variant只持有一种类型的值。这是两者最根本的区别。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <variant>
#include <tuple>
#include <iostream>

void storageDifference() {
// tuple同时持有int、double、string三个值
std::tuple<int, double, std::string> t{1, 2.0, "hello"};
std::cout << "Tuple size (incomplete): " << sizeof(t) << " bytes" << std::endl;
// 输出: Tuple包含int、double、string三个对象

// variant只持有一种类型的值
std::variant<int, double, std::string> v{1};
std::cout << "Variant size: " << sizeof(v) << " bytes" << std::endl;
// 输出: Variant只包含一个值加上类型索引
}

访问方式不同:tuple的访问是位置固定的(通过std::get<N>),而variant的访问是类型或索引的(通过std::get<T>std::get<N>),并且类型必须匹配当前存储的类型。

1
2
3
4
5
6
7
8
9
10
11
12
void accessDifference() {
std::tuple<int, std::string> t{42, "hello"};
std::variant<int, std::string> v{42};

// tuple: 按位置访问
std::cout << std::get<0>(t) << std::endl; // 总是42
std::cout << std::get<1>(t) << std::endl; // 总是"hello"

// variant: 按类型访问(必须与当前存储的类型匹配)
std::cout << std::get<int>(v) << std::endl; // 如果当前是int,正确
// std::get<std::string>(v) 如果当前是int,会抛出异常
}

大小和内存布局不同:tuple的大小是其所有元素大小的总和(加上可能的填充),而variant的大小是其最大元素的大小加上一些开销(用于存储类型索引)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <tuple>
#include <variant>
#include <iostream>

struct LargeStruct {
char data[1000];
};

void sizeComparison() {
std::tuple<int, LargeStruct> t;
std::variant<int, LargeStruct> v;

std::cout << "Tuple size: " << sizeof(t) << " bytes" << std::endl; // ~1004 bytes
std::cout << "Variant size: " << sizeof(v) << " bytes" << std::endl; // ~1008 bytes (加上了索引)
}

默认构造行为不同:tuple在默认构造时会默认构造所有元素,而variant只默认构造其第一个类型(如果第一个类型可默认构造的话)。

1
2
3
4
5
6
void defaultConstruction() {
std::tuple<int, std::string> t; // int=0, string=""

std::variant<int, std::string> v; // int=0
// 如果第一个类型是std::string,variant默认构造string
}

适用场景不同:tuple适用于”一组相关的值”,如函数的多个返回值、数据的聚合作”组合”;variant适用于”一个值,可能是多种类型之一”,如解析结果、状态数据。

5.3 选择指南

在选择variant还是tuple时,考虑以下问题:

如果需要同时存储多个不同类型的值,选择tuple。

如果需要从多个类型中选择一个存储,选择variant。

如果需要固定大小的数据集合,选择tuple。

如果需要运行时决定使用哪种类型,选择variant。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 使用tuple的场景:函数返回多个值
std::tuple<int, double, std::string> getStats() {
return {100, 98.5, "Alice"};
}

// 使用variant的场景:函数返回可能失败,结果类型不固定
std::variant<int, std::string, double> processInput(const std::string& input) {
if (input.empty()) {
return std::string("Error: empty input");
}
if (input == "yes") {
return 1;
}
return 3.14;
}

六、如何避免滥用variant

虽然std::variant是一个强大的工具,但不当使用会导致代码复杂度增加、维护困难甚至运行时错误。理解常见的滥用模式以及如何避免它们,对于编写高质量的C++代码至关重要。

第一个常见的滥用是过度使用variant作为”万能类型”。variant应该用于真正需要表示”多种可能类型”的情况,而不是为了避免设计决策。如果一个variant包含太多不相关的类型,可能意味着应该重新审视设计。

1
2
3
4
5
6
7
8
9
10
11
// 不好的设计:variant类型过多且不相关
using BadDesign = std::variant<
int, double, std::string, std::vector<int>,
std::map<std::string, int>, CustomClass,
std::function<void()>, std::thread
>;

// 更好的设计:识别真正需要处理的类型集合
// 如果不同类型的处理逻辑完全不同,可能需要不同的设计
using Value = std::variant<int, double, std::string>; // 数值或字符串
using Action = std::variant<std::function<void()>, std::thread>; // 可执行对象

第二个常见的滥用是在不需要运行时类型区分的场景下使用variant。如果所有可能的类型都需要相同的处理逻辑,使用variant可能过于复杂。在这种情况下,模板可能是更好的选择。

1
2
3
4
5
6
7
// 不必要的使用variant
VariantProcesser v = 42;
std::visit([](auto&& x) { process(x); }, v); // process对所有类型做同样的事

// 更简单的方案:模板函数
template<typename T>
void process(T&& value) { /* 对所有类型做同样的事 */ }

第三个常见的滥用是忽略variant的异常安全性。当variant的赋值操作抛出异常时,variant可能进入valueless_by_exception状态。忽略这种状态会导致后续访问出现问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <variant>
#include <iostream>
#include <stdexcept>

struct MayThrow {
int value;
MayThrow(int v) : value(v) {}
MayThrow(const MayThrow& other) : value(other.value) {
if (value < 0) throw std::runtime_error("Negative value");
}
};

void safeVariantUsage() {
std::variant<int, MayThrow> v = 10;

try {
v = MayThrow(20); // 可能抛出异常
} catch (...) {
// 必须检查variant是否处于有效状态
if (v.valueless_by_exception) {
std::cerr << "Variant is valueless due to exception" << std::endl;
return;
}
}
// 继续处理...
}

第四个常见的滥用是频繁改变variant的类型。variant的类型改变涉及析构和构造操作,频繁的类型变化会带来性能开销。如果需要频繁改变类型,可能需要考虑其他设计。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 性能敏感的代码中,频繁类型变化可能不是最佳选择
void performanceConcern() {
std::variant<int, std::string, double> v;

// 循环中频繁改变类型
for (int i = 0; i < 1000000; ++i) {
if (i % 3 == 0) {
v = i;
} else if (i % 3 == 1) {
v = std::to_string(i);
} else {
v = static_cast<double>(i) / 1000.0;
}
}
}

第五个常见的滥用是忘记处理所有可能的类型。在编写visitor时,如果遗漏了某些类型的处理,编译器会报错(这是好的),但如果使用std::getstd::get_if,遗漏的情况可能导致运行时错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void completeHandling() {
std::variant<int, double, std::string> v;

// 不完整的处理会导致编译错误(好的情况)
std::visit([](auto&& arg) {
using T = std::decay_t<decltype(arg)>;
if constexpr (std::is_same_v<T, int>) {
// 处理int
}
// 如果缺少double和string的处理,编译器会警告或报错
}, v);

// 使用get_if时,需要显式处理所有情况
if (std::get_if<int>(&v)) {
// 处理int
} else if (std::get_if<double>(&v)) {
// 处理double
} else if (std::get_if<std::string>(&v)) {
// 处理string
}
// 如果忘记某个分支,silent bug
}

第六个常见的滥用是将variant与原始类型转换混用。虽然variant提供了类型转换的便利,但过度依赖隐式转换可能导致意外行为。

1
2
3
4
5
6
7
8
9
10
11
void implicitConversion() {
std::variant<int, double> v = 42;

// 访问时可能发生隐式转换
double d = std::get<double>(v); // int被提升为double?不,这里会抛出异常

// 正确的做法是明确知道当前类型
if (auto* pInt = std::get_if<int>(&v)) {
double d = static_cast<double>(*pInt); // 显式转换
}
}

为了避免滥用variant,遵循以下原则:

保持variant类型列表的简洁和内聚。每个variant应该代表一个清晰的概念。

在编写visitor时,使用if constexpr确保所有类型都被处理。

注意异常安全性,特别是在处理可能抛出异常的赋值操作时。

考虑性能影响,特别是在高频调用的代码路径中。

将variant作为明确的设计选择,而不是避免设计决策的权宜之计。

七、相关标准库

C++17标准库为std::variant提供了丰富的配套设施,包括类型特性、辅助函数和算法。了解这些相关组件对于充分发挥variant的潜力至关重要。

7.1 类型特性

标准库提供了多个与variant相关的类型特性,用于在编译期查询variant的类型信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <variant>
#include <type_traits>

void typeTraitsDemo() {
using MyVariant = std::variant<int, double, std::string>;

// 获取variant的大小(类型列表中的类型数量)
std::cout << "variant_size: " << std::variant_size<MyVariant>::value << std::endl; // 3

// 获取指定位置的类型
using FirstType = std::variant_alternative<0, MyVariant>::type; // int
using SecondType = std::variant_alternative<1, MyVariant>::type; // double

// 获取variant中最大类型的对齐要求
std::cout << "aligned_storage: " << alignof(MyVariant) << std::endl;
}

7.2 辅助函数

标准库提供了多个辅助函数来简化variant的操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <variant>
#include <iostream>

void helperFunctions() {
using MyVariant = std::variant<int, double, std::string>;

MyVariant v = 42;

// 获取当前值的索引
std::cout << "index: " << v.index() << std::endl;

// 检查variant是否持有特定类型的值
std::cout << "holds_alternative<int>: " << std::holds_alternative<int>(v) << std::endl;

// 获取特定类型的值(可能抛出异常)
int value = std::get<int>(v);

// 安全地获取特定类型的值(返回空指针表示不匹配)
auto* ptr = std::get_if<int>(&v);
}

7.3 std::visit详解

std::visit是使用variant最强大的方式,它允许我们对variant中存储的值应用一个visitor。std::visit可以接受一个或多个variant参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <variant>
#include <iostream>
#include <string>

void visitAdvanced() {
std::variant<int, double> v1 = 10;
std::variant<int, double> v2 = 20;

// 使用多个variant调用visit
std::visit([](auto a, auto b) {
std::cout << "a + b = " << (a + b) << std::endl;
}, v1, v2);

// visitor可以有状态
int sum = 0;
std::visit([&sum](auto value) {
sum += value;
}, v1);
std::cout << "Sum: " << sum << std::endl;

// visitor可以返回结果
auto result = std::visit([](auto a, auto b) -> double {
return a + b;
}, v1, v2);
std::cout << "Result: " << result << std::endl;
}

7.4 std::monostate

std::monostate是一个辅助类型,用于表示variant的”空”或”无效”状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <variant>
#include <iostream>

void monostateUsage() {
// Optional类型的设计模式
using OptionalInt = std::variant<std::monostate, int>;

OptionalInt empty;
OptionalInt hasValue = 42;

// 检查空状态
if (std::holds_alternative<std::monostate>(empty)) {
std::cout << "empty has no value" << std::endl;
}

// 在visit中处理空状态
std::visit([](auto&& arg) {
if constexpr (std::is_same_v<std::decay_t<decltype(arg)>, std::monostate>) {
std::cout << "Empty" << std::endl;
} else {
std::cout << "Value: " << arg << std::endl;
}
}, empty);
}

7.5 异常类

当variant访问失败时,会抛出std::bad_variant_access异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <variant>
#include <iostream>
#include <stdexcept>

void exceptionHandling() {
std::variant<int, double> v = 10;

try {
// 尝试获取错误的类型
std::cout << std::get<double>(v) << std::endl;
} catch (const std::bad_variant_access& e) {
std::cout << "Variant access error: " << e.what() << std::endl;
}
}

7.6 组合使用

在实际应用中,经常需要将variant与其他标准库组件组合使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <variant>
#include <vector>
#include <algorithm>

void combineWithContainers() {
// variant的vector
std::vector<std::variant<int, std::string, double>> values;
values.push_back(1);
values.push_back("hello");
values.push_back(3.14);

// 处理vector中的每个variant
std::for_each(values.begin(), values.end(), [](const auto& v) {
std::visit([](auto&& arg) {
std::cout << arg << " ";
}, v);
});
std::cout << std::endl;
}

// 与std::function组合
#include <functional>

using Operation = std::variant<
std::function<int(int, int)>, // 算术函数
std::function<std::string(const std::string&)> // 字符串函数
>;

void functionVariant() {
Operation op = [](int a, int b) { return a + b; };

std::visit([](auto&& func) {
if constexpr (std::is_invocable_v<decltype(func), int, int>) {
std::cout << "Result: " << func(10, 20) << std::endl;
}
}, op);
}

八、可以实现的设计模式

std::variant可以用于实现多种经典的设计模式,它的类型安全性和灵活性使其成为替代传统面向对象实现的有力工具。下面介绍几种与variant密切相关的设计模式及其实现方式。

8.1 状态模式

使用variant实现状态模式是一种高效且类型安全的方法。与传统的使用虚函数的实现相比,variant-based状态模式避免了虚函数调用的开销,并且状态转换的逻辑更加清晰。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
#include <variant>
#include <iostream>
#include <string>

// 前向声明
class TCPConnection;

// 状态类型定义
struct TCPState {
virtual ~TCPState() = default;
virtual void process(TCPConnection& conn) = 0;
};

struct ClosedState : TCPState {
void process(TCPConnection& conn) override;
};

struct EstablishedState : TCPState {
void process(TCPConnection& conn) override;
};

struct ListenState : TCPState {
void process(TCPConnection& conn) override;
};

// 使用variant的状态模式(更轻量级的实现)
struct Closed {
void enter(TCPConnection& conn) { std::cout << "Entering Closed state" << std::endl; }
void exit(TCPConnection& conn) { std::cout << "Exiting Closed state" << std::endl; }
};

struct Established {
void enter(TCPConnection& conn) { std::cout << "Connection established" << std::endl; }
void exit(TCPConnection& conn) { std::cout << "Closing connection" << std::endl; }
};

struct Listening {
int backlog = 10;
void enter(TCPConnection& conn) { std::cout << "Listening on port" << std::endl; }
void exit(TCPConnection& conn) { std::cout << "Stopped listening" << std::endl; }
};

// 使用variant的TCPConnection
class TCPConnection {
public:
using State = std::variant<Closed, Established, Listening>;
State currentState;

void transitionTo(State newState) {
std::visit([this](auto&& s) {
s.exit(*this);
}, currentState);

currentState = std::move(newState);

std::visit([this](auto&& s) {
s.enter(*this);
}, currentState);
}

void processData(const std::string& data) {
std::visit([&data](auto&& s) {
using T = std::decay_t<decltype(s)>;
if constexpr (std::is_same_v<T, Established>) {
std::cout << "Processing data: " << data << std::endl;
} else {
std::cout << "Cannot process data in this state" << std::endl;
}
}, currentState);
}
};

8.2 访问者模式

variant为访问者模式提供了一种类型安全的替代实现。相比传统的双分派实现,variant-based访问者模式更简洁,且编译器会确保所有类型都被处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#include <variant>
#include <iostream>
#include <string>

// 抽象语法树节点类型
struct NumberNode {
int value;
};

struct BinaryOpNode {
char op;
void* left;
void* right;
};

struct StringNode {
std::string value;
};

// 使用variant表示所有节点类型
using ASTNode = std::variant<NumberNode, BinaryOpNode, StringNode>;

// 访问者定义
struct PrintVisitor {
void operator()(const NumberNode& n) {
std::cout << n.value;
}

void operator()(const BinaryOpNode& op) {
std::cout << "(";
std::visit(*this, *static_cast<ASTNode*>(op.left));
std::cout << " " << op.op << " ";
std::visit(*this, *static_cast<ASTNode*>(op.right));
std::cout << ")";
}

void operator()(const StringNode& s) {
std::cout << "\"" << s.value << "\"";
}
};

// 使用variant的表达式求值器
struct EvalVisitor {
int operator()(const NumberNode& n) {
return n.value;
}

int operator()(const BinaryOpNode& op) {
int left = std::visit(*this, *static_cast<ASTNode*>(op.left));
int right = std::visit(*this, *static_cast<ASTNode*>(op.right));

switch (op.op) {
case '+': return left + right;
case '-': return left - right;
case '*': return left * right;
case '/': return left / right;
default: return 0;
}
}

int operator()(const StringNode&) {
return 0; // 字符串不参与数值运算
}
};

8.3 策略模式

使用variant实现策略模式允许在编译期选择不同的策略,同时保持运行时绑定的灵活性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <variant>
#include <iostream>
#include <functional>

// 不同的压缩策略
struct NoCompression {
std::string compress(const std::string& data) { return data; }
std::string decompress(const std::string& data) { return data; }
};

struct GzipCompression {
std::string compress(const std::string& data) { return "[gzip]" + data; }
std::string decompress(const std::string& data) { return data.substr(6); }
};

struct DeflateCompression {
std::string compress(const std::string& data) { return "[deflate]" + data; }
std::string decompress(const std::string& data) { return data.substr(9); }
};

// 使用variant的压缩器
class Compressor {
public:
using CompressionStrategy = std::variant<NoCompression, GzipCompression, DeflateCompression>;
CompressionStrategy strategy;

Compressor(CompressionStrategy s) : strategy(std::move(s)) {}

std::string compress(const std::string& data) {
return std::visit([&data](auto&& s) {
return s.compress(data);
}, strategy);
}

std::string decompress(const std::string& data) {
return std::visit([&data](auto&& s) {
return s.decompress(data);
}, strategy);
}
};

8.4 命令模式

variant可以用于实现命令模式,统一处理不同类型的命令对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <variant>
#include <iostream>
#include <string>
#include <memory>

// 命令定义
struct CreateCommand {
std::string name;
void execute() const { std::cout << "Creating: " << name << std::endl; }
};

struct DeleteCommand {
int id;
void execute() const { std::cout << "Deleting ID: " << id << std::endl; }
};

struct UpdateCommand {
int id;
std::string field;
std::string value;
void execute() const {
std::cout << "Updating ID " << id << ": " << field << " = " << value << std::endl;
}
};

// 使用variant的命令模式
class CommandQueue {
public:
using CommandType = std::variant<CreateCommand, DeleteCommand, UpdateCommand>;
std::vector<CommandType> commands;

void addCommand(CommandType cmd) {
commands.push_back(std::move(cmd));
}

void executeAll() {
for (const auto& cmd : commands) {
std::visit([](const auto& c) { c.execute(); }, cmd);
}
}
};

8.5 解析器模式

variant非常适合实现解析器,因为解析结果可能有多种类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#include <variant>
#include <iostream>
#include <string>
#include <vector>

// 解析结果的类型
struct NumberResult { double value; };
struct StringResult { std::string value; };
struct ErrorResult { std::string message; };
struct ListResult { std::vector<double> values; };

using ParseResult = std::variant<NumberResult, StringResult, ErrorResult, ListResult>;

// 简单的解析器
class Parser {
public:
ParseResult parse(const std::string& input) {
if (input.empty()) {
return ErrorResult{"Empty input"};
}

if (input[0] == '[') {
// 解析列表
return ListResult{{1.0, 2.0, 3.0}};
}

if (isdigit(input[0]) || input[0] == '-') {
try {
return NumberResult{std::stod(input)};
} catch (...) {
return ErrorResult{"Invalid number"};
}
}

return StringResult{input};
}
};

void printResult(const ParseResult& result) {
std::visit([](auto&& r) {
using T = std::decay_t<decltype(r)>;
if constexpr (std::is_same_v<T, NumberResult>) {
std::cout << "Number: " << r.value << std::endl;
} else if constexpr (std::is_same_v<T, StringResult>) {
std::cout << "String: " << r.value << std::endl;
} else if constexpr (std::is_same_v<T, ErrorResult>) {
std::cout << "Error: " << r.message << std::endl;
} else if constexpr (std::is_same_v<T, ListResult>) {
std::cout << "List: ";
for (auto v : r.values) std::cout << v << " ";
std::cout << std::endl;
}
}, result);
}

8.6 事件系统

使用variant可以实现类型安全的事件系统。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#include <variant>
#include <iostream>
#include <string>
#include <vector>
#include <functional>

// 事件类型定义
struct MouseEvent { int x, y; };
struct KeyEvent { char key; bool pressed; };
struct ResizeEvent { int width, height; };

using Event = std::variant<MouseEvent, KeyEvent, ResizeEvent>;

// 事件处理函数类型
using EventHandler = std::function<void(const Event&)>;

// 事件分发器
class EventDispatcher {
std::vector<EventHandler> handlers;

public:
void subscribe(EventHandler handler) {
handlers.push_back(std::move(handler));
}

void dispatch(const Event& event) {
for (auto& handler : handlers) {
handler(event);
}
}
};

// 使用示例
void setupEventSystem() {
EventDispatcher dispatcher;

// 注册不同的处理器
dispatcher.subscribe([](const Event& e) {
std::visit([](auto&& event) {
using T = std::decay_t<decltype(event)>;
if constexpr (std::is_same_v<T, MouseEvent>) {
std::cout << "Mouse at (" << event.x << ", " << event.y << ")" << std::endl;
} else if constexpr (std::is_same_v<T, KeyEvent>) {
std::cout << "Key " << event.key << (event.pressed ? " pressed" : " released") << std::endl;
} else if constexpr (std::is_same_v<T, ResizeEvent>) {
std::cout << "Resized to " << event.width << "x" << eventendl;
}
}, e);
});

// .height << std::发送事件
dispatcher.dispatch(MouseEvent{100, 200});
dispatcher.dispatch(KeyEvent{'A', true});
dispatcher.dispatch(ResizeEvent{800, 600});
}

通过这些设计模式的variant实现,我们可以看到variant作为一种类型安全的异构联合体,在现代C++编程中具有广泛的应用价值。它不仅简化了代码,还提供了编译期的类型安全保证,是C++17最重要的特性之一。

variant特别适合以下场景:需要表示”一个值,多种可能类型”;需要类型安全的联合体;需要实现状态机或解析器;需要避免使用void*或手动类型标签。使用variant时,应注意保持类型列表的简洁、处理所有可能的类型、考虑异常安全性,并在适当的情况下选择更简单的替代方案(如模板)。

一、concept是什么

concept是C++20引入的一项革命性特性,它是一种编译期布尔表达式,用于指定模板参数必须满足的约束条件。concept本质上是一种类型谓词(type predicate),它可以在编译期对模板参数进行静态检查,如果参数不满足约束条件,编译器会给出清晰的错误信息,而不会触发复杂的模板错误实例化。

在传统的C++模板编程中,我们只能通过SFINAE(Substitution Failure Is Not An Error)机制来限制模板参数的类型,这种方式通常会在模板实例化失败时产生冗长且难以理解的错误信息。concept的出现彻底改变了这一局面,它提供了一种更加直观、声明式的方式来表达模板约束条件,使得代码的可读性和可维护性得到了显著提升。

concept的定义使用concept关键字 followed by a boolean constant expression。concept可以在命名空间中定义,也可以在类内部定义,但通常我们将其定义在全局命名空间或专门的命名空间中以便复用。下面是一个最基本的concept定义示例:

1
2
3
4
5
6
// 定义一个检查类型是否可比较的concept
template<typename T>
concept Comparable = requires(T a, T b) {
{ a == b } -> std::convertible_to<bool>;
{ a != b } -> std::convertible_to<bool>;
};

这个concept名为Comparable,它要求类型T必须支持==!=运算符,并且这些运算符的返回类型必须可以转换为boolrequires表达式是concept定义的核心语法,它允许我们使用一种更加灵活和表达力更强的方式来指定约束条件。

concept的求值结果是一个编译期的布尔常量,这意味着我们可以在条件语句中使用concept来选择不同的模板实现,也可以将多个concept组合成更复杂的约束条件。C++标准库在<concepts>头文件中提供了大量预定义的concept,涵盖了从基本类型特性到复杂关系约束的各种场景,这些标准concept为我们的日常编程提供了极大的便利。

二、concept有什么作用

concept的核心作用是提供编译期的类型检查和约束验证机制,它从根本上改变了我们编写和设计泛型代码的方式。通过使用concept,我们可以将模板参数的要求明确地表达出来,使得代码的意图更加清晰,同时也让编译器能够在编译期捕获类型不匹配的错误。

第一个重要作用是改善编译错误信息。当传统的模板约束被违反时,编译器通常会产生大量令人困惑的错误信息,这些信息往往指向模板实例化的深层内部结构,使得定位问题变得异常困难。而当约束使用concept表达时,编译器可以在第一时间给出清晰的错误提示,告诉开发者哪个concept约束没有被满足,甚至可以指出具体是哪个操作无法编译。

第二个重要作用是实现更精细的模板分派机制。通过将concept作为模板约束条件,我们可以在同一个函数名下提供多个重载版本参数,编译器会根据传入的类型自动选择最合适的实现。这种机制类似于函数重载,但是作用在模板层面,能够处理更广泛的类型场景。例如,我们可以为支持加法的类型提供一个版本,为不支持加法的类型提供另一个版本。

第三个重要作用是提升代码的文档性和可读性。当我们查看一个使用concept约束的函数签名时,可以立即了解该函数对模板参数有哪些要求,而不需要深入阅读函数实现来推断这些约束。这种声明式的约束表达方式使得代码自文档化(self-documenting)成为可能,团队成员可以更快地理解和使用他人编写的泛型代码。

第四个重要作用是支持更安全的泛型编程。在没有concept的时代,泛型代码往往会在模板实例化的深层位置出现难以追踪的错误,这些错误可能在代码部署后才被发现,给系统稳定性带来隐患。concept通过将类型检查提前到编译期,大大降低了运行时出现类型错误的风险,提高了整个代码库的健壮性。

第五个重要作用是促进泛型库的标准化和互操作性。由于concept提供了一种标准化的约束表达方式,不同开发者编写的泛型组件可以更容易地相互配合使用。只要两个组件使用了相同的concept来表达它们的约束要求,它们就可以无缝地协同工作,这极大地促进了C++泛型编程生态系统的健康发展。

三、concept怎么使用

concept的使用是C++20泛型编程的核心技能,掌握它需要从定义、约束组合、函数模板约束、类模板约束、模板别名约束等多个维度进行学习。下面我们将详细讲解concept的各种使用方法,这些内容是本文的重点部分。

3.1 定义基本的concept

定义concept的基本语法使用template参数列表 followed by concept关键字和concept名称,最后是一个编译期布尔表达式。最简单的concept可以只包含一个布尔常量表达式:

1
2
3
4
5
6
7
// 最简单的concept定义
template<typename T>
concept Integral = std::is_integral_v<T>;

// 使用类型别名简化concept定义
template<typename T>
concept Arithmetic = std::is_arithmetic_v<T>;

然而,更强大和灵活的concept定义需要使用requires表达式。requires表达式允许我们使用一种类似于函数体的语法来指定约束条件,它可以包含多个约束子句,每个子句检查一个特定的要求是否被满足。requires表达式有四种形式,分别适用于不同的场景。

第一种形式是简单约束(Simple Requirement),它只是要求某个表达式是有效的:

1
2
3
4
template<typename T>
concept Hashable = requires(T t) {
std::hash<T>{}(t); // 要求T类型的对象可以用于std::hash
};

第二种形式是类型约束(Type Requirement),使用typename关键字来要求某个类型名是有效的:

1
2
3
4
5
6
7
template<typename T>
concept Container = requires(T t) {
typename T::value_type; // 要求T有value_type类型成员
typename T::iterator; // 要求T有iterator类型成员
{ t.begin() }; // 要求T有begin()成员函数
{ t.end() }; // 要求T有end()成员函数
};

第三种形式是复合约束(Compound Requirement),使用大括号包围表达式,followed by约束后置条件:

1
2
3
4
5
template<typename T>
concept Incrementable = requires(T x) {
{ x++ } -> std::same_as<T>; // 后置递增返回原类型
{ ++x } -> std::same_as<T&>; // 前置递增返回引用
};

第四种形式是嵌套约束(Nested Requirement),使用requires关键字嵌套更复杂的条件:

1
2
3
4
template<typename T>
concept Addable = requires(T a, T b) {
requires std::same_as<decltype(a + b), T>; // 加法结果类型与操作数相同
};

3.2 在函数模板中使用concept约束

concept最常见的用途是约束函数模板。约束函数模板有三种主要语法,每种语法都有其适用场景和特点。第一种语法是将concept名称放在template参数列表后面:

1
2
3
4
5
// 使用concept约束函数模板
template<Comparable T>
T max(T a, T b) {
return (a > b) ? a : b;
}

第二种语法是使用requires子句,这是最灵活的方式,允许指定多个concept的组合:

1
2
3
4
5
6
// 使用requires子句约束函数模板
template<typename T>
requires Integral<T> || FloatingPoint<T>
T abs(T value) {
return value >= 0 ? value : -value;
}

第三种语法是使用尾置返回类型:

1
2
3
4
5
// 使用尾置返回类型和concept约束
template<typename T>
auto sum(T a, T b) -> Addable<T> {
return a + b;
}

对于需要返回类型约束的情况,我们可以将concept与尾置返回类型结合使用,这特别适用于返回类型依赖于参数类型的场景:

1
2
3
4
5
6
// 返回类型受concept约束
template<typename T>
requires Numeric<T>
std::common_type_t<T> multiply(T a, T b) {
return a * b;
}

3.3 在类模板中使用concept约束

concept同样可以用于约束类模板,这使得我们可以根据类型约束来选择不同的类实现:

1
2
3
4
5
6
7
8
9
10
11
12
// 约束类模板
template<typename T>
requires Regular<T>
class MyContainer {
// T是常规类型(可默认构造、可赋值、可比较相等)
};

template<typename T>
requires (!Regular<T>)
class MyContainer {
// 处理非Regular类型的特化版本
};

类模板的成员函数也可以独立地使用concept约束,这允许我们为某些成员函数提供更严格的要求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
template<typename T>
class DataHolder {
public:
// 这个成员函数要求T是可拷贝的
T getCopy() requires Copyable<T> {
return data_;
}

// 这个成员函数要求T是可移动的
T getMoved() requires Moveable<T> {
return std::move(data_);
}

private:
T data_;
};

3.4 concept的组合使用

多个concept可以通过逻辑运算符组合使用,这使得我们可以构建复杂的约束条件。C++提供了三种组合运算符:&&(逻辑与)、||(逻辑或)和!(逻辑非):

1
2
3
4
5
6
7
8
9
10
11
12
// 组合多个concept
template<typename T>
concept NumericContainer = Container<T> && Numeric<typename T::value_type>;

// 使用组合concept约束函数
template<NumericContainer T>
void process(T& container) {
// T既是容器,又包含数值类型元素
for (auto& elem : container) {
// 可以对elem进行数值运算
}
}

对于需要多个concept同时满足的场景,使用&&运算符可以优雅地表达这种需求。例如,我们需要一个类型既支持排序又支持随机访问:

1
2
3
4
5
6
7
8
template<typename T>
concept SortableRandomAccessContainer =
RandomAccessContainer<T> && Sortable<T>;

template<SortableRandomAccessContainer T>
void sortContainer(T& container) {
std::sort(container.begin(), container.end());
}

我们也可以使用||运算符来表达”或”的关系,这在需要支持多种类型接口的场景中非常有用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
template<typename T>
concept HasFirst = requires(T t) { { t.first } -> std::convertible_to<int>; };

template<typename T>
concept HasValue = requires(T t) { { t.value } -> std::convertible_to<int>; };

// 只要满足其中一个即可
template<typename T>
requires HasFirst<T> || HasValue<T>
int getIntValue(T t) {
if constexpr (HasFirst<T>) {
return t.first;
} else {
return t.value;
}
}

3.5 在requires表达式中检查可变参数模板

对于需要处理任意数量参数的模板函数,我们可以使用可变参数模板结合requires表达式来实现更灵活的约束:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 要求所有参数类型都支持加法
template<typename... Args>
requires (Addable<Args> && ...)
Args sumAll(Args... args) {
return (args + ...);
}

// 要求至少有一个参数且所有参数可比较相等
template<typename T, typename... Rest>
requires (EqualityComparableWith<T, Rest> && ...)
bool allEqual(T first, Rest... rest) {
return ((first == rest) && ...);
}

3.6 使用concept选择函数重载

concept的一个强大特性是允许编译器根据concept约束自动选择最优的函数重载。这种机制被称为约束分派(constrained dispatch),它使得我们可以为一组相关但不完全相同的类型提供不同的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 为支持加法的类型提供泛型实现
template<Addable T>
T add(T a, T b) {
return a + b;
}

// 为字符串类型提供特化实现(优先匹配)
template<>
std::string add<std::string>(std::string a, std::string b) {
return a + b;
}

// 使用concept进行重载解析
template<typename T>
requires std::same_as<T, int>
T add(T a, T b) {
return a + b + 1; // 整数有特殊处理
}

当存在多个重载版本时,编译器会选择约束条件最具体(最严格)的那个版本。这种机制类似于函数重载的解析过程,但是作用在模板约束层面。

四、concept有哪些使用场景

concept在实际编程中有广泛的应用场景,从简单的类型检查到复杂的泛型库设计,都能看到concept的身影。理解这些使用场景对于有效地运用concept至关重要。

第一个常见场景是约束容器类型参数。当我们设计泛型容器算法时,通常需要确保传入的类型满足容器的基本要求,例如拥有value_typeiterator等类型成员,以及begin()end()等成员函数。使用concept可以清晰地表达这些要求:

1
2
3
4
5
6
7
template<Container C>
void printContainer(const C& container) {
for (const auto& elem : container) {
std::cout << elem << " ";
}
std::cout << std::endl;
}

第二个常见场景是约束算法模板参数。不同的算法对操作数有不同的要求,例如排序算法要求元素支持<运算符,数值算法要求元素支持算术运算符。使用concept可以明确地表达这些要求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 要求容器元素支持小于比较
template<Sortable T>
void quickSort(std::vector<T>& vec) {
// 排序实现
}

// 要求元素支持加减运算
template<Addable T>
T accumulate(const std::vector<T>& vec) {
T sum = T{};
for (const auto& elem : vec) {
sum += elem;
}
return sum;
}

第三个常见场景是实现策略模式(Strategy Pattern)。concept使得我们可以在运行时根据类型约束选择不同的策略,这是对传统策略模式的一种编译期扩展:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 定义不同的处理策略concept
template<typename T>
concept ParallelProcessable = requires(T t) {
{ t.parallelProcess() };
};

template<typename T>
concept SequentialProcessable = requires(T t) {
{ t.sequentialProcess() };
};

// 根据concept选择不同的处理方式
template<Processor P>
void execute(P& processor) {
if constexpr (ParallelProcessable<P>) {
processor.parallelProcess();
} else if constexpr (SequentialProcessable<P>) {
processor.sequentialProcess();
}
}

第四个常见场景是约束模板元编程的输入类型。在模板元编程中,我们经常需要确保输入类型满足某些条件,否则后续的计算可能没有意义。concept提供了一种优雅的方式来表达这些先决条件:

1
2
3
4
5
6
7
8
9
10
// 确保输入是有效的矩阵类型
template<Matrix M>
class MatrixCalculator {
public:
// 矩阵乘法运算
auto multiply(const M& other) -> MatrixResult<M> {
static_assert(M::rows == M::cols, "必须是方阵");
// 乘法实现
}
};

第五个常见场景是实现概念检查库。许多现代C++库(如Ranges库、Eigen库等)都使用concept来定义它们的接口约束,这使得用户可以清楚地了解使用这些库需要满足的条件:

1
2
3
4
5
6
7
8
9
10
// 使用Ranges库的concept
#include <ranges>

void processRange(std::ranges::input_range auto& range) {
// 处理输入范围
}

void sortRandomAccess(std::ranges::random_access_range auto& range) {
std::sort(range.begin(), range.end());
}

第六个常见场景是在模板库中提供更好的错误信息。传统的模板库在类型不匹配时会产生冗长的错误信息,而使用concept可以显著改善这种情况:

1
2
3
4
5
6
7
8
9
10
11
// 没有concept约束时,错误信息可能指向模板内部的深处
template<typename T>
void legacyFunction(T t) {
t.complexOperation(); // 如果T没有这个方法,错误信息可能很长
}

// 使用concept约束后,错误信息更加清晰
template<HasComplexOperation T>
void modernFunction(T t) {
t.complexOperation();
}

第七个常见场景是实现类型安全的异构容器。concept使得我们可以在编译期检查容器元素的类型约束,从而实现更严格的类型安全保证:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
template<typename T>
concept Serializable = requires(T t) {
{ t.serialize() } -> std::convertible_to<std::string>;
};

template<Serializable T>
class SerializationAwareContainer {
std::vector<T> items_;

public:
void add(const T& item) {
items_.push_back(item);
}

void saveAll(const std::string& filename) {
std::ofstream file(filename);
for (const auto& item : items_) {
file << item.serialize() << std::endl;
}
}
};

五、concept与SFINAE之间的等效替代方式与优势

SFINAE(Substitution Failure Is Not An Error)是C++11引入的一种机制,它允许在模板参数替换失败时不产生错误,而是简单地不匹配该模板。concept与SFINAE在功能上有很大的重叠,但concept提供了更加优雅和可维护的解决方案。理解两者的等效替代方式以及concept的优势,对于从传统SFINAE代码迁移到concept至关重要。

5.1 SFINAE的基本机制

在concept出现之前,SFINAE是实现模板约束的主要方式。SFINAE有两种主要的表现形式:一种是使用enable_if模板,另一种是使用函数返回类型或参数列表中的表达式求值失败。

使用enable_if进行约束的典型模式如下:

1
2
3
4
5
6
7
8
9
10
11
12
// SFINAE模式:使用enable_if约束模板
template<typename T>
typename std::enable_if<std::is_integral<T>::value, T>::type
abs(T value) {
return value >= 0 ? value : -value;
}

template<typename T>
typename std::enable_if<std::is_floating_point<T>::value, T>::type
abs(T value) {
return value >= 0 ? value : -value;
}

另一种SFINAE模式是利用函数模板参数替换时的失败:

1
2
3
4
5
6
7
8
9
10
11
// SFINAE模式:利用替换失败
template<typename T>
auto process(T t) -> decltype(t.compile_check(), void()) {
// t必须支持compile_check成员函数
}

template<typename T>
auto process(T t) -> void(*)() {
// T不支持compile_check时的替代实现
return [] { /* 默认行为 */ };
}

5.2 使用concept替代SFINAE

使用concept可以完全替代SFINAE的功能,同时代码更加清晰。下面的代码展示了如何用concept重写上面的例子:

1
2
3
4
5
6
7
8
9
10
// Concept模式:使用concept约束模板
template<Integral T>
T abs(T value) {
return value >= 0 ? value : -value;
}

template<FloatingPoint T>
T abs(T value) {
return value >= 0 ? value : -value;
}

对于需要根据类型选择不同实现的场景,concept提供了更优雅的表达方式:

1
2
3
4
5
6
7
8
9
10
// 使用concept的分派机制替代SFINAE分派
template<typename T>
void process(T t) requires HasCompileCheck<T> {
t.compile_check();
}

template<typename T>
void process(T t) requires (!HasCompileCheck<T>) {
// 默认实现
}

5.3 concept相比SFINAE的优势

concept相对于SFINAE有多个显著优势,这些优势使得concept成为现代C++编程的首选方案。

第一个优势是错误信息的可读性。当SFINAE模板匹配失败时,错误信息往往包含大量的模板内部实现细节,开发者需要仔细分析才能找到问题根源。而concept失败时,编译器会明确指出哪个concept约束没有被满足:

1
2
3
4
5
// SFINAE的错误信息示例(假设)
// error: no type named 'type' in 'struct std::enable_if<false, void>'

// Concept的错误信息示例
// error: candidate function template not viable: requires 'Integral' concept was not satisfied

第二个优势是代码的可维护性。使用enable_if的代码往往冗长且难以理解,需要编写复杂的模板元代码来表达简单的类型约束。而concept允许我们使用声明式的语法来表达这些约束,代码更加简洁明了。

第三个优势是命名和复用的便利性。一旦定义了concept,就可以在多个地方复用它,而SFINAE的约束条件通常是内联写的,难以复用和共享:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Concept可以定义后复用
template<typename T>
concept Numeric = std::is_arithmetic_v<T>;

// 在多个地方使用同一个concept
template<Numeric T>
void process1(T value);

template<Numeric T>
T calculate(T a, T b);

// SFINAE的条件难以复用
template<typename T>
typename std::enable_if<std::is_arithmetic_v<T>>::type
process1(T value);

template<typename T>
typename std::enable_if<std::is_arithmetic_v<T>, T>::type
calculate(T a, T b);

第四个优势是支持组合和逻辑运算。concept可以通过&&||!等运算符优雅地组合,而SFINAE需要嵌套使用enable_if来实现相同的逻辑:

1
2
3
4
5
6
7
8
9
10
// Concept的组合使用
template<typename T>
concept ComplexConstraint = SimpleConcept<T> && OtherConcept<T>;

// SFINAE的组合使用
template<typename T>
typename std::enable_if<
SimpleConcept<T>::value && OtherConcept<T>::value
>::type
process(T t);

第五个优势是与C++20 Ranges库的深度集成。C++20引入的Ranges库大量使用concept来定义其接口,学习concept对于有效使用这些现代库是必不可少的。

5.4 渐进式的迁移策略

从SFINAE迁移到concept可以采用渐进式的策略,不需要一次性重写所有代码。以下是一种推荐的迁移方法:

首先,将现有的enable_if约束转换为简单的concept:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 原始SFINAE代码
template<typename T>
typename std::enable_if<std::is_copy_constructible_v<T>>::type
clone(T& original) {
// 实现
}

// 第一步:创建concept
template<typename T>
concept CopyConstructible = std::is_copy_constructible_v<T>;

// 第二步:使用concept约束
template<CopyConstructible T>
void clone(T& original) {
// 实现
}

然后,逐步将更复杂的SFINAE模式迁移到concept。对于需要多个条件组合的情况,可以定义组合concept:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 复杂的SFINAE条件
template<typename T>
typename std::enable_if<
std::is_default_constructible_v<T> &&
std::is_move_constructible_v<T>
>::type
initialize();

// 迁移到concept
template<typename T>
concept MovableAndDefaultConstructible =
std::is_default_constructible_v<T> &&
std::is_move_constructible_v<T>;

template<MovableAndDefaultConstructible T>
void initialize() {
// 实现
}

六、如何避免滥用concept

虽然concept是一个强大的工具,但滥用concept会导致代码复杂度增加、维护困难等问题。了解常见的滥用模式以及如何避免它们,对于写出高质量的C++代码至关重要。

第一个常见的滥用是将concept应用于所有模板参数。实际上,并非所有模板参数都需要concept约束。对于内部实现不需要了解类型细节的通用容器,可以使用无约束的模板参数,concept约束应该只在需要表达接口要求时使用:

1
2
3
4
5
6
7
8
9
10
11
// 过度约束的例子
template<DefaultConstructible T, Destructible T, Copyable T>
class OverConstrainedContainer {
// 可能过度约束
};

// 更合理的约束方式
template<Regular T> // Regular已经包含了基本要求
class BetterContainer {
// Regular = DefaultConstructible + Copyable + Moveable + EqualityComparable
};

第二个常见的滥用是定义过于复杂的concept。concept应该保持简单和可读,过于复杂的concept难以理解和维护。如果一个concept需要表达大量的约束条件,可能意味着应该将其拆分成多个更小的concept:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 过于复杂的concept
template<typename T>
concept ComplexConcept = requires(T t) {
typename T::value_type;
typename T::reference;
typename T::const_reference;
typename T::iterator;
typename T::const_iterator;
{ t.begin() };
{ t.end() };
{ t.size() };
{ t.empty() };
{ t[0] };
{ t.at(0) };
};

// 拆分成更小的concept
template<typename T>
concept HasValueType = requires { typename T::value_type; };

template<typename T>
concept Iterable = requires(T t) {
{ t.begin() };
{ t.end() };
};

template<typename T>
concept Sized = requires(T t) {
{ t.size() } -> std::convertible_to<std::size_t>;
{ t.empty() } -> std::convertible_to<bool>;
};

template<typename T>
concept RandomAccess = requires(T t) {
{ t[0] };
{ t.at(0) };
};

// 组合使用
template<typename T>
concept Container = HasValueType<T> && Iterable<T> && Sized<T>;

第三个常见的滥用是在不需要的地方使用concept。concept主要用于模板代码的编译期约束,对于非模板代码或运行时多态,使用concept是不合适的:

1
2
3
4
5
6
7
8
9
10
11
// 不需要concept的场景
// 普通函数不需要concept约束
int add(int a, int b) {
return a + b;
}

// 类成员函数如果不需要模板化,也不应该使用concept
class SimpleClass {
public:
void process() { /* 实现 */ }
};

第四个常见的滥用是忽略concept的命名规范。良好的命名可以使代码更加自文档化,concept名称应该清晰地表达它们检查的属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 不好的命名
template<typename T>
concept C1 = std::is_integral_v<T>;

template<typename T>
concept C2 = requires(T t) { { t.value() }; };

// 好的命名
template<typename T>
concept IntegralNumber = std::is_integral_v<T>;

template<typename T>
concept HasValueMethod = requires(T t) { { t.value() }; };

第五个常见的滥用是过度依赖concept而忽略运行时检查。concept只提供编译期检查,对于某些需要在运行时验证的条件,仍然需要使用断言或其他运行时检查机制:

1
2
3
4
5
6
7
8
9
10
// 只依赖concept是不够的
template<PositiveNumber T>
void process(T value) {
// concept只保证T是数值类型,不保证value是正数
// 需要添加运行时检查
if (value <= 0) {
throw std::invalid_argument("Value must be positive");
}
// ...
}

第六个常见的滥用是创建重复的标准concept。C++标准库在<concepts>头文件中提供了大量预定义的concept,在定义自己的concept之前,应该先检查标准库是否已经提供了相同或相似的concept:

1
2
3
4
5
6
7
8
// 重复造轮子
template<typename T>
concept MyIntegral = std::is_integral_v<T>;

// 应该直接使用标准concept
template<Integral T>
// 或
template<std::integral<T>

为了避免滥用concept,遵循以下原则会有所帮助:

保持concept简单:一个concept应该只检查一个特定的属性或能力。如果需要检查多个属性,使用组合。

只在需要的地方使用concept:只有当模板代码需要表达对类型的要求时,才使用concept约束。不要为了使用concept而使用它。

优先使用标准concept:在编写自己的concept之前,先查看标准库是否提供了类似的concept。标准concept经过充分的测试和优化。

使concept的名称具有描述性:concept的名称应该清楚地表达它检查的属性,例如使用Addable而不是C1来表示支持加法的类型。

区分编译期和运行期约束:concept提供编译期检查,某些运行时验证仍然需要使用断言或其他机制。

七、concept相关标准库

C++20标准库在<concepts>头文件中定义了一系列预定义的concept,这些concept涵盖了从基本类型属性到复杂关系的各种场景。熟悉这些标准concept对于高效使用concept至关重要。

7.1 基础类型concept

基础类型concept用于检查类型的基本属性,这些concept是最常用也是最基础的:

1
2
3
4
5
6
7
8
9
10
11
// 检查类型是否相同
template<typename T, typename U>
concept std::same_as = /* ... */;

// 检查类型是否可转换为另一种类型
template<typename From, typename To>
concept std::convertible_to = /* ... */;

// 检查是否是派生关系
template<typename Derived, typename Base>
concept std::derived_from = /* ... */;

7.2 类型分类concept

类型分类concept用于检查类型的分类属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 检查是否是整数类型
template<typename T>
concept std::integral = /* ... */;

// 检查是否是浮点类型
template<typename T>
concept std::floating_point = /* ... */;

// 检查是否是算术类型(整数或浮点)
template<typename T>
concept std::arithmetic = /* ... */;

// 检查是否是对象类型
template<typename T>
concept std::object = /* ... */;

// 检查是否是函数类型
template<typename T>
concept std::function = /* ... */;

// 检查是否是引用类型
template<typename T>
concept std::reference = /* ... */;

// 检查是否是成员指针类型
template<typename T>
concept std::member_pointer = /* ... */;

// 检查是否是枚举类型
template<typename T>
concept std::enum = /* ... */;

// 检查是否是联合类型
template<typename T>
concept std::union_type = /* ... */;

7.3 类型特性concept

类型特性concept用于检查类型的更复杂特性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 检查是否是移动构造的类型
template<typename T>
concept std::move_constructible = /* ... */;

// 检查是否是拷贝构造的类型
template<typename T>
concept std::copy_constructible = /* ... */;

// 检查是否是默认构造的类型
template<typename T>
concept std::default_initializable = /* ... */;

// 检查是否是分配的类型
template<typename T>
concept std::allocatable = /* ... */;

7.4 可调用概念

可调用concept用于检查类型是否可以被调用:

1
2
3
4
5
6
7
// 检查是否是可调用类型
template<typename F, typename... Args>
concept std::invocable = /* ... */;

// 检查调用结果是否可以转换为指定类型
template<typename F, typename... Args, typename R>
concept std::predicate = /* ... */;

7.5 比较概念

比较concept用于检查类型的比较操作:

1
2
3
4
5
6
7
// 检查是否可以比较相等
template<typename T>
concept std::equality_comparable = /* ... */;

// 检查是否可以严格弱序比较
template<typename T>
concept std::strict_weak_order = /* ... */;

7.6 对象概念

对象concept用于检查对象的属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 检查是否是可移动的类型
template<typename T>
concept std::movable = /* ... */;

// 检查是否是可拷贝的类型
template<typename T>
concept std::copyable = /* ... */;

// 检查是否是半常规类型(可移动、可拷贝、可默认构造)
template<typename T>
concept std::semiregular = /* ... */;

// 检查是否是常规类型(半常规 + 可相等比较)
template<typename T>
concept std::regular = /* ... */;

7.7 自定义标准concept

除了使用标准concept外,我们还可以基于标准concept创建自己的自定义concept:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 基于标准concept创建自定义concept
template<typename T>
concept MyPointer = std::is_pointer_v<T>;

template<typename T>
concept MyContainer = requires(T t) {
typename T::value_type;
{ t.begin() };
{ t.end() };
{ t.size() } -> std::convertible_to<std::size_t>;
};

// 组合使用多个标准concept
template<typename T>
concept MyNumericContainer =
std::regular<T> &&
requires(T t) {
typename T::value_type;
std::is_arithmetic_v<typename T::value_type>;
};

7.8 使用标准库concept的完整示例

下面是一个使用标准库concept的完整示例,展示了如何在实际编程中组合使用这些concept:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <concepts>
#include <vector>
#include <list>
#include <iostream>

// 使用标准concept定义函数模板
template<std::regular T>
class Container {
std::vector<T> data_;

public:
void add(const T& value) {
data_.push_back(value);
}

template<std::strict_weak_order<T> Comp>
void sort(Comp comp = std::less<T>{}) {
std::sort(data_.begin(), data_.end(), comp);
}

void print() const {
for (const auto& elem : data_) {
std::cout << elem << " ";
}
std::cout << std::endl;
}
};

int main() {
Container<int> intContainer;
intContainer.add(5);
intContainer.add(2);
intContainer.add(8);
intContainer.add(1);
intContainer.sort();
intContainer.print(); // 输出: 1 2 5 8

return 0;
}

八、concept相关的设计模式

concept在设计模式中的应用为传统的面向对象设计模式注入了新的活力。通过concept,我们可以在编译期实现更加灵活和高效的变体,这些变体往往比传统的运行时多态具有更好的性能。下面介绍几种与concept密切相关的设计模式及其实现方式。

8.1 策略模式的概念化实现

传统的策略模式通过接口和多态实现不同算法的切换,而使用concept可以在编译期实现类似的分派功能,同时保持零运行时开销:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 定义不同的策略concept
template<typename Strategy>
concept AdditionStrategy = requires(Strategy s, int a, int b) {
{ s.compute(a, b) } -> std::same_as<int>;
};

template<typename Strategy>
concept MultiplicationStrategy = requires(Strategy s, int a, int b) {
{ s.compute(a, b) } -> std::same_as<int>;
};

// 策略类
struct AddStrategy {
int compute(int a, int b) { return a + b; }
};

struct MultiplyStrategy {
int compute(int a, int b) { return a * b; }
};

struct SubtractStrategy {
int compute(int a, int b) { return a - b; }
};

// 使用concept约束的策略计算器
template<AdditionStrategy Strategy>
int calculate(Strategy strategy, int a, int b) {
return strategy.compute(a, b);
}

template<MultiplicationStrategy Strategy>
int calculate(Strategy strategy, int a, int b) {
return strategy.compute(a, b);
}

// 通用策略计算器
template<typename Strategy>
requires (!AdditionStrategy<Strategy> && !MultiplicationStrategy<Strategy>)
int calculate(Strategy strategy, int a, int b) {
return strategy.compute(a, b);
}

8.2 模板方法模式的概念化实现

concept使得模板方法模式可以更加灵活地定义算法的各个步骤,而不需要使用虚函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
template<typename T>
concept Drawable = requires(T t) {
{ t.draw() };
{ t.getColor() } -> std::convertible_to<std::string>;
};

template<typename T>
concept Resizable = requires(T t, double factor) {
{ t.resize(factor) };
};

template<Drawable T>
class Shape {
public:
void render() {
// 模板方法:draw()是虚函数般的存在
onBeforeDraw();
T& self = static_cast<T&>(*this);
self.draw();
onAfterDraw();
}

std::string getDescription() {
T& self = static_cast<T&>(*this);
return "Shape with color: " + self.getColor();
}

protected:
void onBeforeDraw() { /* 通用前置处理 */ }
void onAfterDraw() { /* 通用后置处理 */ }
};

// 具体形状类
class Circle : public Shape<Circle> {
public:
void draw() { /* 绘制圆形 */ }
std::string getColor() const { return "red"; }
};

class Rectangle : public Shape<Rectangle> {
public:
void draw() { /* 绘制矩形 */ }
std::string getColor() const { return "blue"; }
};

8.3 访问者模式的概念化实现

使用concept可以创建类型安全的访问者模式,避免传统访问者模式中的类型安全问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 前向声明
template<typename Derived>
class Visitable;

template<typename T>
concept Acceptable = requires(T& visitor) {
{ visitor.visit(std::declval<Visitable<typename T::ElementType>>()) };
};

template<typename Derived>
class Visitable {
public:
template<Acceptable Visitor>
void accept(Visitor& visitor) {
visitor.visit(static_cast<Derived&>(*this));
}
};

class Circle : public Visitable<Circle> {
public:
double radius = 1.0;
};

class Rectangle : public Visitable<Rectangle> {
public:
double width = 1.0;
double height = 1.0;
};

// 访问者类
class ShapeVisitor {
public:
void visit(Circle& circle) {
std::cout << "Circle with radius: " << circle.radius << std::endl;
}

void visit(Rectangle& rectangle) {
std::cout << "Rectangle: " << rectangle.width << "x" << rectangle.height << std::endl;
}
};

8.4 工厂模式的概念化实现

concept使得工厂模式可以更加灵活地处理不同类型的产品,同时保持类型安全:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// 产品concept
template<typename T>
concept Product = requires {
typename T::id;
{ T::create() };
};

// 产品注册表
template<Product T>
class ProductRegistry {
using Id = typename T::id;

public:
static Id id() { return T::id; }

static std::unique_ptr<T> create() {
return std::make_unique<T>();
}
};

// 产品基类和产品类
struct ProductBase {
virtual ~ProductBase() = default;
virtual void operation() = 0;
};

struct ProductA : ProductBase {
static constexpr auto id = "ProductA";

static std::unique_ptr<ProductA> create() {
return std::make_unique<ProductA>();
}

void operation() override {
std::cout << "ProductA operation" << std::endl;
}
};

struct ProductB : ProductBase {
static constexpr auto id = "ProductB";

static std::unique_ptr<ProductB> create() {
return std::make_unique<ProductB>();
}

void operation() override {
std::cout << "ProductB operation" << std::endl;
}
};

8.5 装饰器模式的概念化实现

使用concept可以创建更加灵活的装饰器模式,支持动态组合不同的装饰功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
template<typename T>
concept Decoratable = requires(T t, int v) {
{ t.getValue() } -> std::convertible_to<int>;
{ t.setValue(v) };
};

template<Decoratable T>
class Decorator {
protected:
std::unique_ptr<T> wrapped_;

public:
explicit Decorator(std::unique_ptr<T> wrapped)
: wrapped_(std::move(wrapped)) {}

int getValue() {
return wrapped_->getValue();
}

void setValue(int v) {
wrapped_->setValue(v);
}
};

class TimestampingDecorator : public Decorator<TimestampingDecorator> {
using Decorator::Decorator;

public:
int getValue() {
auto val = Decorator::getValue();
std::cout << "Timestamp: " << std::chrono::system_clock::now() << std::endl;
return val;
}
};

class ValidationDecorator : public Decorator<ValidationDecorator> {
using Decorator::Decorator;

public:
void setValue(int v) {
if (v < 0) {
throw std::invalid_argument("Value must be non-negative");
}
Decorator::setValue(v);
}
};

通过这些设计模式的概念化实现,我们可以看到concept不仅是一种类型约束工具,更是一种设计思维方式的转变。它允许我们在编译期实现原本需要在运行时才能实现的多态和灵活性,同时带来更好的类型安全性和性能。这些模式在现代C++库(如Ranges库)中得到了广泛的应用,掌握它们对于编写高质量的泛型代码具有重要意义。

concept作为C++20最重要的特性之一,它为泛型编程带来了革命性的变化。通过提供声明式的类型约束机制,concept使得代码更加清晰、错误信息更加友好、设计更加灵活。从基本类型检查到复杂的设计模式实现,concept都有其独特的价值。在实际开发中,我们应该充分利用concept的优势,同时注意避免滥用,以写出高质量、可维护的C++代码。

SwiftUI学习笔记

基本组件的使用

Text

简介

显示文本的组件

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

import SwiftUI

struct ContentView: View {
var body: some View {
Text("文本")
.foregroundColor(.blue) /// 文本颜色
.bold() /// 粗体
.italic() /// 斜体
.font(.system(.largeTitle)) /// 字体
.fontWeight(.medium) /// 字重
.shadow(color: .black, radius: 1, x: /*@START_MENU_TOKEN@*/0.0/*@END_MENU_TOKEN@*/, y: 2) /// 阴影

}
}

效果图

image-20210203153356368

TextField

简介

文本输入框

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import SwiftUI

struct ContentView: View {

@State var phone: String = "+86182****6667"

var body: some View {
VStack {
TextField("手机号", text: $phone) { /// 内容变化回调
print("changed: \($0)")
} onCommit: { /// 按下键盘的enter时触发
print("phone: \(self.phone)")
}
.foregroundColor(.white) /// 白色字体
.padding() /// 第一次内边距可以调整文字
.background(Color.green) /// 设置背景色,会设置整个组件背景色
.cornerRadius(10) /// 再切出圆角
.padding() /// 再整体添加内边距
}
.background(Color.purple)
.padding()
}
}

效果

紫色背景是VStack容器

image-20210203160047884

SecureField

简介

功能和使用方法与 TextField差不多, 只是多了个密码显示的功能

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import SwiftUI

struct ContentView: View {

@State var password: String = "admin"

var body: some View {
VStack {
SecureField("密码", text: $password) {
print("commit: \(self.password)")
}
.foregroundColor(.white) /// 白色字体
.padding() /// 第一次内边距可以调整文字
.background(Color.green) /// 设置背景色,会设置整个组件背景色
.cornerRadius(10) /// 再切除圆角
.padding() /// 再整体添加内边距
}
.background(Color.purple)
.padding()
}
}

效果

image-20210203160708310

Image

简介

图片显示组件

代码

1
2
3
4
5
6
7
8
9
10
import SwiftUI

struct ContentView: View {

var body: some View {
Image(systemName: "4k.tv")
.font(.system(size: 44, weight: .ultraLight))
.border(Color.red, width: 1)
}
}

效果

image-20210203161559768

Button

简介

按钮组件,可以点击触发相关事件

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import SwiftUI

struct ContentView: View {

@State var swither: Bool = false

var body: some View {
Button(swither ? "状态A":"状态B") { /// 这里处理点击事件
self.swither.toggle()
}
.frame(width: 100, height: 44, alignment: .center) /// 尺寸
.background(swither ? Color.red: Color.blue) /// 背景色
.foregroundColor(swither ? .yellow: .white) /// 字体色
.cornerRadius(6) /// 圆角
}
}

效果

Kapture 2021-02-03 at 16.26.33

DatePicker

简介

日期/时间 选择

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import SwiftUI

struct ContentView: View {

@State var date: Date = Date()

var body: some View {
DatePicker("日期", selection: $date, displayedComponents: .date)
.background(Color.gray)
.cornerRadius(4)
.padding()

}
}

效果

Kapture 2021-02-03 at 16.34.25

Toggle

简介

开关状态组件

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import SwiftUI

struct ContentView: View {

@State var enable: Bool = false

var body: some View {
Toggle(enable ? "启用": "禁用", isOn: $enable)
.frame(width: 100, height: 22, alignment: .center)
.padding()
.background(Color.pink)
.cornerRadius(6)

}
}

效果

Kapture 2021-02-03 at 16.43.33

Slider

简介

滑动条

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import SwiftUI

struct ContentView: View {

@State var power: Float = 0.0

var body: some View {
HStack {
Slider(value: $power, in: 0...100)
.frame(width: 160)
Text("能量值:\(power)")
.foregroundColor(.blue)
.font(.system(size: 12))
.frame(width: 120, height: 44, alignment: .leading)
}.padding()

}
}

效果

Stepper

简介

步进器,-/+控制增减

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import SwiftUI

struct ContentView: View {

@State var count: Int = 0

var body: some View {
HStack {
Stepper("数量", value: $count, step: 1) {
print("changed: \($0)")
}
Text("\(count)")
.frame(width: 60, height: 44, alignment: .center)
}.padding()

}
}

效果

Kapture 2021-02-03 at 17.05.56