C++20的概念与约束

C++20的概念与约束
https://open.spotify.com/playlist/6zCID88oNjNv9zx6puDHKj?si=2697caec55594412&nd=1&dlsi=1ac4dd1566274a75

现在让我们设好番茄钟放一首好听的音乐开始学习吧 🌈 😋


🧠 导读:这篇文章怎么读?

 

  • 1|背景与动机:先看 C++20 为啥要引入 Concepts,它解决了模板错误难读、SFINAE 写法复杂等问题。
  • 2|基础定义:再看 concept 的基本语法和几种典型定义方式(类型萃取、常量表达式、简单 requires)。
  • 3|requires 四种要求:搞清楚简单要求、类型要求、复合要求、嵌套要求分别长什么样,各自检查什么。
  • 4|使用姿势:对比几种用法:写在模板参数里、前置/尾置 requires、临时约束、约束 auto
  • 5|约束组合:理解如何用 && / || / ! 和参数包折叠把多个约束拼成一个更强的 Concept。
  • 6|偏序规则:看“更受约束的模板优先被选中”的几个重载示例,理解编译器选哪个模板。
  • 7|实战案例:重点看三个例子:HashType 限制 hashmap key、PrintableRange 限制可打印容器、IteratorType 修正 vector 构造歧义。
  • 8|Concepts vs SFINAE:对比同一个需求在 C++17 和 C++20 下的写法,感受可读性的差异。
  • 9|Checklist 总结:最后用 checklist 回顾:写 Concept 前后应该思考什么、怎么优先用标准库概念,以及如何给常用模板加语义标签。

建议:先按上面这 1~9 点快速扫一遍章节标题,心里有个结构,再根据自己当前项目/作业的需求重点看第 4、7、8 部分。


🧩 C++20 概念与约束:让模板真正“说人话”

C++20 概念(Concepts)是这一代标准里最改变模板体验的特性之一。它把原来“靠注释说明”的模板约束,变成了真正的语言级规则,让编译器帮你检查、帮你报更清晰的错。

💡 可以这样理解:Concept = “给模板参数加标签和规则”,谁不符合规则,就在模板实例化之前被拒之门外。


📌 1. 概念是什么?解决了什么问题?

在 C++20 之前:

  • 模板参数只有 typename Tclass T,几乎没有语义信息
  • 约束主要靠 <strong>SFINAE / enable_if / decltype</strong> 等技巧
  • 一旦写错类型,编译器报一大堆“读不懂”的模板错误

在 C++20 之后:

  • concept 定义编译期谓词(返回 bool 的条件)
  • 使用概念直接约束模板参数,不满足就不匹配模板
  • 错误信息会以“哪个 concept 不满足”的形式出现,可读性大幅提升

简单一句话:

Concept = 编译期布尔表达式 + 模板参数语义标签


🧱 2. 如何定义一个概念?(基础语法)

标准形式:

// template-parameter-list 是模板参数列表
// concept-name 是概念名
// constraint-expression 是可求值为 bool 的常量表达式
template <template-parameter-list>
concept concept-name = constraint-expression;

 

课件里的几种典型写法:

#include <type_traits>

template<typename T>
concept Integral = std::is_integral_v<T>;

 

含义:T 必须是整型int/long/char 等)。

template<typename T>
concept SmallType = sizeof(T) <= 4;

 

含义:T 的大小 ≤ 4 字节

template<typename T>
concept Incrementable = requires(T t) {
    ++t; // 检查前缀++是否有效
    t++; // 检查后缀++是否有效
};

 

含义:T 必须支持前后缀自增


🔍 3. requires 表达式的四种形式

课件给了一个框架:

template<typename T>
concept ConceptName = requires (/* parameter-list 可选 */) {
    /* requirement-seq */
};

 

requires \\\{ ... \\\} 里面,可以出现四类“要求”:

只检查“这条表达式能不能编译过”。

#include <concepts>

template<typename T>
concept Addable = requires(T a, T b) {
    a + b;  // 只要 a + b 这句能编译就行
};

 

检查某个嵌套类型是否存在:

template<typename T>
concept HasValueType = requires {
    typename T::iterator;    // 必须有 iterator
    typename T::value_type;  // 必须有 value_type
};

 

同时约束“表达式能否编译 + 返回类型 + noexcept 与否”。

语法:

{ 表达式 } [noexcept] -> 类型约束;

 

示例:

#include <concepts>

template<typename T>
concept ConvertibleAddable = requires(T a, T b) {
    { a + b } -> std::convertible_to<T>;       // a+b 可转成 T
    { a += b } noexcept -> std::same_as<T&>;   // a+=b 不抛异常且返回 T&
};

 

requires 里再写 requires,放更复杂的逻辑条件:

#include <type_traits>

template<typename T>
concept ComplexConcept = requires(T t) {
    requires sizeof(T) <= sizeof(long); // 额外静态布尔条件
    requires std::is_class_v<T>;        // 必须是类类型
};

 


🧪 4. 概念在模板里的几种用法

课件总结了 5 种常见用法:

#include <concepts>
#include <type_traits>

template<class T>
concept Integral = std::is_integral_v<T>;

// 1)模板参数处直接使用 Concept
template<Integral T>
void f1(T x) {
    std::cout << "有 concepts 约束" << std::endl;
}

 

调用:

f1(1);      // OK
f1("xxx"); // 编译期报错:不满足 Integral

 

template<typename T>
requires Integral<T>
void f2(T x) {}

 

template<typename T>
void f3(T x) requires Integral<T> {}

 

效果与 4.2 相同,只是写法不一样。

适用于“只在这个函数体里临时需要的条件”:

template<typename T>
requires requires(T x) { x.size(); x + x; }
void f4(T x) {}

 

含义:T 必须有 <strong>size()</strong> 成员,并且 <strong>x + x</strong> 可行。

void f5(Integral auto x) {}

 

比起泛写 auto,这里要求参数必须是整型。


🔗 5. 约束表达式的逻辑组合

概念本质上是“返回 bool 的约束表达式”,自然可以用逻辑运算:

  • 单个概念:std::integral<T>
  • 单个 requires 表达式
  • 单个 constexpr bool 表达式

所有约束都为真,整体才为真。

#include <concepts>
#include <iostream>

using std::cout;

// 必须既是整数,又能输出

template<typename T>
concept IntegralAndPrintable = std::integral<T> && requires(T t) {
    cout << t;
};

 

至少一个约束为真,整体就为真。

template<typename T>
concept Number = std::integral<T> || std::floating_point<T>;

 

template<typename T>
concept NonPointer = !std::is_pointer_v<T>;

 

对参数包做“约束折叠”:

#include <type_traits>
#include <iostream>

using std::cout;
using std::endl;

template <class T> concept A = std::is_move_constructible_v<T>;
template <class T> concept B = std::is_copy_constructible_v<T>;
template <class T> concept C = A<T> && B<T>;

template <class... T>
requires (C<T> && ...)
void g(T...) {
    cout << "g()->C" << endl;
}

 

这里 (C<T> && ...) 就是对参数包做逻辑与折叠。


⚖️ 6. 约束的偏序规则:重载如何选择“更合适”的模板?

当多个模板都“能匹配”实参时,编译器需要决定选哪个,这里就用到了约束偏序(constraint ordering)

如果约束 A 蕴含约束 B(A ∈ B),那么 A 比 B “更受约束”,编译器会优先选 A。

课件示例:

#include <iostream>
#include <concepts>
#include <type_traits>

// 要求 T 是整型

template<class T>
concept Integral = std::is_integral_v<T>;

// 有 concept 约束的版本
template<Integral T>
void f(T x) {
    std::cout << "有 concepts 约束" << std::endl;
}

// 无约束的版本
template<class T>
void f(T x) {
    std::cout << "无 concepts 约束" << std::endl;
}

int main() {
    f(1);      // 同时匹配两个模板,但 Integral 版本更受约束,被优先选择
    f("xxx"); // 只匹配无约束版本
}

 

再看更复杂一点的:

template <class T> concept A = std::is_move_constructible_v<T>;
template <class T> concept B = std::is_copy_constructible_v<T>;
template <class T> concept C = A<T> && B<T>;

template <class T>
requires A<T>
void g(T x) {
    std::cout << "g()->A" << std::endl;
}

template <class T>
requires C<T>
void g(T x) {
    std::cout << "g()->C" << std::endl;
}

int main() {
    g(1.1);                    // 同时满足 A、C → 选 C 版本
    g(std::unique_ptr<int>()); // 只满足 A → 选 A 版本
}

 

这里 C<T> = A<T> && B<T> 明显比 A<T> 更“强”,所以当两者都能匹配时,编译器选 requires C<T> 那个。


🧰 7. Concepts 在真实代码中的实践

需求:只有 能被 <strong>std::hash</strong> 处理,且结果可转成 <strong>std::size_t</strong> 的类型,才能作为 hashmap 的 key。

#include <cstddef>
#include <concepts>
#include <functional>
#include <string>

// HashType 概念:
// 对于 T 类型的 a,std::hash<T>{}(a) 必须可以编译且结果可转成 std::size_t
template<typename T>
concept HashType = requires(T a) {
    { std::hash<T>{}(a) } -> std::convertible_to<std::size_t>;
};

// 使用概念约束 key 类型

template<HashType K, class HashFunc = std::hash<K>>
class hashmap {
public:
    bool insert(const K& key) {
        size_t hashi = HashFunc()(key);
        // ... 真实插入逻辑
        return true;
    }
};

int main() {
    hashmap<int> hm1;            // OK
    hashmap<std::string> hm2;    // OK
    // hashmap<std::pair<std::string, int>> hm3; // 编译失败:不满足 HashType
}

 

目标:只允许“存放整数且可输出”的容器调用 print_range

#include <concepts>
#include <iostream>
#include <vector>
#include <mutex>

// 对 T 的要求:
// 1)有 begin()/end(),返回 iterator 类型
// 2)value_type 是整型
// 3)value_type 能被 std::cout 输出

template<typename T>
concept PrintableRange = requires(T t) {
    { t.begin() } -> std::same_as<typename T::iterator>;
    { t.end() }   -> std::same_as<typename T::iterator>;
    requires std::integral<typename T::value_type>;
    requires requires(typename T::value_type e) { std::cout << e; };
};

// 只允许 PrintableRange 调用

template<PrintableRange R>
void print_range(const R& r) {
    for (const auto& e : r) {
        std::cout << e << ' ';
    }
    std::cout << 'n';
}

int main() {
    std::vector<int> v{1, 2, 3};
    print_range(v); // OK

    std::vector<std::mutex> vm;
    // print_range(vm); // 编译失败:mutex 既不是整型,也不能直接输出
}

 

用 Concept 限制“假迭代器”匹配到迭代器构造函数

#include <concepts>

// 迭代器概念:只要求支持 ++ 和 *

template<typename T>
concept IteratorType = requires(T it) {
    ++it;
    *it;
};

namespace zdl{

template<class T>
class vector {
public:
    using iterator = T*;

    vector() = default;

    // 迭代器区间构造(加 concept 后更安全)
    template<IteratorType InputIterator>
    vector(InputIterator first, InputIterator last) {
        while (first != last) {
            push_back(*first);
            ++first;
        }
    }

    vector(size_t n, const T& val = T()) {}

    void push_back(const T& x) {}

private:
    iterator _start = nullptr;
    iterator _finish = nullptr;
    iterator _endofstorage = nullptr;
};

} // namespace bit

int main() {
    bit::vector<int> v7(10, 1);
    bit::vector<size_t> v9(10, 1);
}

 

没有 IteratorType 时,bit::vector<int> v7(10, 1); 这种调用有可能被错误匹配为“迭代器区间构造”,加上 Concept 就能让语义更明确,避免二义性。


🆚 8. Concepts 与 SFINAE 的关系

  • SFINAESubstitution Failure Is Not An Error
    • 靠模板替换失败来“静默”排除候选
    • 常见工具:std::enable_ifdecltype
    • 代码冗长、晦涩,错误信息非常难读
  • Concepts
    • 直接用 concept 声明语义约束,如“整型可比较”“可哈希”等
    • 写法接近自然语言,可读性和可维护性都更好
    • 是 SFINAE 的“官方替代方案”,大幅简化模板元编程

对比示例(同一个需求:约束 T 为整型):

// C++17:SFINAE 写法

template<typename T, typename = std::enable_if_t<std::is_integral_v<T>>>
void foo(T x) {}

// C++20:Concept 写法

template<std::integral T>
void foo(T x) {}

 

C++20 的版本语义清晰到“几乎不需要注释”,这就是 Concept 的最大价值之一。


✅ 9. 小结:写 Concept 时可以记住的 checklist

  • 先问自己:我希望 T 满足什么语义?
    • 是否是某类类型?(整型 / 浮点 / 容器 / 迭代器 …)
    • 是否支持某些操作?(+++size()begin()/end() …)
    • 表达式的返回类型、是否 noexcept 有要求吗?
  • 优先用标准库现成概念
    • std::integralstd::floating_pointstd::regularstd::ranges::range
  • 自定义概念时
    • 简单属性 → 用 std::is_xxx_v + requires
    • 操作合法性 → 用 requires (T t) { 表达式… }
    • 复杂组合 → 用逻辑与 / 或 / 非 + 嵌套 requires
  • 在模板中使用时
    • 写在模板参数列表:templat<MyConcept >
    • 或用 requires 子句:template<typename T> requires MyConcept<T>
    • 或约束 autovoid f(MyConcept auto x)

建议你在自己的代码里做一件事