C++ 20 Concept 语法

requires expression

一种表达式,它很像一个lambda表达式,一个未命名元函数。例如:

requires(int a,int b){ a+b;}

其中:()部分是参数列表,{ }部分是需求列表。这个表达式是在编译期求值的,表达式的结果是true或者false。它的工作机制是:对于{ }内的若干条语句检查可行性,如果都能通过,则本条requires expression的值为true。在本例中,a+b;是提出了需求,即:有关两个int要求能相加。显然是满足的。则本例的表达式值为true。

int main(){
    static_assert( requires(int a,int b){ a+b;} );
}

这个静态断言,证明了此种表示式是编译期求值的特性。

int main(){
    static_assert( requires{ false; } );  //断言仍正确
}

上述语句,可以看出()部分是可选的,因此可省略。{ }部分中false;这个表达式是可行的,记住我们不关心被检查的表达式的结果,只关心它是否可行。{ }中是列举需求的,我们有四种需求描述方法:1)简单需求 2)类型需求 3)复杂需求 4)嵌套需求。 
前文的a+b就是个简单需求。另外一个简单需求例子:

int main(){
    static_assert( requires{ new int; } );  //OK
}

而当我们不但要检查表达式是否可行,还要检查表达式是否抛例外,需要用到复杂需求形式。可以像这样写:

int main(){
    static_assert( requires{ (new int) noexcept; );  //ERROR
}

显然,new int是抛例外的,我们却需求不跑例外,需求不满足,requires表达式求值为false,则静态断言失败。 当我们需要表达式的值的类型符合某种要求时,这样写:

#include <type_traits>
int main(){
    static_assert( 
        requires{  {new int} -> std::same_as<int*>;  }  
   );
}

下一个是类型需求(Type Requirement)的例子

#include <vector>
int main(){
    static_assert( requires{ typename std::vector<int>::iterator; } );
}

类型需求的标志就是typename,平铺直叙,直截了当。我们看到std::vector<int>::iterator这个内嵌类型是存在的。
下一个例子:

#include <type_traits>
#include <vector>

int main(){
    static_assert( 
        requires(std::vector<int> v, int i){  
            v.at(i);  //Simple Requirement
            typename std::vector<int>::value_type;   //Type Requirement
            sizeof(int)==5;  //? 居然检查通过了
        }  
   );
}

看到sizeof(int)==5检查通过了,这是当然的,还记得Simple Requirements只检查表达式可行性? 如果要检查表达式的值,则需要使用Nest Requirements语法:

#include <type_traits>
#include <vector>

int main(){
    static_assert( 
        requires(std::vector<int> v, int i){  
            v.at(i);  //Simple Requirement
            typename std::vector<int>::value_type;   //Type Requirement
            requires (sizeof(int)==4);  //Nest Requirement
        }  
   );
}

requires clause

C++对关键字充分榨取价值,往往一个关键字在多个场合使用。此处使用的是需求从句场合。用于限定模板函数的类型参数。

template<typename T> 
requires true
T add(T a, T b) { 
    return a + b; 
}

基本原理是,requires 之后一个常量表达式,当此表达式为true时,模板函数被选用。requires true恒成立,相当于对T没有限制。clause表达式可以使用的操作数为:1)基本表达式

template<typename T> 
requires std::is_integral<T>::value  //基本表达式
T add(T a, T b) { 
    return a + b; 
}

什么是“基本表达式”(primary expression)? 文字量,标识符,fold表达式,还有requires表达式等,这些都是基本的。反例,2>1 就不是基本表达式,因为这个表达式可以分解为一个运算符> ,配合两个基本表达式1和2。所以:

template<typename T> 
requires 2>1   //语法错误
T add(T a, T b) { 
    return a + b; 
}

requires 2>1这个从句就是语法错误的。解决办法是加括号,括号内的是基本表达式。例如:

template<typename T> 
requires (2>1)  //括号括起来,作为一个整体的基本表达式
T add(T a, T b) { 
    return a + b; 
}
#include <type_traits>

template<typename... Args> 
requires (std::is_integral_v<Args> && ...)   //折叠表达式 fold expression
int add(Args... a) { 
    return (a + ...);
}

int main(){
    
    int a=1;
    short b=2;
    char c=3;
    
    return add(a,b,c);
}
template<typename T> 
requires requires(T a){ ++a; }  //require表达式
T add(T a) { 
    return ++a;
}

int main(){
    add(false);  //自C++17开始,bool型无++运算,因此触发constraint报警
}

requires从句的constraint表达式部分,可以用逻辑运算符把基本表达式组合起来,例如:

#include <type_traits>

template<typename T> 
requires std::is_integral_v<T> && ( sizeof(T)>=4 )
T add(T a) { 
    return ++a;
}

int main(){
    short v=1;  //因为short长度2字节,不符合>=4字节的这条限制,故触发报警
    add(v);
}

constraint-expression

此表达式的基本表达式必须是返回bool纯右值(true或false),这些基本表达式用逻辑运算符连接。往往这个表达式是泛型的,当它生存在模板类或模板函数定义上下文中,就会出现模板的类型参数。

concept

终于到达了这个高级概念。前面说了constraint-expression用于表达对于类型应该张什么样子的需求描述。现在我们把这个需求集合整理起来,给起一个名字。这个机制就是C++20的concept机制。concept就是类型需求说明书。

#include <type_traits>

template<typename T>
concept My_First_Type_Requirement_Specification =   //我的第一个类型需求规格说明书
    std::is_integral_v<T> &&   //第一条:必选是整形
    ( sizeof(T)>=4 ) &&        //第二条:类型尺寸不小于4
    requires(T a){ ++a; }      //第三条:类型支持自++运算符。
;

template<typename T> 
requires My_First_Type_Requirement_Specification<T>    //requires从句
T add(T a) { 
    return ++a;
}

int main(){
    short v=1;  //因为short长度2字节,不符合>=4字节的这条限制,故触发报警
    add(v);
}

concept可以看做可重用的元函数,我可以只在一处修改这个元函数,其它引用此元函数的地方都不必一一修改了。而requires表达式更像一个lambda表达式,用于ad-hoc用途(拉丁语:尽在此处使用),避免了先在很远的地方定义一个元函数,然后才能在使用的尴尬。

#include <type_traits>
#include <concepts> 

//std::integral<T>是C++20 concept库中定义的一个东西,目的是不要大家重复造轮子。
//即一个简单的:template < class T > concept integral = std::is_integral_v<T>;

//requires-clause中使用concept。把requires true,替换为requires std::integral<T>
template<typename T> 
requires std::integral<T>    //constaint-expression, 要求std::integral<T>实例化为true
T add(T a) { 
    return ++a;
}
 
//把concept放到typename位置,要求concept是一元的谓词元函数,即C<T>形式
template<std::integral T> 
T add(T a, T b) { 
    return a+b;
}

template<typename V,typename U>
concept binary_concept = true;

//ERROR: binary_concept是二元的谓词,不能这么写
template<binary_concept T> //binary_concept<T>,不符合binary_concept<V,U>
void add2(T a, T b) { 
    return a+b;
}

int main(){
    
    static_assert(std::integral<int>);//static_assert(true), 把true替换走
    
    //requires表达式中使用concept,就像表达式中调函数的意思。例如:(x + f(2))*10
    static_assert( requires(){ 
                      std::integral<int> ;  //调concept
                   } );
    
}

上面例子是concept一般可能出现的地方。