Concept 是 C++20 对模板参数的命名约束。它解决了模板编程的核心痛点:错误信息晦涩、约束表达隐式、SFINAE 代码难以维护

核心语法

1
2
template<typename T>
concept ConceptName = constraint_expression;

定义示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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())>;
};

使用方式

四种等价写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 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; }

requires 表达式

requires 表达式有四种形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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 要求表达式结果满足约束

标准 Concept 分类

<concepts> 头文件提供四类约束:

类型关系

1
2
3
4
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 有公共类型

类型属性

1
2
3
4
std::integral<T>             // 整型
std::floating_point<T> // 浮点型
std::is_pointer<T> // 指针
std::is_array<T> // 数组

对象属性

1
2
3
std::regular<T>              // 可拷贝、可比较、可默认构造
std::semiregular<T> // 可拷贝、可默认构造
std::trivially_copyable<T> // 可平凡拷贝

可调用

1
2
3
std::invocable<F, Args...>           // F 可以用 Args... 调用
std::regular_invocable<F, Args...> // 相等输入产生相等输出
std::predicate<F, Args...> // 返回可转换为 bool

Subsumption(概念包含)

Concept 支持重载决议时的包含关系:更严格的 concept 优先匹配。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 优先。

错误信息对比

SFINAE 时代的错误(数百行模板展开):

1
2
3
4
5
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 时代的错误

1
2
3
4
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

编译器直接告诉你哪个约束失败,而不是展开整个模板实例化链。

与 SFINAE 对比

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 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);

实际应用

约束迭代器类型

1
2
3
4
5
6
7
8
9
10
11
12
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;
}

约束可序列化类型

1
2
3
4
5
6
7
8
9
10
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;
}

约束数值计算

1
2
3
4
5
6
7
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);
}

注意事项

1. Concept 是编译期约束

1
2
3
4
5
template<std::integral T>
void func(T x);

func(42); // OK
func(3.14); // 编译错误,不是运行时检查

2. 约束不满足时的替代方案

1
2
3
4
5
6
7
8
// 使用 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. 避免过度约束

1
2
3
4
5
6
7
8
9
10
11
// 不好:过度约束,限制了可用类型
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 结合

1
2
3
4
5
6
// 简写语法
void process(std::integral auto x);

// 等价于
template<std::integral T>
void process(T x);

与 Ranges 库结合

Ranges 库大量使用 concept 约束算法:

1
2
3
4
5
6
7
8
9
10
#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要求
rangebegin()end()
sized_rangesize() 在常数时间返回
random_access_range支持常数时间随机访问
view可移动、常数时间构造/析构

前置知识:typename 与嵌套类型

在模板中使用嵌套类型(如 T::iterator)时,必须使用 typename 关键字明确告诉编译器这是一个类型而非静态成员。

为什么需要 typename

当编译器遇到 T::iterator 这样的语法时,无法确定这是一个嵌套类型还是一个静态数据成员:

1
2
3
4
template<typename T>
void process(T container) {
T::iterator it; // 编译错误!iterator 是类型还是静态成员?
}

编译器默认假设这是静态数据成员。使用 typename 消除歧义:

1
2
3
4
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) 是指在类(或结构体)内部定义的类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
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 枚举

与成员变量的区别

1
2
3
4
5
6
7
8
9
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. 类型要求(检查嵌套类型存在)

1
2
3
4
template<typename T>
concept HasIterator = requires {
typename T::iterator; // 检查 T 内部是否定义了 iterator 这个类型
};

2. 类型声明(使用嵌套类型)

1
2
3
4
5
template<typename T>
concept Container = requires(T c) {
// 使用 T::iterator 作为类型参与比较
{ c.begin() } -> std::same_as<typename T::iterator>;
};

不需要 typename 的情况

  1. 非依赖类型(类型已知,不依赖模板参数):
1
2
struct MyClass { using Inner = int; };
MyClass::Inner x; // 不需要 typename
  1. 使用 auto 推导(C++11 起):
1
2
3
4
template<typename Container>
void process(const Container& c) {
auto it = c.begin(); // 编译器自动推导,无需显式声明
}

典型编译错误

如果不使用 typename

1
2
3
4
template<typename T>
void bad(T& c) {
T::iterator it = c.begin(); // 错误!
}

错误信息:

1
error: need 'typename' before 'T::iterator' because 'T' is a dependent scope

总结

特性SFINAEConcept
语法晦涩清晰
错误信息冗长明确
组合性困难自然
重载决议手动自动(subsumption)

Concept 不是语法糖,而是模板元编程范式的转变:从"类型替换失败不是错误"到"类型约束显式表达"。当你写模板时,先问自己:这个类型需要满足什么条件?然后用 concept 表达它。