C++20 Concepts:约束与概念的现代范式
C++进阶计划 · Day 09 | 预计学习时长:2小时
引言
为什么这个知识点重要
C++20 Concepts 是自 C++11 模板元编程诞生以来最重要的语言特性之一。它将模板约束从"隐式契约"提升为"显式声明",让编译器在模板实例化前就能提供有意义的错误信息,将 SFINAE 的隐式失败转变为 Concepts 的显式约束。
对于有 7 年经验的 C++ 工程师,你一定经历过这样的痛苦:
cpp
// SFINAE 时代的"天书"错误信息
error: no type named 'type' in 'std::enable_if<...>'
// 整整 87 行的模板展开错误
Concepts 让我们写出的代码意图清晰,错误信息简洁明确:
cpp
error: constraints not satisfied
note: concept 'std::integral' was not satisfied
与前面内容的关联
Concepts 并非凭空出现,它是建立在以下知识之上的集大成者:
| 前置知识 | Concepts 中的体现 |
|---|---|
| 模板基础 (Day 21) | 概念本身就是模板 |
| SFINAE (Day 22) | Concepts 底层仍基于 SFINAE |
| Type Traits (Day 23) | 标准库 Concepts 大多基于 type traits |
requires 子句 (Day 7, if constexpr) |
requires 表达式是定义概念的核心语法 |
核心概念
1. Concepts 的本质
**概念(Concept)**是一个编译期的谓词,它指定了对模板参数的要求。一个类型要么满足概念,要么不满足------这是编译期的布尔关系。
cpp
// 概念定义语法 (C++20)
template <typename T>
concept MyIntegral = std::integral<T> && sizeof(T) >= 2;
关键理解:Concepts 不是类,不是结构体,不是类型------它是编译期的布尔常量。
2. 三种定义方式
2.1 使用 concept 关键字(推荐)
cpp
// 基本形式
template <typename T>
concept Hashable = requires(T a) {
std::hash<T>{}(a);
};
// 带约束的组合概念
template <typename T>
concept SequentialContainer =
requires(T c) {
typename T::value_type;
typename T::iterator;
requires std::movable<T>;
{ c.begin() } -> std::same_as<typename T::iterator>;
{ c.end() } -> std::same_as<typename T::iterator>;
};
2.2 使用 requires 子句(约束表达式)
cpp
template <typename T>
requires std::integral<T> && (sizeof(T) >= 4)
T multiply(T a, T b) {
return a * b;
}
2.3 简化的约束语法(C++20 最强大特性)
cpp
// 约束模板参数,等价于上面的 requires 子句
std::integral auto add(std::integral auto a, std::integral auto a2) {
return a + a2;
}
3. requires 表达式详解
requires 表达式是定义 Concepts 的核心语法,有四种形式:
3.1 简单要求(Simple Requirements)
cpp
template <typename T>
concept Addable = requires(T a, T b) {
a + b; // T 必须支持 operator+
a - b; // T 必须支持 operator-
};
3.2 类型要求(Type Requirements)
cpp
template <typename T>
concept Container = requires {
typename T::value_type; // T 必须有嵌套类型 value_type
typename T::iterator; // T 必须有嵌套类型 iterator
typename T::const_iterator; // T 必须有嵌套类型 const_iterator
};
3.3 复合要求(Compound Requirements)
cpp
template <typename T>
concept StackLike = requires(T a, const T b) {
// 表达式 a.push(b) 必须是 void 类型
{ a.push(b) } -> std::same_as<void>;
// 表达式 a.size() 必须是 std::size_t 类型
{ a.size() } -> std::same_as<std::size_t>;
// 表达式 a.empty() 必须是布尔类型
{ a.empty() } -> std::convertible_to<bool>;
};
3.4 嵌套要求(Nested Requirements)
cpp
template <typename T>
concept RandomAccessContainer = requires(T a, std::size_t n) {
// 首先 a[n] 返回的必须是 T::value_type
// 然后这个类型必须满足可默认构造
{ a[n] } -> std::same_as<typename T::value_type>;
requires std::default_initializable<typename T::value_type>;
};
4. 标准库 Concepts 全览
C++20 标准库提供了丰富的预定义 Concepts:
┌─────────────────────────────────────────────────────────────────┐
│ C++20 标准库 Concepts │
├─────────────────┬───────────────────────────────────────────────┤
│ 核心语言 Concepts│ │
├─────────────────┼───────────────────────────────────────────────┤
│ same_as │ T 和 U 是同一类型 │
│ derived_from │ D public/protected 继承自 B │
│ convertible_to │ T 可转换为 U,且不丢失信息 │
│ common_ref_with │ T 和 U 有共同的引用类型 │
│ assignable_from │ T 可从 U 赋值 │
│ swappable │ T 对象可交换 │
└─────────────────┴───────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ 概念族 │ 说明 │
├─────────────────┼───────────────────────────────────────────────┤
│ object_concepts │ movable, copyable, assignable, swappable │
│ callable_concepts│ invocable, regular_invocable, predicate │
│ comparison_concepts│ equality_comparable, totally_ordered │
│ arithmetic_concepts│ addable, subtractable, multipliable... │
└─────────────────┴───────────────────────────────────────────────┘
5. Concepts 的语义约束 vs 语法约束
这是区分优秀 Concepts 和普通 Concepts 的关键:
cpp
// ❌ 错误的做法:只检查语法,不检查语义
template <typename T>
concept BadAddable = requires(T a, T b) {
a + b; // 只检查 operator+ 是否存在
};
// ✅ 正确的做法:检查语义约束
template <typename T>
concept Addable = requires(T a, T b) {
a + b;
a += b; // 加法应该与增量赋值一致
}
&& requires(const T a, const T b) {
{ a + b } -> std::convertible_to<T>;
// 加法结果应该仍是 T 或可转换为 T
}
&& std::copyable<T>; // 可加对象应该可拷贝(通常)
代码实战
实战 1:手写一个类型无关的排序算法
cpp
#include <concepts>
#include <vector>
#include <iostream>
#include <algorithm>
// 约束 1:类型必须是可比较有序的
template <typename T>
concept Ordered = std::totally_ordered<T>;
// 约束 2:类型必须支持交换
template <typename T>
concept Swappable = std::swappable<T>;
// 约束 3:容器必须支持随机访问迭代器
template <typename C>
concept RandomAccessRange =
std::ranges::random_access_range<C> &&
Ordered<std::ranges::range_value_t<C>> &&
Swappable<std::ranges::range_value_t<C>>;
// 使用 Concepts 约束的快速排序
template <RandomAccessRange R>
void quicksort(R& range) {
quicksort_impl(range,
std::ranges::begin(range),
std::ranges::end(range));
}
template <RandomAccessRange R>
requires Ordered<std::ranges::range_reference_t<R>>
void quicksort_impl(R& range,
std::ranges::iterator_t<R> first,
std::ranges::iterator_t<R> last) {
// 递归终止条件
if (std::ranges::distance(first, last) <= 1) {
return;
}
// 分区:选择最后一个元素作为 pivot
auto pivot_iter = std::ranges::prev(last);
auto pivot_value = *pivot_iter;
// 分区逻辑
auto middle = std::partition(first, pivot_iter,
[&pivot_value](const auto& elem) {
return elem < pivot_value;
});
// 将 pivot 放到正确位置
std::ranges::iter_swap(middle, pivot_iter);
// 递归排序
quicksort_impl(range, first, middle);
quicksort_impl(range, std::ranges::next(middle), last);
}
// 测试
struct Point {
int x, y;
// 为 Point 实现比较运算符
friend bool operator<(const Point& a, const Point& b) {
return a.x < b.x || (a.x == b.x && a.y < b.y);
}
friend bool operator==(const Point& a, const Point& b) {
return a.x == b.x && a.y == b.y;
}
// C++20 spaceship 运算符自动生成所有比较操作
auto operator<=>(const Point&) const = default;
};
int main() {
// 测试整数数组
std::vector<int> ints = {64, 34, 25, 12, 22, 11, 90};
std::cout << "Before sorting: ";
for (auto i : ints) std::cout << i << " ";
std::cout << "\n";
quicksort(ints);
std::cout << "After sorting: ";
for (auto i : ints) std::cout << i << " ";
std::cout << "\n\n";
// 测试 Point 数组
std::vector<Point> points = {{3, 1}, {1, 4}, {2, 2}, {5, 0}};
std::cout << "Before sorting points: ";
for (auto& p : points) std::cout << "(" << p.x << "," << p.y << ") ";
std::cout << "\n";
quicksort(points);
std::cout << "After sorting points: ";
for (auto& p : points) std::cout << "(" << p.x << "," << p.y << ") ";
std::cout << "\n";
// 尝试用不支持比较的类型实例化(将产生清晰的编译错误)
// struct NotComparable {};
// std::vector<NotComparable> broken = {...};
// quicksort(broken); // 编译错误:NotComparable 不满足 Ordered concept
return 0;
}
实战 2:Concepts 与 Policy-Based Design 结合
cpp
#include <concepts>
#include <iostream>
#include <vector>
#include <numeric>
// ============ 策略概念定义 ============
template <typename Operation>
concept NumericOperation = requires(Operation op, double a, double b) {
{ op(a, b) } -> std::convertible_to<double>;
};
template <typename Formatter>
concept ResultFormatter = requires(Formatter fmt, double value) {
{ fmt.format(value) } -> std::convertible_to<std::string>;
};
// ============ 具体策略实现 ============
struct AddOp {
double operator()(double a, double b) const { return a + b; }
};
struct MultiplyOp {
double operator()(double a, double b) const { return a * b; }
};
struct PowerOp {
double operator()(double a, double b) const { return std::pow(a, b); }
};
struct ScientificFormatter {
std::string format(double value) const {
return std::format("{:.6e}", value);
}
};
struct SimpleFormatter {
std::string format(double value) const {
return std::format("{:.2f}", value);
}
};
// ============ Concepts 约束的计算器 ============
template <NumericOperation Op, ResultFormatter Formatter>
class Calculator {
public:
Calculator(Op op, Formatter fmt) : operation_(op), formatter_(fmt) {}
double calculate(const std::vector<double>& values) const {
if (values.empty()) return 0.0;
double result = values.front();
for (size_t i = 1; i < values.size(); ++i) {
result = operation_(result, values[i]);
}
return result;
}
std::string formatResult(const std::vector<double>& values) const {
return formatter_.format(calculate(values));
}
private:
Op operation_;
Formatter formatter_;
};
// 约束推导:不需要显式指定模板参数
template <NumericOperation Op, ResultFormatter Formatter>
Calculator(Op, Formatter) -> Calculator<Op, Formatter>;
int main() {
std::vector<double> data = {1.0, 2.0, 3.0, 4.0, 5.0};
// 组合不同策略
Calculator calc1{AddOp{}, ScientificFormatter{}};
Calculator calc2{MultiplyOp{}, SimpleFormatter{}};
Calculator calc3{PowerOp{}, ScientificFormatter{}};
std::cout << "Data: ";
for (auto v : data) std::cout << v << " ";
std::cout << "\n\n";
std::cout << "Sum (scientific): " << calc1.formatResult(data) << "\n";
std::cout << "Product (simple): " << calc2.formatResult(data) << "\n";
std::cout << "2^mean (scientific): " << calc3.formatResult(data) << "\n";
// 错误示例:使用不满足概念的类型
// struct BadOp { int operator()(int, int); }; // 返回 int 不是 double
// Calculator bad{BadOp{}, SimpleFormatter{}}; // 编译错误!
return 0;
}
实战 3:requires 表达式的高级用法
cpp
#include <concepts>
#include <type_traits>
#include <iostream>
#include <memory>
// ============ 检测成员存在性 ============
template <typename T, typename... Args>
concept ConstructibleFrom = std::constructible_from<T, Args...>;
// 检测是否有 reserve(size_t) 和 push_back(T) 成员
template <typename C, typename T>
concept BackInsertable = requires(C& c, T val) {
c.reserve(std::size_t{1}); // 可预留空间
c.push_back(val); // 可从尾部插入
};
// 检测是否有 at(size_t) 运算符
template <typename C, typename T>
concept RandomIndexable = requires(const C& c, std::size_t idx) {
{ c.at(idx) } -> std::same_as<T&>;
};
// ============ 可组合的 Constraints ============
template <typename T>
concept ArithmeticType =
std::integral<T> || std::floating_point<T>;
template <typename T>
concept BitwiseType =
std::integral<T> && requires(T a, T b) {
{ a & b } -> std::same_as<T>;
{ a | b } -> std::same_as<T>;
{ a ^ b } -> std::same_as<T>;
{ ~a } -> std::same_as<T>;
{ a << b } -> std::same_as<T>;
{ a >> b } -> std::same_as<T>;
};
// ============ 条件 Concepts ============
template <typename T>
concept StringLike =
std::same_as<T, std::string> ||
std::same_as<T, std::wstring> ||
std::same_as<T, std::u8string> ||
std::same_as<T, std::u16string> ||
std::same_as<T, std::u32string> ||
requires(T s) {
{ s.c_str() } -> std::convertible_to<const char*>;
{ s.length() } -> std::convertible_to<std::size_t>;
};
// ============ 实践:类型安全的工厂函数 ============
class Widget {
public:
virtual ~Widget() = default;
virtual void draw() const = 0;
};
class Button : public Widget {
public:
void draw() const override { std::cout << "Drawing Button\n"; }
};
class TextBox : public Widget {
public:
void draw() const override { std::cout << "Drawing TextBox\n"; }
};
// 要求 T 必须派生自 Widget
template <typename T>
concept WidgetDerived = std::derived_from<T, Widget>;
template <WidgetDerived T>
std::unique_ptr<T> createWidget() {
return std::make_unique<T>();
}
// 测试
int main() {
// 正常工作
auto btn = createWidget<Button>();
auto txt = createWidget<TextBox>();
btn->draw();
txt->draw();
// 编译期错误:int 不派生自 Widget
// auto bad = createWidget<int>(); // error!
// BitwiseType 示例
static_assert(BitwiseType<int>);
static_assert(BitwiseType<unsigned>);
static_assert(!BitwiseType<double>); // double 不支持位运算
// StringLike 示例
static_assert(StringLike<std::string>);
static_assert(!StringLike<int>);
return 0;
}
常见陷阱与最佳实践
陷阱 1:概念过于宽松
cpp
// ❌ 错误:只检查语法存在,不检查语义
template <typename T>
concept BadContainer = requires(T c) {
typename T::value_type;
{ c.begin() };
{ c.end() };
{ c.size() };
};
// 任何有这些成员的类型都满足,包括:
struct FakeContainer {
using value_type = int;
int begin() { return 0; }
int end() { return 0; }
double size() { return 1.5; } // size() 返回 double!这在语义上是错误的
};
最佳实践 :使用语义约束,使用 -> 返回类型约束。
cpp
// ✅ 正确:严格约束返回类型
template <typename T>
concept Container = requires(T c) {
typename T::value_type;
typename T::iterator;
typename T::const_iterator;
{ c.begin() } -> std::same_as<typename T::iterator>;
{ c.end() } -> std::same_as<typename T::iterator>;
{ c.size() } -> std::same_as<typename T::size_type>;
{ c.empty() } -> std::convertible_to<bool>;
};
陷阱 2:在概念定义中使用实现细节
cpp
// ❌ 错误:暴露了内部实现
template <typename T>
concept BadHashable = requires(T t) {
{ std::hash<T>{}(t) } -> std::convertible_to<std::size_t>;
};
// ✅ 正确:使用类型要求即可
template <typename T>
concept Hashable = requires(T t) {
typename std::hash<T>; // std::hash<T> 必须特化存在
{ std::hash<T>{}(t) } -> std::convertible_to<std::size_t>;
};
陷阱 3:忽略概念的可组合性
cpp
// ❌ 错误:重复定义类似概念
template <typename T>
concept AddableInt = std::integral<T> && requires(T a, T b) { a + b; };
template <typename T>
concept AddableFloat = std::floating_point<T> && requires(T a, T b) { a + b; };
// ✅ 正确:设计可组合的概念
template <typename T>
concept Addable = requires(T a, T b) {
{ a + b } -> std::same_as<T>;
};
template <typename T>
concept Numeric = std::movable<T> && Addable<T>;
// 然后组合使用
template <typename T>
requires Numeric<T>
T sum(T a, T b) { return a + b; }
陷阱 4: Concepts vs requires 子句 混淆使用
cpp
// ❌ 错误场景:混用导致冗余
template <typename T>
requires std::integral<T> // 第一个约束
concept MyIntegral = requires(T t) {
requires std::integral<T>; // 重复约束!
};
最佳实践:
| 场景 | 推荐方式 |
|---|---|
| 定义通用概念 | concept 关键字 |
| 约束函数模板 | requires 子句或简化的 auto 语法 |
| 条件继承/特化 | requires 子句 |
陷阱 5:可变参数 Concepts 的陷阱
cpp
// ❌ 错误:直接用参数包展开
template <typename... Ts>
concept VariadicAddable = requires(Ts... args) {
(args + ...); // 这只在 Ts... 非空时有效!
};
// ✅ 正确:分离空包和非空包情况
template <typename... Ts>
concept VariadicAddable = (sizeof...(Ts) >= 2) && requires(Ts... args) {
(args + ...);
};
进阶思考
1. Concepts 与 Concepts 库
C++20 Ranges 是 Concepts 应用最广泛的领域:
cpp
// 使用标准库的 range concepts
#include <ranges>
template <std::ranges::input_range R>
requires std::ranges::viewable_range<R>
class MyProcessor {
// ...
};
// 常用 ranges concepts 速查
std::ranges::range // 有 begin/end 的类型
std::ranges::view // 不拥有数据的视图
std::ranges::input_range // 只读遍历
std::ranges::forward_range // 可多次遍历
std::ranges::bidirectional_range // 可前后移动
std::ranges::random_access_range // O(1) 下标访问
std::ranges::contiguous_range // 内存连续
2. Concepts 与编译期多态
Concepts 让我们可以同时使用编译期多态和运行时多态:
cpp
// 编译期多态:通过 Concepts 约束
template <typename T>
requires std::integral<T>
T process(T value) {
// 编译期选择实现
}
// 运行时多态:通过虚函数
class Processor {
public:
virtual ~Processor() = default;
virtual void process() = 0;
};
// 组合使用
template <std::derived_from<Processor> ConcreteProcessor>
class SmartProcessor : public ConcreteProcessor {
// 同时使用 Concepts 约束和运行时多态
};
3. C++23 和未来展望
C++23 对 Concepts 的增强:
cpp
// C++23: 显式对象参数(deducing this)
template <typename Self>
requires std::integral<Self>
auto operator+(const Self& self, const Self& other) {
return self + other; // 更自然的语法
}
// C++23: std::expected (结合 Concepts)
template <typename T, typename E>
class Expected {
// 可以用 Concepts 约束 T 和 E 的类型
};
参考资源
标准文档
权威书籍
- 《C++20 Concepts: The Principles and Practice of Generic Programming》 - Bjarne Stroustrup 等
- 《Programming Constraints》 - Microsoft Patterns & Practices
- 《Ranges for the Standard Library》 - Tristram Macdonald
高质量博客
- Fluent C++ - C++20 Concepts: What They Are and Why We Need Them
- Barry Revzin - requires expressions
- Simon Brand - C++20 Concepts Speficiation
标准库实现参考
下期预告:Day 10 - C++20 Ranges(上):View/Range 模型、管道操作符、惰性求值、常用 views