文章目录
- 引言
- 一、Concepts 的基本语法
-
- 1.1 定义一个概念
- 1.2 使用概念的四种方式
- 1.3 使用标准库中的 Concepts
- 二、`requires` 表达式深入
-
- 2.1 四种需求(Requirements)
- 2.2 `requires` 表达式的真假判断
- 三、用 Concepts 约束类模板
- 四、Concepts 的重载解析
-
- 4.1 更特化的概念优先匹配
- 4.2 多个约束的优先级
- 五、Concepts 与实践——用约束消除运行时检查
-
- 5.1 对比:传统模板 vs Concepts 模板
- 5.2 实战:一个类型安全的通用数值容器
- 六、标准库 Concepts 速查
- 总结
本系列为《C++深度修炼:基础、STL源码与多线程实战》第32篇 前置条件:理解函数模板(第25篇)、类型萃取(第29篇),了解模板的错误信息(第27篇)
引言
传统模板的错误信息让人望而生畏——一个 sort(list) 的错误能吐出 200 行日志。根本原因在于:模板参数没有"类型契约"。template <typename T> 中的 typename 只是一个占位符——没有任何约束。编译器在深度实例化(几十层嵌套模板之后)才发现 T 不支持某个操作,然后从那个最深处开始,一层层向外报告"替换失败"。
C++20 的 Concepts(概念) 改变了这一切。它让你在模板声明时就写清楚"T 必须满足什么条件":
// C++20 之前:错误在实例化深度爆发
template <typename T>
T add(T a, T b) { return a + b; }
// C++20:在签名中就约束了 T
template <std::integral T>
T add(T a, T b) { return a + b; }
add(1, 2); // ✅ int 是 integral
// add(1.5, 2.5); // ❌ 编译错误——但只有一行:"double 不满足 integral 约束"
一、Concepts 的基本语法
1.1 定义一个概念
#include <concepts>
#include <type_traits>
// 方式一:用一个编译期布尔表达式定义
template <typename T>
concept Integral = std::is_integral_v<T>;
// 方式二:用 requires 表达式定义——检查是否支持某种操作
template <typename T>
concept Addable = requires(T a, T b) {
{ a + b } -> std::convertible_to<T>; // a + b 合法且结果可转换为 T
};
// 方式三:组合已有概念
template <typename T>
concept Numeric = std::integral<T> || std::floating_point<T>;
1.2 使用概念的四种方式
// 方式一:用 concept 代替 typename(最简洁)
template <Integral T>
T gcd(T a, T b) {
while (b != 0) {
T t = b;
b = a % b;
a = t;
}
return a;
}
// 方式二:requires 子句(最灵活——可以表达多参数约束)
template <typename T>
requires Integral<T>
T gcd2(T a, T b) {
while (b != 0) { T t = b; b = a % b; a = t; }
return a;
}
// 方式三:trailing requires(紧跟在参数列表之后)
template <typename T>
T gcd3(T a, T b) requires Integral<T> {
while (b != 0) { T t = b; b = a % b; a = t; }
return a;
}
// 方式四:requires 表达式作为参数约束
template <typename T>
requires requires(T a, T b) { // 第一个 requires 是子句,第二个是表达式
{ a + b } -> std::same_as<T>;
}
T add(T a, T b) { return a + b; }
1.3 使用标准库中的 Concepts
C++20 标准库在 <concepts> 头文件中提供了大量预定义概念:
#include <concepts>
#include <iostream>
// std::integral——整数类型
template <std::integral T>
T multiply(T a, T b) { return a * b; }
// std::floating_point——浮点类型
template <std::floating_point T>
T safe_divide(T a, T b) {
return b != 0 ? a / b : 0;
}
// std::copyable——可以拷贝的类型
template <std::copyable T>
T clone(const T &src) { return T(src); }
// std::movable——可以移动的类型
template <std::movable T>
void sink(T &&obj) {
T local(std::move(obj));
// 使用 local…
}
// std::same_as——类型完全相同
template <typename T>
requires std::same_as<T, int> || std::same_as<T, long>
T increment(T x) { return x + 1; }
// std::derived_from——是某个类的派生类
template <std::derived_from<std::iostream> T>
void process_stream(T &stream) {
// T 必须继承自 iostream
}
二、requires 表达式深入
2.1 四种需求(Requirements)
requires 表达式可以检查四种需求:
template <typename T>
concept ContainerLike = requires(T c, const T cc, size_t n) {
// 1. 简单需求——表达式是否合法(不关心类型)
c.begin(); // c.begin() 必须合法
c.end(); // c.end() 必须合法
// 2. 类型需求——某个类型是否存在
typename T::value_type; // T 必须有 value_type 类型成员
typename T::iterator; // T 必须有 iterator 类型成员
// 3. 复合需求——表达式的返回类型满足某概念
{ c.begin() } -> std::input_iterator; // begin() 返回的必须是输入迭代器
{ c.size() } -> std::convertible_to<size_t>; // size() 返回值能转为 size_t
// 4. 嵌套需求——requires 里面再套 requires
requires std::same_as<decltype(cc.size()), size_t>; // const 容器也能调 size()
};
2.2 requires 表达式的真假判断
requires 表达式本身是一个编译期布尔值——表达式合法时为 true,不合法时为 false:
static_assert(requires(int a, int b) { a + b; }); // true——int 可以加
static_assert(!requires(std::string a) { a + 1; }); // false——string + int 非法
static_assert(requires { typename std::vector<int>::iterator; }); // true
// 用于 static_assert 或者 if constexpr
if constexpr (requires(T x) { x.foo(); }) {
// 只有 T 有 foo() 时才编译这段
}
三、用 Concepts 约束类模板
#include <concepts>
#include <iostream>
#include <vector>
// 约束:T 必须是可打印的(支持 operator<<)
template <typename T>
concept Printable = requires(std::ostream &os, T val) {
{ os << val } -> std::same_as<std::ostream&>;
};
// 只有 Printable 类型才能放进 PrintableVector
template <Printable T>
class PrintableVector {
std::vector<T> data_;
public:
void push(const T &val) { data_.push_back(val); }
void print_all() const {
for (const auto &x : data_) {
std::cout << x << ' ';
}
std::cout << '\\n';
}
};
// 使用时——
struct NoStream {};
int main() {
PrintableVector<int> pv; // ✅ int 有 operator<<
pv.push(1);
pv.push(2);
pv.print_all(); // 1 2
// PrintableVector<NoStream> pv2; // ❌ 编译错误:NoStream 不满足 Printable
}
四、Concepts 的重载解析
4.1 更特化的概念优先匹配
#include <concepts>
#include <iostream>
// 通用版本——接受任意类型
template <typename T>
void classify(T) { std::cout << "other\\n"; }
// 整数版本——比通用版本更特化
template <std::integral T>
void classify(T) { std::cout << "integral\\n"; }
// 浮点版本——和整数版本互斥,都比通用版更特化
template <std::floating_point T>
void classify(T) { std::cout << "floating point\\n"; }
int main() {
classify(42); // integral(integral 比通用更特化)
classify(3.14); // floating point
classify("hello"); // other(只有通用版匹配)
}
4.2 多个约束的优先级
// 定义两个概念
template <typename T>
concept Small = sizeof(T) <= 4;
template <typename T>
concept Large = sizeof(T) > 4;
// 编译器会选择约束更强的版本
template <Small T> void optimize(T) { std::cout << "small path\\n"; }
template <Large T> void optimize(T) { std::cout << "large path\\n"; }
// template <typename T> void optimize(T) { std::cout << "fallback\\n"; }
int main() {
optimize(42); // small path(int 是 4 字节)
optimize(3.14); // large path(double 是 8 字节)
}
五、Concepts 与实践——用约束消除运行时检查
5.1 对比:传统模板 vs Concepts 模板
// C++17 及之前——运行时 static_assert 或等实例化失败
template <typename T>
T divide_ceiling(T a, T b) {
static_assert(std::is_integral_v<T>, "divide_ceiling requires integral type");
// static_assert 在实例化时才触发——但如果这行放在 cpp 文件中,可能永远不被触发
return (a + b – 1) / b;
}
// C++20——编译器在选择重载时就排除非整数类型
template <std::integral T>
T divide_ceiling(T a, T b) {
return (a + b – 1) / b;
}
5.2 实战:一个类型安全的通用数值容器
#include <concepts>
#include <iostream>
#include <vector>
#include <numeric>
#include <algorithm>
// 定义数值概念——整数或浮点
template <typename T>
concept Numeric = std::integral<T> || std::floating_point<T>;
// 只有数值类型才能进 MathVector
template <Numeric T>
class MathVector {
std::vector<T> data_;
public:
MathVector(std::initializer_list<T> il) : data_(il) {}
T sum() const {
return std::accumulate(data_.begin(), data_.end(), T(0));
}
T average() const {
if (data_.empty()) return T(0);
return sum() / static_cast<T>(data_.size());
}
T max() const {
return *std::max_element(data_.begin(), data_.end());
}
void print() const {
for (const auto &x : data_) std::cout << x << ' ';
std::cout << '\\n';
}
};
int main() {
MathVector<int> vi = {1, 2, 3, 4, 5};
MathVector<double> vd = {1.5, 2.5, 3.5};
std::cout << "sum: " << vi.sum() << " avg: " << vi.average() << '\\n';
std::cout << "sum: " << vd.sum() << " avg: " << vd.average() << '\\n';
// MathVector<std::string> vs; // ❌ 编译错误——string 不满足 Numeric
}
六、标准库 Concepts 速查
C++20 <concepts> 中包含的核心概念:
类型属性:
| std::integral<T> | 整数类型 |
| std::floating_point<T> | 浮点类型 |
| std::signed_integral<T> | 有符号整数 |
| std::unsigned_integral<T> | 无符号整数 |
| std::same_as<T, U> | T 和 U 完全相同 |
| std::derived_from<T, U> | T 派生自 U |
| std::convertible_to<T, U> | T 能隐式转换为 U |
构造与赋值:
| std::default_initializable<T> | 可默认构造 |
| std::copy_constructible<T> | 可拷贝构造 |
| std::move_constructible<T> | 可移动构造 |
| std::copyable<T> | 可拷贝(构造+赋值) |
| std::movable<T> | 可移动(构造+赋值) |
| std::destructible<T> | 可析构 |
比较:
| std::equality_comparable<T> | 支持 == 和 != |
| std::totally_ordered<T> | 支持全部比较运算符 |
迭代器与函数:
| std::input_iterator<T> | 输入迭代器 |
| std::output_iterator<T, V> | 输出迭代器 |
| std::forward_iterator<T> | 前向迭代器 |
| std::bidirectional_iterator<T> | 双向迭代器 |
| std::random_access_iterator<T> | 随机访问迭代器 |
| std::invocable<F, Args…> | F 可以用 Args… 调用 |
| std::predicate<F, Args…> | F 用 Args… 调用并返回 bool |
总结
Concepts 是 C++20 给泛型编程带来的最大变革——它把模板从"无类型约束的泛化"升级为"有类型契约的泛化":
这是第 6 章的最后一篇。从第 25 篇的函数模板入门,到这一篇的 Concepts——我们走完了"从无约束泛化到有约束泛化"的完整进化路径。下一章开始,我们将进入 Effective C++ 的核心习惯——这些是从 Scott Meyers 经典著作中提炼出的"不踩坑"日常编码准则。
动手练习:
