Cpp Concept

Concept 是 C++20 对模板参数的命名约束。它解决了模板编程的核心痛点:错误信息晦涩、约束表达隐式、SFINAE 代码难以维护。
1. 核心语法
template<typename T>
concept ConceptName = constraint_expression;定义示例:
template<typename T>
concept Addable = requires(T a, T b) {
a + b; // 表达式必须合法
};
template<typename T>
concept Numeric = std::integral<T> || std::floating_point<T>;
template<typename T>
concept Container = requires(T c) {
typename T::value_type; // 必须有嵌套类型
c.begin(); // 必须有成员函数
c.end();
requires std::same_as<typename T::iterator, decltype(c.begin())>;
};2. 使用方式
四种等价写法:
// 1. 模板参数约束
template<Addable T>
T add(T a, T b) { return a + b; }
// 2. requires 子句(函数声明后)
template<typename T>
requires Addable<T>
T add(T a, T b) { return a + b; }
// 3. requires 子句(函数签名中)
template<typename T>
T add(T a, T b) requires Addable<T> { return a + b; }
// 4. 简写语法(constrained auto)
Addable auto add(Addable auto a, Addable auto b) { return a + b; }3. requires 表达式
requires 表达式有四种形式:
template<typename T>
concept Example = requires(T t, T::value_type v) {
// 1. 简单要求:表达式必须合法
t.size();
// 2. 类型要求:嵌套类型必须存在
typename T::value_type;
typename T::iterator;
// 3. 复合要求:表达式结果约束
{ t.begin() } -> std::same_as<typename T::iterator>;
{ t.empty() } -> std::convertible_to<bool>;
{ t.size() } noexcept -> std::same_as<size_t>;
// 4. 嵌套要求:约束组合
requires std::default_initializable<T>;
requires std::copy_constructible<T>;
};复合要求语法:{ expression } noexcept -> type-constraint;
noexcept可选,要求表达式不抛异常-> type-constraint要求表达式结果满足约束
4. 标准 Concept 分类
<concepts> 头文件提供四类约束:
类型关系
std::same_as<T, U> // T 和 U 是同一类型
std::derived_from<D, B> // D 公有继承自 B
std::convertible_to<From, To> // From 可隐式转换为 To
std::common_with<T, U> // T 和 U 有公共类型类型属性
std::integral<T> // 整型
std::floating_point<T> // 浮点型
std::is_pointer<T> // 指针
std::is_array<T> // 数组对象属性
std::regular<T> // 可拷贝、可比较、可默认构造
std::semiregular<T> // 可拷贝、可默认构造
std::trivially_copyable<T> // 可平凡拷贝可调用
std::invocable<F, Args...> // F 可以用 Args... 调用
std::regular_invocable<F, Args...> // 相等输入产生相等输出
std::predicate<F, Args...> // 返回可转换为 bool5. Subsumption(概念包含)
Concept 支持重载决议时的包含关系:更严格的 concept 优先匹配。
template<typename T>
concept Integral = std::integral<T>;
template<typename T>
concept SignedIntegral = Integral<T> && std::is_signed_v<T>;
// SignedIntegral 包含 Integral,更严格
// 调用 SignedIntegral 版本时,signed 类型优先匹配
template<Integral T>
void process(T x) { std::print("Integral\n"); }
template<SignedIntegral T>
void process(T x) { std::print("SignedIntegral\n"); }
process(42); // int 是 signed,输出 SignedIntegral
process(42u); // unsigned int,输出 Integral规则:当 concept A 的约束蕴含 concept B(A 更严格),则 A 包含 B。重载决议时,更严格的 concept 优先。
6. 错误信息对比
SFINAE 时代的错误(数百行模板展开):
error: no match for 'operator<<' (operand types are 'std::ostream'
{aka 'std::basic_ostream<char>'} and 'std::vector<int>')
/usr/include/c++/13/ostream:108:7: note: candidate: ...
/usr/include/c++/13/ostream:117:7: note: candidate: ...
[... 50+ lines of template instantiation backtrace ...]Concept 时代的错误:
error: cannot bind non-const lvalue reference of type 'int&'
to an rvalue of type 'int'
note: constraints not satisfied
note: the required expression 't.size()' is invalid编译器直接告诉你哪个约束失败,而不是展开整个模板实例化链。
7. 与 SFINAE 对比
// SFINAE:晦涩、难以组合
template<typename T,
typename = std::enable_if_t<std::is_integral_v<T>>>
T multiply(T a, T b) { return a * b; }
// Concept:清晰、可组合
template<std::integral T>
T multiply(T a, T b) { return a * b; }
// SFINAE 组合多个约束:噩梦
template<typename T,
typename = std::enable_if_t<
std::is_integral_v<T> &&
std::is_copy_constructible_v<T>>>
void func(T);
// Concept 组合:自然
template<std::integral T>
requires std::copy_constructible<T>
void func(T);8. 实际应用
约束迭代器类型
template<typename T>
concept ForwardIterator = requires(T it) {
{ *it } -> std::same_as<typename T::reference>;
{ ++it } -> std::same_as<T&>;
{ it++ } -> std::same_as<T>;
requires std::equality_comparable<T>;
};
template<ForwardIterator It>
void advance(It& it, size_t n) {
while (n--) ++it;
}约束可序列化类型
template<typename T>
concept Serializable = requires(std::ostream& os, const T& obj) {
{ os << obj } -> std::same_as<std::ostream&>;
};
template<Serializable T>
void save(const T& obj, const std::filesystem::path& file) {
std::ofstream ofs(file);
ofs << obj;
}约束数值计算
template<typename T>
concept Arithmetic = std::integral<T> || std::floating_point<T>;
template<Arithmetic T>
constexpr T clamp(T value, T lo, T hi) {
return value < lo ? lo : (value > hi ? hi : value);
}9. 注意事项
1. Concept 是编译期约束
template<std::integral T>
void func(T x);
func(42); // OK
func(3.14); // 编译错误,不是运行时检查2. 约束不满足时的替代方案
// 使用 requires 选择不同实现
template<typename T>
requires std::integral<T>
T abs(T x) { return x < 0 ? -x : x; }
template<typename T>
requires std::floating_point<T>
T abs(T x) { return std::fabs(x); }3. 避免过度约束
// 不好:过度约束,限制了可用类型
template<typename T>
concept StrictContainer = requires(T c) {
{ c.size() } -> std::same_as<size_t>; // 太严格
};
// 好:宽松约束,更通用
template<typename T>
concept Container = requires(T c) {
{ c.size() } -> std::convertible_to<size_t>;
};4. Concept 与 auto 结合
// 简写语法
void process(std::integral auto x);
// 等价于
template<std::integral T>
void process(T x);10. 与 Ranges 库结合
Ranges 库大量使用 concept 约束算法:
#include <ranges>
#include <algorithm>
// std::ranges::sort 要求 random_access_range
std::vector<int> vec = {3, 1, 4, 1, 5};
std::ranges::sort(vec); // OK
std::list<int> lst = {3, 1, 4};
// std::ranges::sort(lst); // 编译错误:list 不满足 random_access_range
std::ranges::sort(std::views::take(vec, 3)); // OK常用 Ranges concept:
| Concept | 要求 |
|---|---|
range | 有 begin() 和 end() |
sized_range | size() 在常数时间返回 |
random_access_range | 支持常数时间随机访问 |
view | 可移动、常数时间构造/析构 |
11. 前置知识:typename 与嵌套类型
在模板中使用嵌套类型(如 T::iterator)时,必须使用 typename 关键字明确告诉编译器这是一个类型而非静态成员。
为什么需要 typename
当编译器遇到 T::iterator 这样的语法时,无法确定这是一个嵌套类型还是一个静态数据成员:
template<typename T>
void process(T container) {
T::iterator it; // 编译错误!iterator 是类型还是静态成员?
}编译器默认假设这是静态数据成员。使用 typename 消除歧义:
template<typename T>
void process(T container) {
typename T::iterator it; // 明确声明 iterator 是嵌套类型
}必须使用 typename 的场景
| 场景 | 示例 |
|---|---|
| 声明依赖类型的嵌套类型变量 | typename T::iterator it; |
| 类型别名声明 | using ValueType = typename T::value_type; |
| 函数返回类型 | typename T::value_type get(); |
| 模板参数传递 | std::same_as<typename T::iterator, It> |
| Concept 的复合要求 | { c.begin() } -> std::same_as<typename T::iterator>; |
什么是嵌套类型
嵌套类型(Nested Type) 是指在类(或结构体)内部定义的类型。
class Container {
public:
// 嵌套类型定义(四种方式)
using value_type = int; // 方式1:using 别名
typedef int* pointer; // 方式2:typedef 别名
struct iterator { /* ... */ }; // 方式3:内部类
enum class Status { Ok, Error }; // 方式4:内部枚举
};
// 使用嵌套类型
Container::value_type x; // 等同于 int x
Container::iterator it; // Container 内部的 iterator 类型
Container::Status s; // Container 内部的 Status 枚举与成员变量的区别:
class Container {
public:
using value_type = int; // 嵌套类型(类型别名)
static int count; // 静态成员变量(是变量,不是类型)
int size; // 普通成员变量
};
Container::value_type x; // OK:value_type 是类型
Container::count = 10; // OK:count 是静态变量Concept 中的两种 typename 用法
在 requires 表达式中,typename T::name 有两种含义:
1. 类型要求(检查嵌套类型存在)
template<typename T>
concept HasIterator = requires {
typename T::iterator; // 检查 T 内部是否定义了 iterator 这个类型
};2. 类型声明(使用嵌套类型)
template<typename T>
concept Container = requires(T c) {
// 使用 T::iterator 作为类型参与比较
{ c.begin() } -> std::same_as<typename T::iterator>;
};不需要 typename 的情况
- 非依赖类型(类型已知,不依赖模板参数):
struct MyClass { using Inner = int; };
MyClass::Inner x; // 不需要 typename- 使用
auto推导(C++11 起):
template<typename Container>
void process(const Container& c) {
auto it = c.begin(); // 编译器自动推导,无需显式声明
}典型编译错误
如果不使用 typename:
template<typename T>
void bad(T& c) {
T::iterator it = c.begin(); // 错误!
}错误信息:
error: need 'typename' before 'T::iterator' because 'T' is a dependent scope12. 总结
| 特性 | SFINAE | Concept |
|---|---|---|
| 语法 | 晦涩 | 清晰 |
| 错误信息 | 冗长 | 明确 |
| 组合性 | 困难 | 自然 |
| 重载决议 | 手动 | 自动(subsumption) |
Concept 不是语法糖,而是模板元编程范式的转变:从"类型替换失败不是错误"到"类型约束显式表达"。当你写模板时,先问自己:这个类型需要满足什么条件?然后用 concept 表达它。







