声明:本章采纳 https://cppreference.com/ 和 MSVC 来演示。
目录
[一 什么是概念](#一 什么是概念)
[1. 官方文档介绍:](#1. 官方文档介绍:)
[二 概念的定义](#二 概念的定义)
[1 自定义概念的定义](#1 自定义概念的定义)
[2. 库内置的概念](#2. 库内置的概念)
[三 requires子句和表达式](#三 requires子句和表达式)
[3.1 requires 子句](#3.1 requires 子句)
[3.2 requries 表达式](#3.2 requries 表达式)
[四 约束](#四 约束)
[1. 约束的 4 种类型](#1. 约束的 4 种类型)
[2. 约束的偏序规则](#2. 约束的偏序规则)
一 什么是概念
1. 官方文档介绍:
- 类模板、函数模板(包括泛型lambda)和其他模板化函数(通常是类模板的成员)可能与一个约束相关联,该约束指定了对模板参数的要求,可用于选择最合适的函数重载和模板特化。
- 这种需求的命名集合称为概念。每个概念都是一个谓词,在编译时计算,并成为模板接口的一部分,用作约束:
说人话,就是给模板添加一种约束,让不符合的条件的错误信息更直观看懂,且编码方式统一。
C++20 之前限制模板:
- SFINAE:decltype,std::enable_if,if constexpr
- 类型萃取:模板特化
等....
这些方式千奇百怪,有了概念之后遵守统一的语法格式更方便编码和调试。
模板有一个缺点就是报错信息很难看懂,并且只有实例化的时候才会报错,浪费的编译时间,且报错信息难看懂,有了概念,就不需要在编译时识别,更快报错信息更容易看懂。
举个例子:
cppstd::list<int> l = {3, -1, 10}; std::sort(l.begin(), l.end()); // Typical compiler diagnostic without concepts: // invalid operands to binary expression ('std::_List_iterator<int>' and // 'std::_List_iterator<int>') // std::__lg(__last - __first) * 2); // ~~~~~~ ^ ~~~~~~~ // ... 50 lines of output ... // // Typical compiler diagnostic with concepts: // error: cannot call std::sort with std::_List_iterator<int> // note: concept RandomAccessIterator<std::_List_iterator<int>> was not satisfied这段代码使用了算法库的 sort,其参数必须为随机访问迭代器,当传入 list 的时候,实例化之后,std::__lg(__last - __first) * 2 报错,该代码表示对整个容器的大小取个 log ,也就是定义他的最高的递归层数,防止栈溢出,这里报错因为 list 不是随机访问迭代器,不支持 - 所以报错,这对于不熟悉 sort 的底层实现的读者看到这样的报错难以理解。
二 概念的定义
1 自定义概念的定义
cpptemplate < template-parameter-list > concept concept-name attr (optional) = constraint-expression;concept 定义概念的关键字
name 可学可不写,不写就是匿名的概念
constraint - expression 返回值为 bool 的常量表达式
1.1 一个最简单的概念
cpptemplate <class T> concept name = true; template <name T> void fun(T t){} fun("hello world" /* 任意传值 */);概念名可以作为后面所有模板的类型声明,根据传入的类型来判断是否符合定义的概念,即定义的概念右操作数结果是否为 true。
1.2 概念右操作数的写法
concept name = 自定义的常量表达式(自己写的)/库内置的表达式/require 子句/表达式等....
示例:
cpptemplate <class T> // T 是否小于等于 4字节 concept name1 = sizeof(T) <= 4; template <class T> // 是否为 int concept name2 = std::is_integral_v<T>; // template <class T> concept name3 = requires(T val) { // 能被转换成 string std::to_string(val); };
2. 库内置的概念
cpp_EXPORT_STD template <class _Ty> concept integral = is_integral_v<_Ty>; _EXPORT_STD template <class _Ty> concept signed_integral = integral<_Ty> && static_cast<_Ty>(-1) < static_cast<_Ty>(0); _EXPORT_STD template <class _Ty> concept unsigned_integral = integral<_Ty> && !signed_integral<_Ty>; _EXPORT_STD template <class _Ty> concept floating_point = is_floating_point_v<_Ty>;库里的概念本质是拿 <type_traits> 来定义了概念,这些概念可以用作于 requires 子句/表达式内,并且可以不用传类型。
示例:
cpptemplate <class T> concept name4 = requires(T val) { { val } -> std::integral<>; // 是否为 int { val } -> std::same_as<int>; // 是否和 int 相等 };val 作为 std::integral 的第一个参数传递。
并且库里的概念和自定义的概念可以直接作为模板的声明
示例:
cpptemplate <std::integral T> void fun(T t){}
三 requires子句和表达式
前面概念的定义的名称可以省略,本质是借助 requries 来实现的。
3.1 requires 子句
示例:requires 子句
cpp// 可以跟在模板后面 template <std::integral T> requires true && std::integral<T>; void fun1(T t){} // 可以跟在函数后面 template <std::integral T> void fun2(T t) requires true {} // 可以跟 && || 运算符
3.2 requries 表达式
前面的 requires 子句用来定义匿名的概念,而 requires 表达式则是用来定义有名的概念
示例:
cpptemplate <class T> concept name5 = requires(T a,T b) { a + b; // 是否能 + a.size(); // 是否有 size 方法 typename a::iterator; // 是否有内嵌迭代器 { a + b } noexpect -> std::same_as<int> // 表达式是否不会抛异常 && 是否是 int requires std::integral<T>; // 内嵌 requires 如果为 false 整个 requires 为false std::integral<T> // 和上面不一样,该表达式结果不影响整个 requires 的结果 requires requires(T val) // 套娃 { std::to_string(val); }; requires requires(T val) // 套娃 { std::to_string(val); } && requires(T val) { std::to_string(val); }; // 上面 2 种写法:requires 后面又跟一个 requires ,requires 表达式会被转换成 bool // 又因为 requires 后面必须跟一个常量表达式,所以符合语法要求 };requires 表达式函数体内至少要有一个约束条件,参数可选。
可以看出来 requires 表达式的写法有非常多种。
四 约束
上面所说的所有情况都可以说是一种约束,不管是库提供的还是自定义的概念或者表达式都可以算是一种约束。
1. 约束的 4 种类型
原子约束:一个返回值为 bool 的常量表达式,为一个整体,不可分割,如前面写的
concept name = std::integral<T>;
合取:多个原子约束之间带 &&,如:
concept name = std::integral<T> && std::same_as<T,T>;
析取:多个原子约束之间带 ||,如:
concept name = std::integral<T> || std::same_as<T,T>;
折叠扩展约束:传入类型参数包对他们进行约束,如:
cpptemplate <class T> concept name1 = sizeof(T) <= 4; template <class ...T> requires (name1<T> && ...) void fun6() {}
2. 约束的偏序规则
如果有多个匹配的版本,则会优先匹配最严格的版本,如:
cpptemplate <class T> concept tt1 = std::integral<T>; template <class T> concept tt2 = std::integral<T> && requires(T val) { val++; }; template <tt1 T> void fun7() { std::cout << "tt1" << std::endl; } template <tt2 T> void fun7() { std::cout << "tt2" << std::endl; } fun7<int>();tt1虽然匹配,但 tt2 包含了 tt1 也更严格