C++20 Concepts:约束与概念的现代范式

C++20 Concepts:约束与概念的现代范式

C++进阶计划 · Day 09 | 预计学习时长:2小时

引言

为什么这个知识点重要

C++20 Concepts 是自 C++11 模板元编程诞生以来最重要的语言特性之一。它将模板约束从"隐式契约"提升为"显式声明",让编译器在模板实例化前就能提供有意义的错误信息,将 SFINAE 的隐式失败转变为 Concepts 的显式约束。

对于有 7 年经验的 C++ 工程师,你一定经历过这样的痛苦:

cpp 复制代码
// SFINAE 时代的"天书"错误信息
error: no type named 'type' in 'std::enable_if<...>'
// 整整 87 行的模板展开错误

Concepts 让我们写出的代码意图清晰,错误信息简洁明确:

cpp 复制代码
error: constraints not satisfied
note: concept 'std::integral' was not satisfied

与前面内容的关联

Concepts 并非凭空出现,它是建立在以下知识之上的集大成者:

前置知识 Concepts 中的体现
模板基础 (Day 21) 概念本身就是模板
SFINAE (Day 22) Concepts 底层仍基于 SFINAE
Type Traits (Day 23) 标准库 Concepts 大多基于 type traits
requires 子句 (Day 7, if constexpr) requires 表达式是定义概念的核心语法

核心概念

1. Concepts 的本质

**概念(Concept)**是一个编译期的谓词,它指定了对模板参数的要求。一个类型要么满足概念,要么不满足------这是编译期的布尔关系。

cpp 复制代码
// 概念定义语法 (C++20)
template <typename T>
concept MyIntegral = std::integral<T> && sizeof(T) >= 2;

关键理解:Concepts 不是类,不是结构体,不是类型------它是编译期的布尔常量。

2. 三种定义方式

2.1 使用 concept 关键字(推荐)
cpp 复制代码
// 基本形式
template <typename T>
concept Hashable = requires(T a) {
    std::hash<T>{}(a);
};

// 带约束的组合概念
template <typename T>
concept SequentialContainer = 
    requires(T c) {
        typename T::value_type;
        typename T::iterator;
        requires std::movable<T>;
        { c.begin() } -> std::same_as<typename T::iterator>;
        { c.end() } -> std::same_as<typename T::iterator>;
    };
2.2 使用 requires 子句(约束表达式)
cpp 复制代码
template <typename T>
    requires std::integral<T> && (sizeof(T) >= 4)
T multiply(T a, T b) {
    return a * b;
}
2.3 简化的约束语法(C++20 最强大特性)
cpp 复制代码
// 约束模板参数,等价于上面的 requires 子句
std::integral auto add(std::integral auto a, std::integral auto a2) {
    return a + a2;
}

3. requires 表达式详解

requires 表达式是定义 Concepts 的核心语法,有四种形式:

3.1 简单要求(Simple Requirements)
cpp 复制代码
template <typename T>
concept Addable = requires(T a, T b) {
    a + b;      // T 必须支持 operator+
    a - b;      // T 必须支持 operator-
};
3.2 类型要求(Type Requirements)
cpp 复制代码
template <typename T>
concept Container = requires {
    typename T::value_type;      // T 必须有嵌套类型 value_type
    typename T::iterator;         // T 必须有嵌套类型 iterator
    typename T::const_iterator;  // T 必须有嵌套类型 const_iterator
};
3.3 复合要求(Compound Requirements)
cpp 复制代码
template <typename T>
concept StackLike = requires(T a, const T b) {
    // 表达式 a.push(b) 必须是 void 类型
    { a.push(b) } -> std::same_as<void>;
    // 表达式 a.size() 必须是 std::size_t 类型
    { a.size() } -> std::same_as<std::size_t>;
    // 表达式 a.empty() 必须是布尔类型
    { a.empty() } -> std::convertible_to<bool>;
};
3.4 嵌套要求(Nested Requirements)
cpp 复制代码
template <typename T>
concept RandomAccessContainer = requires(T a, std::size_t n) {
    // 首先 a[n] 返回的必须是 T::value_type
    // 然后这个类型必须满足可默认构造
    { a[n] } -> std::same_as<typename T::value_type>;
    requires std::default_initializable<typename T::value_type>;
};

4. 标准库 Concepts 全览

C++20 标准库提供了丰富的预定义 Concepts:

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                    C++20 标准库 Concepts                         │
├─────────────────┬───────────────────────────────────────────────┤
│ 核心语言 Concepts│                                                │
├─────────────────┼───────────────────────────────────────────────┤
│ same_as         │ T 和 U 是同一类型                               │
│ derived_from    │ D public/protected 继承自 B                    │
│ convertible_to  │ T 可转换为 U,且不丢失信息                      │
│ common_ref_with │ T 和 U 有共同的引用类型                         │
│ assignable_from │ T 可从 U 赋值                                  │
│ swappable       │ T 对象可交换                                   │
└─────────────────┴───────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│ 概念族           │ 说明                                           │
├─────────────────┼───────────────────────────────────────────────┤
│ object_concepts │ movable, copyable, assignable, swappable      │
│ callable_concepts│ invocable, regular_invocable, predicate      │
│ comparison_concepts│ equality_comparable, totally_ordered       │
│ arithmetic_concepts│ addable, subtractable, multipliable...     │
└─────────────────┴───────────────────────────────────────────────┘

5. Concepts 的语义约束 vs 语法约束

这是区分优秀 Concepts 和普通 Concepts 的关键:

cpp 复制代码
// ❌ 错误的做法:只检查语法,不检查语义
template <typename T>
concept BadAddable = requires(T a, T b) {
    a + b;  // 只检查 operator+ 是否存在
};

// ✅ 正确的做法:检查语义约束
template <typename T>
concept Addable = requires(T a, T b) {
    a + b;
    a += b;      // 加法应该与增量赋值一致
}
&& requires(const T a, const T b) {
    { a + b } -> std::convertible_to<T>;
    // 加法结果应该仍是 T 或可转换为 T
}
&& std::copyable<T>;  // 可加对象应该可拷贝(通常)

代码实战

实战 1:手写一个类型无关的排序算法

cpp 复制代码
#include <concepts>
#include <vector>
#include <iostream>
#include <algorithm>

// 约束 1:类型必须是可比较有序的
template <typename T>
concept Ordered = std::totally_ordered<T>;

// 约束 2:类型必须支持交换
template <typename T>
concept Swappable = std::swappable<T>;

// 约束 3:容器必须支持随机访问迭代器
template <typename C>
concept RandomAccessRange = 
    std::ranges::random_access_range<C> &&
    Ordered<std::ranges::range_value_t<C>> &&
    Swappable<std::ranges::range_value_t<C>>;

// 使用 Concepts 约束的快速排序
template <RandomAccessRange R>
void quicksort(R& range) {
    quicksort_impl(range, 
                   std::ranges::begin(range), 
                   std::ranges::end(range));
}

template <RandomAccessRange R>
    requires Ordered<std::ranges::range_reference_t<R>>
void quicksort_impl(R& range, 
                    std::ranges::iterator_t<R> first, 
                    std::ranges::iterator_t<R> last) {
    // 递归终止条件
    if (std::ranges::distance(first, last) <= 1) {
        return;
    }
    
    // 分区:选择最后一个元素作为 pivot
    auto pivot_iter = std::ranges::prev(last);
    auto pivot_value = *pivot_iter;
    
    // 分区逻辑
    auto middle = std::partition(first, pivot_iter, 
        [&pivot_value](const auto& elem) {
            return elem < pivot_value;
        });
    
    // 将 pivot 放到正确位置
    std::ranges::iter_swap(middle, pivot_iter);
    
    // 递归排序
    quicksort_impl(range, first, middle);
    quicksort_impl(range, std::ranges::next(middle), last);
}

// 测试
struct Point {
    int x, y;
    
    // 为 Point 实现比较运算符
    friend bool operator<(const Point& a, const Point& b) {
        return a.x < b.x || (a.x == b.x && a.y < b.y);
    }
    
    friend bool operator==(const Point& a, const Point& b) {
        return a.x == b.x && a.y == b.y;
    }
    
    // C++20 spaceship 运算符自动生成所有比较操作
    auto operator<=>(const Point&) const = default;
};

int main() {
    // 测试整数数组
    std::vector<int> ints = {64, 34, 25, 12, 22, 11, 90};
    std::cout << "Before sorting: ";
    for (auto i : ints) std::cout << i << " ";
    std::cout << "\n";
    
    quicksort(ints);
    
    std::cout << "After sorting:  ";
    for (auto i : ints) std::cout << i << " ";
    std::cout << "\n\n";
    
    // 测试 Point 数组
    std::vector<Point> points = {{3, 1}, {1, 4}, {2, 2}, {5, 0}};
    std::cout << "Before sorting points: ";
    for (auto& p : points) std::cout << "(" << p.x << "," << p.y << ") ";
    std::cout << "\n";
    
    quicksort(points);
    
    std::cout << "After sorting points:  ";
    for (auto& p : points) std::cout << "(" << p.x << "," << p.y << ") ";
    std::cout << "\n";
    
    // 尝试用不支持比较的类型实例化(将产生清晰的编译错误)
    // struct NotComparable {};
    // std::vector<NotComparable> broken = {...};
    // quicksort(broken);  // 编译错误:NotComparable 不满足 Ordered concept
    
    return 0;
}

实战 2:Concepts 与 Policy-Based Design 结合

cpp 复制代码
#include <concepts>
#include <iostream>
#include <vector>
#include <numeric>

// ============ 策略概念定义 ============
template <typename Operation>
concept NumericOperation = requires(Operation op, double a, double b) {
    { op(a, b) } -> std::convertible_to<double>;
};

template <typename Formatter>
concept ResultFormatter = requires(Formatter fmt, double value) {
    { fmt.format(value) } -> std::convertible_to<std::string>;
};

// ============ 具体策略实现 ============
struct AddOp {
    double operator()(double a, double b) const { return a + b; }
};

struct MultiplyOp {
    double operator()(double a, double b) const { return a * b; }
};

struct PowerOp {
    double operator()(double a, double b) const { return std::pow(a, b); }
};

struct ScientificFormatter {
    std::string format(double value) const {
        return std::format("{:.6e}", value);
    }
};

struct SimpleFormatter {
    std::string format(double value) const {
        return std::format("{:.2f}", value);
    }
};

// ============ Concepts 约束的计算器 ============
template <NumericOperation Op, ResultFormatter Formatter>
class Calculator {
public:
    Calculator(Op op, Formatter fmt) : operation_(op), formatter_(fmt) {}
    
    double calculate(const std::vector<double>& values) const {
        if (values.empty()) return 0.0;
        
        double result = values.front();
        for (size_t i = 1; i < values.size(); ++i) {
            result = operation_(result, values[i]);
        }
        return result;
    }
    
    std::string formatResult(const std::vector<double>& values) const {
        return formatter_.format(calculate(values));
    }
    
private:
    Op operation_;
    Formatter formatter_;
};

// 约束推导:不需要显式指定模板参数
template <NumericOperation Op, ResultFormatter Formatter>
Calculator(Op, Formatter) -> Calculator<Op, Formatter>;

int main() {
    std::vector<double> data = {1.0, 2.0, 3.0, 4.0, 5.0};
    
    // 组合不同策略
    Calculator calc1{AddOp{}, ScientificFormatter{}};
    Calculator calc2{MultiplyOp{}, SimpleFormatter{}};
    Calculator calc3{PowerOp{}, ScientificFormatter{}};
    
    std::cout << "Data: ";
    for (auto v : data) std::cout << v << " ";
    std::cout << "\n\n";
    
    std::cout << "Sum (scientific): " << calc1.formatResult(data) << "\n";
    std::cout << "Product (simple): " << calc2.formatResult(data) << "\n";
    std::cout << "2^mean (scientific): " << calc3.formatResult(data) << "\n";
    
    // 错误示例:使用不满足概念的类型
    // struct BadOp { int operator()(int, int); };  // 返回 int 不是 double
    // Calculator bad{BadOp{}, SimpleFormatter{}};  // 编译错误!
    
    return 0;
}

实战 3:requires 表达式的高级用法

cpp 复制代码
#include <concepts>
#include <type_traits>
#include <iostream>
#include <memory>

// ============ 检测成员存在性 ============
template <typename T, typename... Args>
concept ConstructibleFrom = std::constructible_from<T, Args...>;

// 检测是否有 reserve(size_t) 和 push_back(T) 成员
template <typename C, typename T>
concept BackInsertable = requires(C& c, T val) {
    c.reserve(std::size_t{1});    // 可预留空间
    c.push_back(val);             // 可从尾部插入
};

// 检测是否有 at(size_t) 运算符
template <typename C, typename T>
concept RandomIndexable = requires(const C& c, std::size_t idx) {
    { c.at(idx) } -> std::same_as<T&>;
};

// ============ 可组合的 Constraints ============
template <typename T>
concept ArithmeticType = 
    std::integral<T> || std::floating_point<T>;

template <typename T>
concept BitwiseType = 
    std::integral<T> && requires(T a, T b) {
        { a & b } -> std::same_as<T>;
        { a | b } -> std::same_as<T>;
        { a ^ b } -> std::same_as<T>;
        { ~a } -> std::same_as<T>;
        { a << b } -> std::same_as<T>;
        { a >> b } -> std::same_as<T>;
    };

// ============ 条件 Concepts ============
template <typename T>
concept StringLike = 
    std::same_as<T, std::string> ||
    std::same_as<T, std::wstring> ||
    std::same_as<T, std::u8string> ||
    std::same_as<T, std::u16string> ||
    std::same_as<T, std::u32string> ||
    requires(T s) {
        { s.c_str() } -> std::convertible_to<const char*>;
        { s.length() } -> std::convertible_to<std::size_t>;
    };

// ============ 实践:类型安全的工厂函数 ============
class Widget {
public:
    virtual ~Widget() = default;
    virtual void draw() const = 0;
};

class Button : public Widget {
public:
    void draw() const override { std::cout << "Drawing Button\n"; }
};

class TextBox : public Widget {
public:
    void draw() const override { std::cout << "Drawing TextBox\n"; }
};

// 要求 T 必须派生自 Widget
template <typename T>
concept WidgetDerived = std::derived_from<T, Widget>;

template <WidgetDerived T>
std::unique_ptr<T> createWidget() {
    return std::make_unique<T>();
}

// 测试
int main() {
    // 正常工作
    auto btn = createWidget<Button>();
    auto txt = createWidget<TextBox>();
    btn->draw();
    txt->draw();
    
    // 编译期错误:int 不派生自 Widget
    // auto bad = createWidget<int>();  // error!
    
    // BitwiseType 示例
    static_assert(BitwiseType<int>);
    static_assert(BitwiseType<unsigned>);
    static_assert(!BitwiseType<double>);  // double 不支持位运算
    
    // StringLike 示例
    static_assert(StringLike<std::string>);
    static_assert(!StringLike<int>);
    
    return 0;
}

常见陷阱与最佳实践

陷阱 1:概念过于宽松

cpp 复制代码
// ❌ 错误:只检查语法存在,不检查语义
template <typename T>
concept BadContainer = requires(T c) {
    typename T::value_type;
    { c.begin() };
    { c.end() };
    { c.size() };
};

// 任何有这些成员的类型都满足,包括:
struct FakeContainer {
    using value_type = int;
    int begin() { return 0; }
    int end() { return 0; }
    double size() { return 1.5; }  // size() 返回 double!这在语义上是错误的
};

最佳实践 :使用语义约束,使用 -> 返回类型约束。

cpp 复制代码
// ✅ 正确:严格约束返回类型
template <typename T>
concept Container = requires(T c) {
    typename T::value_type;
    typename T::iterator;
    typename T::const_iterator;
    { c.begin() } -> std::same_as<typename T::iterator>;
    { c.end() } -> std::same_as<typename T::iterator>;
    { c.size() } -> std::same_as<typename T::size_type>;
    { c.empty() } -> std::convertible_to<bool>;
};

陷阱 2:在概念定义中使用实现细节

cpp 复制代码
// ❌ 错误:暴露了内部实现
template <typename T>
concept BadHashable = requires(T t) {
    { std::hash<T>{}(t) } -> std::convertible_to<std::size_t>;
};

// ✅ 正确:使用类型要求即可
template <typename T>
concept Hashable = requires(T t) {
    typename std::hash<T>;  // std::hash<T> 必须特化存在
    { std::hash<T>{}(t) } -> std::convertible_to<std::size_t>;
};

陷阱 3:忽略概念的可组合性

cpp 复制代码
// ❌ 错误:重复定义类似概念
template <typename T>
concept AddableInt = std::integral<T> && requires(T a, T b) { a + b; };

template <typename T>
concept AddableFloat = std::floating_point<T> && requires(T a, T b) { a + b; };

// ✅ 正确:设计可组合的概念
template <typename T>
concept Addable = requires(T a, T b) {
    { a + b } -> std::same_as<T>;
};

template <typename T>
concept Numeric = std::movable<T> && Addable<T>;

// 然后组合使用
template <typename T>
    requires Numeric<T>
T sum(T a, T b) { return a + b; }

陷阱 4: Concepts vs requires 子句 混淆使用

cpp 复制代码
// ❌ 错误场景:混用导致冗余
template <typename T>
    requires std::integral<T>  // 第一个约束
concept MyIntegral = requires(T t) {
    requires std::integral<T>;  // 重复约束!
};

最佳实践

场景 推荐方式
定义通用概念 concept 关键字
约束函数模板 requires 子句或简化的 auto 语法
条件继承/特化 requires 子句

陷阱 5:可变参数 Concepts 的陷阱

cpp 复制代码
// ❌ 错误:直接用参数包展开
template <typename... Ts>
concept VariadicAddable = requires(Ts... args) {
    (args + ...);  // 这只在 Ts... 非空时有效!
};

// ✅ 正确:分离空包和非空包情况
template <typename... Ts>
concept VariadicAddable = (sizeof...(Ts) >= 2) && requires(Ts... args) {
    (args + ...);
};

进阶思考

1. Concepts 与 Concepts 库

C++20 Ranges 是 Concepts 应用最广泛的领域:

cpp 复制代码
// 使用标准库的 range concepts
#include <ranges>

template <std::ranges::input_range R>
    requires std::ranges::viewable_range<R>
class MyProcessor {
    // ...
};

// 常用 ranges concepts 速查
std::ranges::range           // 有 begin/end 的类型
std::ranges::view            // 不拥有数据的视图
std::ranges::input_range     // 只读遍历
std::ranges::forward_range   // 可多次遍历
std::ranges::bidirectional_range  // 可前后移动
std::ranges::random_access_range // O(1) 下标访问
std::ranges::contiguous_range     // 内存连续

2. Concepts 与编译期多态

Concepts 让我们可以同时使用编译期多态和运行时多态:

cpp 复制代码
// 编译期多态:通过 Concepts 约束
template <typename T>
    requires std::integral<T>
T process(T value) {
    // 编译期选择实现
}

// 运行时多态:通过虚函数
class Processor {
public:
    virtual ~Processor() = default;
    virtual void process() = 0;
};

// 组合使用
template <std::derived_from<Processor> ConcreteProcessor>
class SmartProcessor : public ConcreteProcessor {
    // 同时使用 Concepts 约束和运行时多态
};

3. C++23 和未来展望

C++23 对 Concepts 的增强:

cpp 复制代码
// C++23: 显式对象参数(deducing this)
template <typename Self>
    requires std::integral<Self>
auto operator+(const Self& self, const Self& other) {
    return self + other;  // 更自然的语法
}

// C++23: std::expected (结合 Concepts)
template <typename T, typename E>
class Expected {
    // 可以用 Concepts 约束 T 和 E 的类型
};

参考资源

标准文档

权威书籍

  • 《C++20 Concepts: The Principles and Practice of Generic Programming》 - Bjarne Stroustrup 等
  • 《Programming Constraints》 - Microsoft Patterns & Practices
  • 《Ranges for the Standard Library》 - Tristram Macdonald

高质量博客

标准库实现参考


下期预告:Day 10 - C++20 Ranges(上):View/Range 模型、管道操作符、惰性求值、常用 views

相关推荐
可爱系程序猿5 小时前
mfc90.dll 缺失如何处理?VC++ 2008 运行库与并行程序集排查记录
程序人生·电脑·c++20
code_pgf19 小时前
C++11 / C++14 / C++17 / C++20 新特性总结
c++·c++20
C语言Plus2 天前
C++ SqlBuilder一个简单、灵活且类型安全的 C++ SQL 构建器库
数据库·sql·安全·c++20
Billy121386 天前
结构化绑定与 if constexpr
开发语言·c++进阶学习
CHANG_THE_WORLD12 天前
C++20 协程从零入门:用 ASCII 图彻底理解 `co_await`、协程帧与异步执行
c++20
无限的鲜花23 天前
协程本质是函数加状态机——零基础深入浅出 C++20 协程
c++·算法·c++20
十五年专注C++开发1 个月前
全面深入了解C++20 范围库(std::ranges)
c++20·管道·哨兵·视图·ranges
小小龙学IT2 个月前
C++20 协程深度解析:从原理到高性能异步框架实战
junit·c++20
楼田莉子2 个月前
C++20新特性:协程
开发语言·c++·后端·学习·c++20