C++新特性梳理-Concepts

1 Concepts 简介

C++20 引入了 Concepts,这是一个革命性的特性,旨在解决模板编程中长期存在的问题,特别是编译错误信息晦涩难懂和对模板参数缺乏明确约束的问题。Concepts 提供了一种直接、清晰的方式来定义模板参数必须满足的接口要求

在 C++20 之前,模板是“隐式契约”:

1
2
3
4
template<typename T>
T add(T a, T b) {
return a + b; // 隐含要求:T 必须支持 operator+
}

如果传入一个不支持 + 的类型(如 std::thread),编译器会报出一长串难以理解的错误信息,追溯到 operator+ 调用失败。

Concepts 的目标就是将这种“隐式契约”变成“显式契约”

1
2
3
4
5
template<typename T>
requires Addable<T> // 显式要求 T 满足 Addable 概念
T add(T a, T b) {
return a + b;
}

现在,如果 T 不满足 Addable,编译器会直接告诉你“T 不满足 Addable 概念”,错误信息清晰明了。


2 如何定义Concept

2.1 一些定义 Concept 的案例

使用 templateconcept 关键字,结合 requires 表达式来定义。

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
#include <concepts>
#include <type_traits>

// 定义一个名为 Addable 的 concept
template<typename T>
concept Addable = requires(T a, T b) {
a + b; // 要求表达式 a + b 是合法的
// 可以添加更多要求,如返回类型
{ a + b } -> std::same_as<T>; // 要求 a + b 的返回类型是 T
};

// 定义一个名为 Printable 的 concept
template<typename T>
concept Printable = requires(T t, std::ostream& os) {
os << t; // 要求 T 支持输出到 ostream
};

// 定义一个更复杂的 concept:Number
template<typename T>
concept Number = std::is_arithmetic_v<T>; // 基于类型特征

// 或者使用 requires 表达式
template<typename T>
concept Integral = requires(T a) {
requires std::is_integral_v<T>;
// 或者更直接地检查操作
{ a / 2 } -> std::convertible_to<T>;
};

2.2 需求的语法

从前面的案例我们观察到, Concept 的定义中,我们使用了 requires 关键字,其后的块中我们定义了多个限制条件, 这些限制条件我们称为需求(requirement), 其有下面几种语法:


2.2.1 简单需求 (Simple Requirements)

语法:

1
expression;

作用:
检查 expression 是否是一个语法上合法的表达式。它只关心表达式能否被正确解析和编译,而不关心其返回类型、值类别或语义。

示例:

1
2
3
4
5
template<typename T>
concept Addable = requires(T a, T b) {
a + b; // 检查 "a + b" 这个表达式是否语法合法
// 即 T 类型的对象是否重载了 operator+
};
  • 如果 T 定义了 operator+,这个需求就满足。
  • 它不关心 a + b 返回的是 TT&int 还是其他任何类型,只要表达式能写出来就行。

2.2.2 类型需求 (Type Requirements)

语法:

1
typename type-id;

作用:
检查某个类型是否存在。常用于检查嵌套类型(如 value_type, iterator)。

示例:

1
2
3
4
5
6
7
8
9
template<typename T>
concept HasValueType = requires {
typename T::value_type; // 检查 T 是否有名为 value_type 的嵌套类型
};

template<typename T>
concept Iterator = requires(T it) {
typename std::iterator_traits<T>::value_type; // 检查迭代器的 value_type
};

2.2.3 复合需求 (Compound Requirements)

这是最复杂也最强大的一种,使用花括号 {} 包围表达式,并可以附加约束。

基本语法:

1
{ expression } [noexcept] [-> type-constraint];

这里的 -> 不是 JavaScript 的箭头函数,而是 C++ requires 表达式中用于指定返回类型约束的语法。

a: 只检查表达式合法性(等同于简单需求,但更灵活

1
{ a + b };

这与 a + b; 效果相同,检查 a + b 是否语法合法。

b: 检查表达式的返回类型

这才是复合需求的精髓:

1
{ a + b } -> std::same_as<T>;

分解解释:

  • { a + b }: 被检查的表达式。
  • ->: 引入对表达式结果的约束。
  • std::same_as<T>: 一个 type-constraint(类型约束),它是一个 concept,要求左侧的类型与 T 完全相同

这意味着:
a + b 不仅要语法合法,其求值结果的类型还必须与 T 是同一个类型。

对比:

1
2
3
4
5
// 只检查语法
a + b; // OK if operator+ exists, regardless of return type

// 检查语法 AND return type
{ a + b } -> std::same_as<T>; // Fails if operator+ returns int, float, or T&, but not exactly T

c: 更多返回类型约束的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
template<typename T>
concept AddableStrict = requires(T a, T b) {
{ a + b } -> std::convertible_to<T>; // 要求返回类型可隐式转换为 T
};

template<typename T>
concept EqualityComparable = requires(T a, T b) {
{ a == b } -> std::convertible_to<bool>; // 要求 a == b 返回可转为 bool 的类型
{ a != b } -> std::same_as<bool>; // 要求 a != b 返回 exactly bool
};

template<typename T>
concept Callable = requires(T f) {
{ f() } -> std::same_as<int>; // 要求 f() 调用返回 int
};

d:检查异常规范

复合需求还可以检查表达式是否 noexcept

1
2
{ a + b } noexcept; // 要求 a + b 是 noexcept 表达式
{ a + b } noexcept -> std::same_as<T>; // 要求 noexcept 且返回 T

2.2.4 嵌套需求 (Nested Requirements)

使用 requires 关键字在内部引入额外的逻辑条件。

1
2
3
4
5
template<typename T>
concept SignedIntegral = requires(T a) {
requires std::integral<T>; // 嵌套要求:T 必须是整型
requires std::is_signed_v<T>; // 嵌套要求:T 必须是有符号的
};

注意:嵌套需求中的 requires 后面直接跟一个布尔常量表达式(通常是另一个 concept 或 bool 值),而不需要分号结束(它本身就是一个声明)。


2.2.5 总结对比表

语法形式 示例 检查内容
简单需求 a + b; a + b 是否是合法表达式
复合需求 (无 ->) { a + b }; 同上,等价于简单需求
复合需求 (带 ->) { a + b } -> std::same_as<T>; a + b 合法 返回类型 完全等于 T
复合需求 (带 ->) { a + b } -> std::convertible_to<T>; a + b 合法 返回类型 可隐式转换为 T
复合需求 (noexcept) { a + b } noexcept; a + b 合法 noexcept
类型需求 typename T::value_type; T 是否有 value_type 嵌套类型
嵌套需求 requires std::integral<T>; 额外的布尔条件

为什么需要 -> 形式?

因为许多操作符的返回类型是有约定的。例如:

  • operator+ 通常返回一个新对象(值类型),而不是引用。
  • operator== 应该返回 bool

使用 { expr } -> constraint; 可以精确地强制这些约定,使你的 concept 更加严谨和符合直觉。而简单的 expr; 只保证了操作符的存在,无法保证其行为的正确性。

因此,当你需要对表达式的返回类型进行约束时,就必须使用复合需求的 -> 形式。

3 使用 Concepts

有多种语法形式:

形式一:函数模板参数列表中使用 (推荐)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 使用 concept 作为模板参数的约束
template<Addable T>
T add(T a, T b) {
return a + b;
}

// 多个 concept 约束 (使用逻辑运算符)
template<Addable T>
requires Printable<T>
void print_add(T a, T b) {
std::cout << add(a, b) << std::endl;
}

// 等价的简化语法 (C++20)
void print_add(Addable auto a, Addable auto b) {
std::cout << (a + b) << std::endl;
}

形式二:类模板约束

1
2
3
4
5
6
7
8
9
10
11
12
template<Printable T>
class Logger {
public:
void log(const T& value) {
std::cout << "Log: " << value << std::endl;
}
};

// 使用
Logger<int> logger1; // OK
Logger<std::string> logger2; // OK
// Logger<std::thread> logger3; // 错误!std::thread 不满足 Printable

形式三:函数参数约束

1
2
3
4
5
6
7
8
// 直接约束函数参数
void process(Integral auto value) {
std::cout << "Processing integer: " << value << std::endl;
}

// 使用
process(42); // OK
process(3.14); // 错误!double 不是 Integral

标准库中的常用 Concepts

C++20 标准库在 <concepts> 头文件中定义了许多预定义的 Concepts,可以直接使用:

  • 基本类型特征相关:

    • std::integral<T>: T 是整型
    • std::floating_point<T>: T 是浮点型
    • std::arithmetic<T>: T 是算术类型(整型或浮点型)
    • std::same_as<T, U>: TU 是同一类型
    • std::derived_from<Base, Derived>: DerivedBase 派生
  • 对象类别相关:

    • std::movable<T>: T 可移动
    • std::copyable<T>: T 可拷贝
    • std::destructible<T>: T 可析构
  • 可调用相关:

    • std::invocable<F, Args...>: F 可以用 Args... 调用
    • std::predicate<F, Args...>: F 是一个返回布尔值的可调用对象
  • 容器和迭代器相关:

    • std::default_constructible<T>
    • std::swappable<T>
    • std::ranges::input_iterator<It> (在 <ranges> 中)

优势

  1. 清晰的编译错误:当类型不满足概念时,错误信息直接指出哪个概念未满足,而不是深入模板实例化的细节。
  2. 更好的代码文档:模板的意图和要求一目了然。
  3. 函数重载和特化:可以根据不同的 concept 选择最合适的函数重载或类特化。
    1
    2
    3
    4
    5
    template<std::integral T>
    void foo(T) { /* 处理整型 */ }

    template<std::floating_point T>
    void foo(T) { /* 处理浮点型 */ }
  4. 提高代码安全性:在编译期强制执行接口契约。

总结

C++20 的 Concepts 是模板编程的重大进步。它通过显式声明模板参数的约束条件,极大地提升了模板代码的可读性、可维护性和错误诊断能力。掌握 Concepts 是现代 C++ 编程的关键技能之一。