跳至内容

C++20 概念与约束

使用概念定义对模板参数的精确要求。使用 requires 子句、简写函数模板和标准库概念编写更简洁的泛型代码。

概念解决的问题

在概念出现之前,模板错误信息晦涩难懂 ── 对没有 operator< 的类型使用 std::sort 可能产生深入标准库实现内部的数页错误文本。概念通过让你预先声明对模板参数的要求来解决这个问题。

概念是一个在编译时求值的命名布尔谓词。它检查一个类型是否满足一组语法和语义要求。当概念未被满足时,编译器会产生清晰、简洁的错误信息,指向未满足的要求 ── 而不是一堵模板实例化回溯的墙。

概念在 C++20 中引入,代表了自模板本身以来对泛型编程最重要的改进。

定义概念

概念使用 concept 关键字定义,后跟一个 requires 表达式来指定类型必须支持哪些操作。requires 表达式可以检查有效的表达式、返回类型、嵌套类型等。

concept_definition.cpp
#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 作为参数类型,这是最简洁的形式。

requires_clause.cpp
#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 的约束。这允许「精炼」── 更具体的概念胜过更一般的概念。

subsumption.cpp
#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 表达式。

requires_requires.cpp
#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' 出现了两次?