C++20四大之三:concept特性详解

C++20四大之三:concept特性详解,第1张

前言:C++模板的演进
模板的演进是C++发展史中一条十分重要的线,个人觉得Concept是这条线中最大的一个特性了。


在介绍Concept之前,我们先捋一捋模板这条线的发展。



据Stroustrup先生回忆, 对模板的设想早在1982年便有了,正式提出是在1998年的 USENIX C++ conference会议上提出,设计模板的初衷是因为当时的C++缺少一个标准库,而当时没有模板的C++很难设计出“vector、list”这种适用于多种类型的容器。



到1998年模板正式进入标准,在这之前C++模板已经是一个图灵完备的语言了——理论上来讲,可以只用模板代码解决任何可计算的问题。


用递归实现循环、模板特化、偏特化实现分支判断。


例如下面这个模板可以在编译期计算的fibonacci函数:

template<int  N>
int fibonacci() {
  return fibonacci<N-1>() + fibonacci<N-2>(); 
}
template<>
int fibonacci<1>() { return 1; }
template<>
int fibonacci<0>() { return 0; }

SFINAE特性为上述代码的运作提供了保障。


两个模板的特化构造了递归的退出条件,“隐式”的实现了if判断的功能。


到了C++17,有了if constexpr,上面这段代码可以写的更加“直白”

template<int N>
constexpr int fibonacci(){
  if constexpr(N <= 1)
    return N;
  else
    return fibonacci<N-1>() + fibonacci<N-2>();
}
auto val = fibonacci<5>();

不管是SFINAE、std::enable_if、还是if constexpr,其实都在做一件事:使编译期if的使用更加直白易用。


编译期if可以用来判断数值,如fibonacci模板,更多的时候是用来判断“类型”,std::enable_if便是如此。


template <typename T, std::enable_if_t<std::is_enum<T>::value, bool> = true>
void func(const T& value) {
  //当T是枚举类型时,匹配当前模板函数
  .......
}
template <typename T, std::enable_if_t<std::is_arithmetic<T>::value, bool> = true>
void func(const T& value) {
  //当T是数值类型时,匹配当前模板函数
  .......
}

func有一个重载,func的内部逻辑需要区分value的类型:enum跟数值类型分开处理。


为了使编译器能够在实例化时匹配到正确的函数模板,我们用std::enable_if_t这一长串的代码,功能实现了,但代码十分冗长、可读性不好。



此时concept可以登场了。



1,什么是concept
——对T的约束(的集合)
通过一个简单的例子来展示“对T的约束”

template<typename T>
concept Integral = std::is_integral<T>::value;

template<Integral T> 
bool equal(const T& a, const T& b){
    return a == b;
}

函数模板equal返回两个值是否相等,这个 *** 作不能用在浮点类型中。


为防止该模板被float等浮点类型实例化,使用一个Integral concept来约束T——也就是强制保证该模板只能使用整数类型实例化。



函数模板equal的T不再是一个“typename”,变成了“Integral”,显然语义上Integral更精准。


当我们试图用浮点类型实例化时,会给出错误:

auto value_a = equal(1, 3);       //OK
auto value_b = equal(0.11, 0.33); //ERROR

equal(0.11, 0.33) 会在编译时报错:

2,我们为什么需要concept
假如使用std::enable_if来实现上述功能,可以从代码可读性以及报错信息两个方面来感受concept的便捷:
1,更好的代码可读性

template<typename T, std::enable_if_t<std::is_integral<T>::value, bool> = true>
bool equal_2(const T& a, const T& b){
    return a == b;
}

使用concept约束T,比使用enable_if更加简洁、符合自然语义。



2,减少重复代码
使用std::enable_if进行类型约束容易出现大量的重复代码,而concept则很容易重复利用。



3,更清晰、更直接的报错信息
concept不满足时编译器会直接告知哪个concept未满足,enable_if实现方式的报错更像是“隔靴搔痒”

3,约束T的四种方式

template<my_concept T>
void func(T t);

void func(my_concept t);

template<typename T> requires my_concept<T>
void func(T t);

template<typename T> 
void func(T t) requires my_concept<T>;

第二种无疑是最简洁、最符合自然语义的写法了。


后两种使用了requires关键字,这种方式称为“require-clause”(require子句),可以方便的组合多个concept:

template<typename T> requires concept_a<T> && concept_b<T> || sizeof<T> == 4
void func(T t);

4,内置concept
头文件内置了常用的concept,可以满足大多数日常开发需求:
这些concept可分语言核心concept、为比较concept、对象concept、调用concept、迭代器concept、范围concept

5,定义自己的concept
假如遇到了头文件内的concept无法满足实际需求的 场景,我们就需要定义自己的concept:

template <template-parameter-list >
concept concept-name = constraint-expression;

值得注意的是,concept的定义,不能递归、不能在定义时被其他concept约束

template<typename T>
concept V = V<T*>; // Error: 不能递归定义
 
template<C1 T>
concept Error1 = true; // Error: 不能被其他concept约束

template<class T> requires C1<T>
concept Error2 = true; // Error: 不能被其他concept约束

若concept的template-parameter-list声明的形参大于一个,则使用concept时实参列表需比形参列表少1:

template <typename T, typename B>
concept Derived = std::is_base_of<B, T>::value;
template<Derived<Base> T>
void f(T param);

6,requires关键字
我们在第3节中已经介绍过requires关键字,它可以引出一个“require-clause”(require子句)来实现约束,require子句可以配合 && 、|| *** 作符灵活组合多个约束。



除了require-clause之外,requires关键字还有另外一种用法,那就是require-expression:

requires {requirement-seq}
requires (parameter-list) {requirement-seq}

template<typename T> 
concept C = requires(T x, T y) {
  //简单约束
  x+y;            // x+y有意义(x与y可以相加) 
  T::func();      // T类型定义了静态方法func(public)
  x.func2();      // T类型定义了非静态方法func2(public)
  &x;             // x可以取地址 
  sizeof(T) == 10;// sizeof(T)可以与10比较——该约束永远为成立 
   
  //类型约束
  typename T::MyType; //  MyType是T内定义的一个内置类型(public) 
  
  //复合约束
  //1, x.func() 合法(func是x的成员方法)
  //2, MyType在T内有定义
  //3, func()的返回值可以转换为MyType
  {x.func()} -> std::convertible_to<typename T::MyType>; 
  //x.func() 合法且不会抛出异常
  {x.func()} noexcept{*x}//嵌套约束
  requires MyConcept<T>;    //T满足MyConcept概念的约束
  requires (sizeof(T) == 4);//T的对象占用4字节内存                             
};

使用requires-expression可以定义逻辑非常复杂的concept,()内是参数列表——是可省略的,{}内是约束列表。


约束列表中的表达式会在编译器检查其合法性(并不是求表达式的值,只是做一个可行性检查),结果是true或者false。


若约束列表中的表达式都能通过,则本条requires表达式的值为true。


在本例中,x+y; 约束并不会真的相加求值,而是要求x与y是“可相加的”。



约束列表中的约束可以分为四类:

  • 简单结束:一个随意的表达式,编译器只检查其语法合法性。


    x+y; 便是一个简单约束。


  • 类型约束: 以typename开头,后跟随一个类型名。


  • 复合约束: 形如{ expression } noexcept(可略) -> return-type-requirement(可略) ;编译器会检查expression的合法性,假如noexcept有被使用,编译器还会检查expression有无潜在的抛出异常的可能,最后,假如return-type-requirement没被省略,则检查exp
    嵌套约束: 以requires关键字开头,后跟其他约束表达式

后语:
在查阅资料时与concept一起出现的还有constraints一词,该词意为“约束”,这两个名词经常让人迷糊,个人觉得可以这样理解:

  • concept是手段,是实现约束的手段,constraints——约束才是目的。


  • concept的本质是一个集合——该集合内定义了一个或多个“条件”,编译期将这些“条件”实施到T上,若T不满足这些“条件”中的任何一个则给出编译错误,这个过程便是约束。


  • concept之于模板的意义,恰似模板之于普通代码的意义。


    后者将某个具体类型泛化成T,前者则给T以约束。


欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/565176.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-04-06
下一篇 2022-04-06

发表评论

登录后才能评论

评论列表(0条)

保存