Bootstrap

[C++] C++20约束表达式和requires子句

约束


约束是逻辑操作和操作数的序列,它指定了对模板实参的要求。

  1. 合取

    两个约束的合取是用 && 运算符。

    template <typename T>
    concept luser = std::integral<T> && std::signed_integral<T>;
    

需要约束同时满足两个要求。合取判断的时候,使用短路检测,即对 std::integral<T> 结果为false时,不再对 std::signed_integral<T> 进行判断。

  1. 析取

    两个约束的析取是用 || 运算符。

    template <typename T>
    concept luser = std::integral<T> || std::signed_integral<T>;
    

需要约束满足任意一个要求。析取判断的时候,使用短路检测,即对 std::integral<T> 结果为ture时,不再对 std::signed_integral<T> 进行判断。

  1. 原子约束

    原子约束,不会是逻辑与(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>;

luser1luser2 是两个不同的概念,因为在源码层面上不同,一个是 <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(可选) 返回类型要求(可选);

替换和语义约束检测按照顺序进行:

  1. 模板参数(若存在)被替换
  2. 如果使用noexcept,一定不能潜在抛出异常
  3. 如果返回类型要求存在,则:
    1. 模板参数被替换到返回类型要求。
    2. 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,因为它更受约束
;