C++20 概念与约束
使用概念定义对模板参数的精确要求。使用 requires 子句、简写函数模板和标准库概念编写更简洁的泛型代码。
概念解决的问题
在概念出现之前,模板错误信息晦涩难懂 ── 对没有 operator< 的类型使用 std::sort 可能产生深入标准库实现内部的数页错误文本。概念通过让你预先声明对模板参数的要求来解决这个问题。
概念是一个在编译时求值的命名布尔谓词。它检查一个类型是否满足一组语法和语义要求。当概念未被满足时,编译器会产生清晰、简洁的错误信息,指向未满足的要求 ── 而不是一堵模板实例化回溯的墙。
概念在 C++20 中引入,代表了自模板本身以来对泛型编程最重要的改进。
定义概念
概念使用 concept 关键字定义,后跟一个 requires 表达式来指定类型必须支持哪些操作。requires 表达式可以检查有效的表达式、返回类型、嵌套类型等。
#include <concepts>
#include <iostream>
#include <string>
// Define a concept: T must support +, ==, and copy construction
template <typename T>
concept Addable = requires(T a, T b) {
{ a + b } -> std::convertible_to<T>; // a+b must return something convertible to T
{ a == b } -> std::convertible_to<bool>; // must be comparable
requires std::copy_constructible<T>; // nested requirement
};
// Concept for types that have .size() and .empty()
template <typename T>
concept Sizable = requires(const T& t) {
{ t.size() } -> std::convertible_to<std::size_t>;
{ t.empty() } -> std::convertible_to<bool>;
};
// Use the concept to constrain a template
template <Addable T>
T sum_three(T a, T b, T c) {
return a + b + c;
}
template <Sizable Container>
void report_size(const Container& c) {
std::cout << "Size: " << c.size()
<< (c.empty() ? " (empty)" : "") << '\n';
}
int main() {
std::cout << sum_three(1, 2, 3) << '\n'; // 6
std::cout << sum_three(1.5, 2.5, 3.0) << '\n'; // 7.0
std::string s = "hello";
report_size(s); // Size: 5
std::vector<int> v{1, 2, 3};
report_size(v); // Size: 3
}Requires 子句与简写语法
有多种方式将概念应用于模板。requires 子句放在模板参数列表之后或函数签名之后。简写函数模板语法使用 Concept auto 作为参数类型,这是最简洁的形式。
#include <concepts>
#include <iostream>
// Method 1: Concept as template parameter constraint
template <std::integral T>
T gcd(T a, T b) {
while (b != 0) {
T temp = b;
b = a % b;
a = temp;
}
return a;
}
// Method 2: requires clause after template parameters
template <typename T>
requires std::integral<T>
T lcm(T a, T b) {
return (a / gcd(a, b)) * b;
}
// Method 3: Trailing requires clause
template <typename T>
T absolute(T value) requires std::signed_integral<T> {
return value < 0 ? -value : value;
}
// Method 4: Abbreviated function template (Concept auto)
void print_integral(std::integral auto value) {
std::cout << "Integral: " << value << '\n';
}
// Concept auto works for return types too (C++20)
std::integral auto compute() {
return 42; // must return an integral type
}
int main() {
std::cout << gcd(12, 8) << '\n'; // 4
std::cout << lcm(12, 8) << '\n'; // 24
std::cout << absolute(-42) << '\n'; // 42
print_integral(100); // Integral: 100
// print_integral(3.14); // ERROR: constraint not satisfied
}概念组合与标准概念
概念通过 &&(合取)和 ||(析取)自然组合。标准库在 中提供了丰富的概念集:
核心语言概念:
- std::same_as ── T 和 U 是相同类型
- std::derived_from ── Derived 派生自 Base
- std::convertible_to ── From 可以隐式转换为 To
- std::integral、std::floating_point、std::signed_integral
比较概念:
- std::equality_comparable ── 支持 == 和 !=
- std::totally_ordered ── 支持 <、>、<=、>=、==、!=
对象概念:
- std::movable、std::copyable、std::semiregular、std::regular
可调用概念:
- std::invocable ── F 可以用 Args 调用
- std::predicate ── F 是返回 bool 的谓词
迭代器概念():
- std::input_iterator、std::forward_iterator、std::random_access_iterator、std::contiguous_iterator
范围概念():
- std::ranges::range、std::ranges::sized_range、std::ranges::random_access_range
概念包含与重载决议
当多个受约束的重载匹配时,编译器使用概念包含来选择最具体的那个。概念 A 包含概念 B,如果 A 的约束在逻辑上蕴含 B 的约束。这允许「精炼」── 更具体的概念胜过更一般的概念。
#include <concepts>
#include <iostream>
// Concept hierarchy: sorted_range is "more specific" than range
template <typename T>
concept Printable = requires(std::ostream& os, const T& t) {
{ os << t };
};
template <typename T>
concept PrintableNumber = Printable<T> && std::integral<T>;
// Less constrained overload
template <Printable T>
void display(const T& value) {
std::cout << "[generic] " << value << '\n';
}
// More constrained overload — subsumes Printable
template <PrintableNumber T>
void display(const T& value) {
std::cout << "[number] " << value << '\n';
}
int main() {
display(42); // [number] 42 — PrintableNumber is more specific
display(std::string("hi")); // [generic] hi — only Printable matches
display(3.14); // [generic] 3.14 — not integral
}requires requires 模式
有些不寻常的 requires requires 出现在你直接在 requires 子句中使用临时 requires 表达式而不定义命名概念时。第一个 requires 引入约束子句;第二个 requires 开始 requires 表达式。
#include <iostream>
#include <string>
// Ad-hoc constraint without naming a concept
template <typename T>
requires requires(T a, T b) {
{ a + b } -> std::same_as<T>;
}
T add(T a, T b) {
return a + b;
}
// Equivalent named concept (preferred for readability)
template <typename T>
concept AddableToSelf = requires(T a, T b) {
{ a + b } -> std::same_as<T>;
};
template <AddableToSelf T>
T add_named(T a, T b) {
return a + b;
}
int main() {
std::cout << add(1, 2) << '\n'; // 3
std::cout << add(std::string("a"), std::string("b")) << '\n'; // ab
std::cout << add_named(10, 20) << '\n'; // 30
}设计概念时:
- 根据类型是什么来命名概念,而非函数需要什么 ── 使用 Sortable 而非 HasLessThan。概念应该捕获语义类别。
- 优先使用标准库概念 ── 在编写自己的概念之前,检查 、 或 中是否已有你需要的。
- 保持概念小而可组合 ── 用 && 组合小概念,而非创建庞大的要求。
- 使用命名概念而非临时的 requires requires ── 命名形式更具可读性和可复用性。
- 在注释中记录语义要求 ── 概念检查语法,而非语义。如果你要求 operator+ 具有交换律,请在注释中说明。
- 概念是约束模板参数的命名布尔谓词,提供清晰的错误信息
requires表达式检查有效的表达式、返回类型和嵌套要求- 四种应用概念的方式:模板参数上的约束、requires 子句、尾随 requires、或
Concept auto - 概念通过
&&和||组合;包含关系选择最受约束的重载 - 标准库在
、和中提供概念 requires requires是临时的内联约束 ── 为了可读性应优先使用命名概念
测验 — 检验你的知识
(20 经验值)1. `void print(std::integral auto value)` 是什么意思?
2. 如果概念 A 定义为 `concept A = B<T> && C<T>`,当 B 约束和 A 约束的重载都匹配时会怎样?
3. 为什么 `requires requires(T a) { a.size(); }` 中关键字 'requires' 出现了两次?