约束
约束是逻辑操作和操作数的序列,它指定了对模板实参的要求。
-
合取
两个约束的合取是用
&&
运算符。template <typename T> concept luser = std::integral<T> && std::signed_integral<T>;
需要约束同时满足两个要求。合取判断的时候,使用短路检测,即对
std::integral<T>
结果为false时,不再对std::signed_integral<T>
进行判断。
-
析取
两个约束的析取是用
||
运算符。template <typename T> concept luser = std::integral<T> || std::signed_integral<T>;
需要约束满足任意一个要求。析取判断的时候,使用短路检测,即对
std::integral<T>
结果为ture时,不再对std::signed_integral<T>
进行判断。
-
原子约束
原子约束,不会是逻辑与(AND)或者逻辑或(OR)表达式(它们分别构成析取和合取)。
在替换后的类型必须严格为 bool。不能有任何转换:
template <typename T>
requires(S<T>{})
void f(T){}; // #1
void f(int) {
cout << "1" << endl;
}; // #2
void g() {
f(10); // 错误:检查 #1 时 S<int>{} 不具有 bool 类型,
// 尽管 #2 能更好地匹配
}
如果两个原子约束由在 源码层面上相同 的表达式组成,且它们的形参映射等价,那么认为它们等同。
template <typename T, typename U>
concept luser1 = std::is_same_v<T, U>;
template <typename T, typename U>
concept luser2 = std::is_same_v<U, T>;
luser1
和luser2
是两个不同的概念,因为在源码层面上不同,一个是<T, U>
,一个是<U, T>。
template <typename T, luser1<T> U>
void fun() {
}
template <typename T, luser2<T> U>
void fun() {
}
int main(int argc, char* argv[]) {
fun(1, 2); // 会有编译错误 找不到匹配的函数因为2个约束均可满足
return 0;
}
gcc13.2.0编译报错:
no matching function for call to ‘funtest(int, int)’
requires表达式
要求产生描述约束的bool类型的纯右值表达式。
语法:其中,参数列表可以没有。
requires( 参数列表 ) { 要求序列 }
简单要求
不以 requires
关键字开头的不求值表达式,仅仅对表达式做正确性检测。
把类型代入约束中,符合语法返回true,不符合语法返回false。
template <typename T>
concept loser = requires(T a, T b) {
a + b;
};
该约束 loser
,需要保证 a + b
为正确表达式即可,基本类型或者重载 operator+
。
struct Test {
void operator+(Test&){};
};
int main(int argc, char* argv[]) {
std::cout << std::boolalpha << loser<int> << "\n"; // true
std::cout << std::boolalpha << loser<Test> << "\n"; // true
std::cout << std::boolalpha << loser<string> << "\n";
return 0;
}
类型要求
要求关键字 typename
后面接一个可选的类型名称。要求指定类型名称是有效的:可以用来验证某个命名的嵌套类型是否存在,或者某个类模板特化是否命名了某个类型,或者某个别名模板特化是否命名了某个类型。命名类模板特化的类型要求并不要求该类型是完整的。
template <typename T>
using Ref = T&;
template <typename T>
concept luser = requires {
typename T::value;
typename Ref<T>;
};
复合要求
{ 表达式 } noexcept(可选) 返回类型要求(可选);
替换和语义约束检测按照顺序进行:
- 模板参数(若存在)被替换
- 如果使用noexcept,一定不能潜在抛出异常
- 如果返回类型要求存在,则:
- 模板参数被替换到返回类型要求。
decltype((expression))
必须满足 类型约束 的约束。否则,被包含的requires 表达式
是false
。
template<typename T>
concept C2 = requires(T x)
{
// 表达式 *x 必须合法
// 并且 类型 T::inner 必须存在
// 并且 *x 的结果必须可以转换为 T::inner
{*x} -> std::convertible_to<typename T::inner>;
// 表达式 x + 1 必须合法
// 并且 std::same_as<decltype((x + 1)), int> 必须满足
// 即, (x + 1) 必须为 int 类型的纯右值
{x + 1} -> std::same_as<int>;
// 表达式 x * 1 必须合法
// 并且 它的结果必须可以转换为 T
{x * 1} -> std::convertible_to<T>;
};
嵌套要求
嵌套要求具有如下形式
requires
约束表达式;
template <typename T>
concept luser3 = requires(T a) {
requires luser<T>;
};
在约束表达式中,嵌套约束表达式。
requires子句
使用 requires
引用 requires子句
,可以对模板参数、函数声明进行约束。
template <typename T>
void fun(T a)
requires(std::output_iterator<T, std::iter_value_t<T>>)
{
}
这种情况下,关键词 requires
必须后随某个常量表达式(因此可以写成 requires true
),但这是为了使用一个具名概念(如上例),具名概念的一条合取/析取,或者一个 requires 表达式。
表达式必须具有下列形式之一:
- 初等表达式:
requires表达式
或任何带括号的表达式 - 以运算符
&&
联结的初等表达式的序列 - 以运算符
||
联结的前述表达式的序列
约束偏序
首先转换 P
为析取范式并转换 Q
为合取范式。当且仅当以下情况下 P
归入 Q
:
P
的析取范式中的每个析取子句都能归入Q
的合取范式中的每个合取子句,其中- 当且仅当析取子句中存在不可分割约束
U
而合取子句中存在原子约束V
,使得U
归入V
时,析取子句能归入合取子句; - 当且仅当使用所述的规则判定为等同时,称不可分割约束
A
能归入原子约
- 当且仅当析取子句中存在不可分割约束
简单来说就是P比Q更受约束,或者说约束条件更多。
template<typename T>
concept Decrementable = requires(T t) { --t; };
template<typename T>
concept RevIterator = Decrementable<T> && requires(T t) { *t; };
// RevIterator 能归入 Decrementable,RevIterator比Decrementable更受约束,但反之不行
template<Decrementable T>
void f(T); // #1
template<RevIterator T>
void f(T); // #2,比 #1 更受约束
f(0); // int 只满足 Decrementable,选择 #1
f((int*)0); // int* 满足两个约束,选择 #2,因为它更受约束