在 C++20 中,”约束” (Constraints) 主要指的是 **概念 (Concepts)**。它是 C++ 模板编程中的一个革命性特性,旨在解决传统模板编程中的一些核心痛点。

简单来说,概念 (Concept) 允许你指定模板参数必须满足的条件或要求。

1. 为什么需要概念 (Concepts)?

在 C++20 之前,我们编写泛型代码(模板)时,编译器通常只在实例化模板时才检查模板参数是否“合适”。如果参数不合适,就会导致非常冗长、难以理解的编译错误信息,这被称为 SFINAE (Substitution Failure Is Not An Error) 的副作用。

考虑一个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// C++11/14/17
template <typename T>
void print_sum(T a, T b) {
std::cout << a + b << std::endl;
}

int main() {
print_sum(1, 2); // OK
print_sum(std::string("hello "), std::string("world")); // OK

// print_sum(std::vector<int>{1}, std::vector<int>{2}); // 编译错误!
// 错误信息会非常长,因为 std::vector 没有定义 operator+
return 0;
}

print_sumstd::vector<int> 实例化时,a + b 操作会失败,导致编译器抛出一大堆错误,告诉你 operator+ 不适用于 std::vector。这些错误信息往往是模板内部的实现细节,而不是清晰地指出“你传入的类型不支持加法操作”。

概念就是为了解决这些问题而生:

  • 改善错误信息: 编译器可以在实例化之前检查模板参数是否满足概念要求,并给出清晰、友好的错误信息。
  • 提高可读性: 模板的接口变得自文档化,开发者可以一眼看出模板参数需要具备哪些能力。
  • 简化模板元编程: 替代了复杂的 SFINAE 技术,使条件编译更加直观。
  • 更好的重载解析: 概念可以参与函数模板的重载解析,帮助编译器选择最合适的模板。

2. 概念 (Concepts) 是什么?

一个概念定义了一组编译时可检查的约束,这些约束描述了模板参数必须支持的操作或特性。

核心思想: 不再关注类型本身是什么,而是关注类型能做什么

3. 如何定义和使用概念?

C++20 引入了 concept 关键字和 requires 关键字(requires 表达式和 requires 子句)。

3.1 定义一个概念

使用 concept 关键字定义一个概念。概念的定义体是一个 requires 表达式,其中列出了对模板参数的各种要求。

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>
#include <string>
#include <concepts> // C++20 标准库概念

// 定义一个名为 Addable 的概念
// T 类型必须支持 operator+,并且结果类型可以打印到 ostream
template <typename T>
concept Addable = requires(T a, T b) {
{ a + b } -> std::same_as<T>; // 要求 a + b 的结果类型与 T 相同
{ std::cout << (a + b) } -> std::same_as<std::ostream&>; // 要求 a + b 的结果可以打印
};
  • concept Addable = ...:声明一个名为 Addable 的概念。
  • requires(T a, T b):这是一个 requires 表达式,它定义了对类型 T 的要求。括号内的 T a, T b 声明了用于测试的变量,这些变量不会实际存在于运行时,仅用于编译时检查。
  • { a + b } -> std::same_as<T>;:这是一个**复合要求 (Compound Requirement)**。
    • { a + b }:要求表达式 a + b 是有效的。
    • -> std::same_as<T>:要求表达式 a + b 的结果类型与 T 相同。
  • { std::cout << (a + b) } -> std::same_as<std::ostream&>;:要求 a + b 的结果可以被 std::cout 操作符 << 输出,并且返回类型是 std::ostream&

3.2 使用概念约束模板参数

有几种方式可以使用概念来约束模板参数:

a) 作为模板参数的类型限制 (最常见和推荐)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 使用 Addable 概念约束模板参数 T
template <Addable T> // 只有满足 Addable 概念的类型才能作为 T
void print_sum(T a, T b) {
std::cout << a + b << std::endl;
}

int main() {
print_sum(1, 2); // OK (int 满足 Addable)
print_sum(std::string("hello "), std::string("world")); // OK (std::string 满足 Addable)

// print_sum(std::vector<int>{1}, std::vector<int>{2});
// 编译错误!错误信息清晰地指出:
// error: 'std::vector<int>' does not satisfy 'Addable'
// because the required expression 'a + b' is invalid
// (std::vector does not have operator+)
return 0;
}

b) 使用 requires 子句 (Trailing requires Clause)

当概念定义比较简单,或者不想单独定义一个概念时,可以直接在模板声明后使用 requires 子句。

1
2
3
4
5
6
7
8
9
10
11
12
template <typename T>
void print_sum_v2(T a, T b) requires requires(T x, T y) { x + y; } // 简单的要求:x+y 表达式有效
{
std::cout << a + b << std::endl;
}

// 也可以使用已定义的概念
template <typename T>
void print_sum_v3(T a, T b) requires Addable<T>
{
std::cout << a + b << std::endl;
}

c) 使用 requires 表达式作为 if constexpr 的条件

虽然概念主要用于模板参数约束,但 requires 表达式也可以在 if constexpr 中用于编译时条件判断。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
template <typename T>
void process(T val) {
if constexpr (Addable<T>) { // 编译时检查 T 是否满足 Addable
std::cout << "Type is addable. Sum with itself: " << val + val << std::endl;
} else {
std::cout << "Type is not addable." << std::endl;
}
}

int main() {
process(5); // Type is addable. Sum with itself: 10
process(std::vector<int>{1, 2}); // Type is not addable.
return 0;
}

3.3 标准库概念

C++20 标准库也提供了许多预定义的概念,例如:

  • std::integral:要求类型是整数类型。
  • std::floating_point:要求类型是浮点类型。
  • std::signed_integral, std::unsigned_integral
  • std::same_as<T, U>:要求 TU 是相同的类型。
  • std::totally_ordered<T>:要求类型 T 支持所有比较运算符 (==, !=, <, >, <=, >=)。
  • std::invocable<F, Args...>:要求函数对象 F 可以用 Args... 参数调用。
  • std::ranges::range:要求类型是一个范围 (Ranges)。

你可以直接使用这些概念来约束你的模板:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
#include <concepts> // 包含标准概念

template <std::integral T> // 约束 T 必须是整数类型
T multiply(T a, T b) {
return a * b;
}

int main() {
std::cout << multiply(5, 3) << std::endl; // OK
// std::cout << multiply(5.0, 3.0) << std::endl; // 编译错误!double 不满足 std::integral
return 0;
}

4. 总结概念 (Concepts) 的优势

  • 清晰的接口定义: 模板的意图一目了然,提高了代码的可读性和可维护性。
  • 友好的编译错误: 编译器能给出准确、易懂的错误信息,大大缩短了调试时间。
  • 编译时检查: 在编译阶段就能发现类型不匹配的问题,而不是等到运行时。
  • 更好的重载解析: 概念可以帮助编译器在多个模板重载中选择最匹配的一个。
  • 替代 SFINAE: 避免了复杂的模板元编程技巧,使泛型代码更易于编写和理解。
  • 零运行时开销: 概念是纯粹的编译时特性,不会增加任何运行时性能负担。