当前位置: 首页 > news >正文

C++20之Concpet(概念部分,之二)

先序文章请先看C++20之Concept(概念部分,之一)

concept的高级语法

上一篇我们介绍了concpet的概念,还有基本的用法。不过到目前为止,我们见到的只能算是concept的「正统」形式,怎么说呢,尽管concept作为C++20的四大颠覆性特性之一(甚至之首),但单从形式上来看,还是有些一板一眼了。

template <typename T>
requires SomeConditon<T>
struct ClassName {};

这种所谓的「标准形式」看上去还是很浓重的C++味道,它给人的感觉就是“嗯~这很C++”。经历过C++17洗礼的同学都知道,C++新标准很喜欢做的事情就是,发布一个看上去非常平平无奇的新特性,但稍微一展开、一组合,这个特性就爆炸了,让人拍案叫绝。同理,concept并不是只有这种标准用法,今天这篇我们就来让concept代码“飞起来~”

concept作为类型关键字使用

当一个concept恰好是修饰模板参数中的一个类型(而不是它的变体)时,concept可以直接作为类型关键字使用。这样描述可能不好理解,我们来看例子:

template <typename T>
concept Condi1 = std::is_trivial_v<T>; // 这是一个concept

template <typename T>
requires Condi1<T> // 这里concept直接作用于T,而不是类似于T *或者std::decay_t<T>之类的
struct Test {};

上面例子中,Test模板类需要一个concpet名为Condi1,它直接作用于模板参数T上,那么就可以改写成下面的形式:

template <Condi1 T>
struct Test {};

这就好像Condi1成为了一个类型关键字,理解为「符合Condi1定义的类型T」。

这里有一个需要注意的就是,如果作为类型关键字使用,那么它必须是一个concept定义,而不能是其他形式的静态布尔表达式,比如说:

template <std::is_trivial_v T> // ERR,因为std::is_trivial_v并不是concept定义
struct Test {};

不过遇到修饰模板参数的变体,或者存在复合约束的时候,那就只能去使用requires语句了,比如说:

template <typename T>
requires std::is_trivial_v<std::decay_t<T>> // 约束T的变体
struct Test1 {};

template <typename T>
requires std::is_trivial_v<T> && (sizeof(T) <= sizeof(void *)) // 复合约束
struct Test2 {};

当然,它本身和requires语句并不冲突,遇到一些复合约束的时候,两种语法可以共存,比如:

template <typename T>
concept Condi1 = std::is_trivial_v<T>;

template <typename T>
concept Condi2 = sizeof(std::decay_t<T>) < sizeof(void *);

template <Condi1 T1, typename T2>
requires Condi1<T2> && Condi2<T2>
struct Test {};

模板函数的concept

前面的章节我们都在介绍模板类使用concept的情况,但其实对于模板函数,还会有不一样的风景等着我们。

首先,模板函数仍然支持concept的标准形式,比如:

template <typename T>
requires std::is_trivial_v<T>
void f() {}

这种用法和模板类完全一致,不再赘述。那么当然它也支持用concept作为类型关键字的形式:

template <typename T>
concept Trivial = std::is_trivial_v<T>;

template <Trivial T>
void f() {}

但模板函数相比模板类,多了一种可以确定模板参数的途径,那就是参数类型自动推导。回忆一下,对于模板函数来说,编译器可以根据实参类型来推导出模板参数类型,比如说:

template <typename T, typename R>
void f(T t, R r) {}

void Demo() {
  f(1, 3.5); // 推导出f<int, double>
  f("abc", 6ul); // 推导出f<const char *, unsigned long>
}

而concept同样可以作为「参数类型」来修饰形参,比如:

template <typename T>
concept Trivial = std::is_trivial_v<std::decay_t<T>>;

void f(Trivial auto t, Trivial auto r) {}

void Demo() {
  f(1, 3.5); // 会推导出f<int, double>,再分别去检查Trivial<int>和Trivial<double>是否合理
  f('A', std::string("abc")); // ERR,首先推导出f<char, std::string &&>而因为Trivial<std::string>会返false,因此不允许这种实例化
}

这个语法着实是把concept玩飞了,至此我们甚至不需要template关键字就可以定义一个模板函数了。当然这么做的缺点就在于,可能不容易让人意识到这是一个模板函数,于是把声明和实现分散在.h和.cpp中,导致链接错误。所以这里着重强调:虽然这种语法没有出现template关键字,但它仍是模板,需要定义在头文件中!!

使用concept后可能会遇到的疑惑

至此我们基本将concept的概念和用法介绍完毕了。但正如我前面所说的,一个新特性本身可能没有太多东西,但和其他特性组合一下就可能会“爆炸”,所以本节主要介绍concept特性相关的可能会遇到的疑惑点。

模板特化

如果一个模板用了concept修饰,它还能不能被特化呢?答案是可以!concept本身不影响SFINAE的匹配原则,同样支持特化。但是必须有一个前提,就是说特化类型必须符合concept,否则不许特化,举例来说:

#include <concepts> // STL标准库提供了很多concept可以使用
template <std::integral T> // 要求T必须是整数
struct Test {};

template <>
struct Test<long> {}; // OK,对于long类型的特化

template <>
struct Test<std::string> {}; // ERR,因为std::string本身不符合std::integral,因此不可以特化

void Demo() {
  Test<int> t1; // 用通用模板进行实例化Test<int>
  Test<long> t2; // 会使用Test<long>的特化
}

而对于偏特化同样是允许的,但也是要符合concept的类型。比如:

template <std::integral T1, std::integral T2>
struct Test {};

template <typename T>
struct Test<int, T> {}; // 偏特化,OK

concept用作偏特化

我们可以定义用concept约束的偏特化。举例来说:

template <typename T>
struct Test {};

template <std::integral T>
struct Test<T> {};

void Demo() {
  Test<int> t1; // 由于int符合std::integral,因此用偏特化模板实例化Test<int>
  Test<std::string> t2; // 会用通用模板实例化Test<std::string>
}

在上面例子中,Test通用模板原本是没有被concept约束的,我们可以定义如果T符合“某些特性”的话,就使用一种特化。比如说上面就是当T符合std::integral时,使用下面的特化。因此,这里本质是也一种模板的偏特化。

需要注意的是,与其他偏特化一致,如果某种实例同时命中多个偏特化,却没有针对于这种类型的全特化模板时,将会报二义性错误:

template <typename T>
struct Test {};

template <std::integral T>
struct Test<T> {};

template <typename T>
requires (sizeof(T) == 1)
struct Test<T> {};

void Demo() {
  Test<int> t1;
  Test<std::string> t2;
  Test<char> t3; // Ambiguous partial specializations of 'Test<char>'
}

另一个要注意的是,偏特化必须要“更加特化”,也就是说偏特化后的范围应当比特化前的范围更窄,但并没有窄到只剩一种类型(不然就叫全特化了),例如:

template <std::integral T>
struct Test {};

template <std::unsigned_integral T> // 针对于无符号数的偏特化
struct Test<T> {};

首先,偏特化的约束必须包含通用模板中的约束,这里unsigned_integral表示无符号整数,而integral表示整数,显然有包含关系。当然了,这种包含关系并不是看出来的,而是要从定义上包含,我们看看unsigned_integral的定义就知道了:

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

因为unsigned_integral内部已经包含unsigned_integral了,所以才OK。但是如果你用的是两种不搭噶的concept,就会报错,比如说:

template <typename T>
requires (sizeof(T) <= 16)
struct Test {};

template <typename T>
requires (sizeof(T) <= 8)
struct Test<T> {}; // Class template partial specialization is not more specialized than the primary template

尽管这里,长度小于8确实是包含在长度小于16中了,但编译器不会做这种集合运算,你必须要显式定义出来才行,所以正确的做法是:

template <typename T>
concept Con1 = (sizeof(T) <= 16);

template <typename T>
concept Con2 = Con1<T> && (sizeof(T) <= 8); // 尽管包含Con1是句废话,但必须要写

template <Con1 T>
struct Test {};

template <Con2 T> 
struct Test<T> {}; // 因为Con2显式包含了Con1,所以才OK

注意这里是「显式」包含才可以,比如说下面这种就不可以:

template <typename T>
requires (sizeof(T) <= 16)
struct Test {};

template <typename T>
requires (sizeof(T) <= 16) && (sizeof(T) <= 8)
struct Test<T> {}; // 仍然不可以,因为没有「显式」包含

下面这种形式也算「显式」包含,所以也是OK的:

template <typename T>
concept Con1 = (sizeof(T) <= 16);

template <typename T>
requires Con1<T>
struct Test {};

template <typename T>
requires Con1<T> && (sizeof(T) <= 8) // 因为这里也显式用到Con1了,所以OK
struct Test<T> {};

通过这些例子就是希望读者能明白在利用concept进行偏特化时,如何才算符合「偏特化」要求。

函数重载

由于模板函数不能偏特化,因此也不会出现前一节出现的各种问题。不过函数是支持重载的,用concept修饰的模板函数并不影响函数重载,请看例程:

void f(std::integral auto t) {}
void f(int t) {} // OK
int f(double t) {return 0;} // OK

由于函数重载的优先级大于SFINAE匹配,因此这里的函数重载都是OK的。当然,它同样不影响模板全特化:

void f(std::integral auto t) {std::cout << 1 << std::endl;}
void f(int t) {std::cout << 2 << std::endl;}
template <>
void f<int>(int t) {std::cout << 3 << std::endl;}

void Demo() {
  f(1.0); // 打印1
  f(1); // 打印2
  f<>(1); // 打印3
}

上面例子说明了函数重载优先级大于SFINAE,所以f(1)会调用重载函数。而显式添加了<>后,强制使用模板实例,再进行SFINAE时全特化优先级大于通用模板,所以会调用全特化实例。

总之这些都跟以前的模板没有任何变化,主要需要大家牢记的一点就是用concept修饰的函数虽然没有template关键字,但它依然是模板函数,其他的东西就都可以推出来了。

模板函数的“偏特化”

大家应该注意到了,我这里的“偏特化”是打双引号的。的确,C++20仍然是不允许模板函数的偏特化的,比如:

template <typename T, typename R>
void f2() {};

template <typename T>
void f2<T, int>() {} // Function template partial specialization is not allowed

但引入了concept之后,模板函数却可以支持「concept的偏特化」,请看例子:

void f(std::integral auto t) {std::cout << 1 << std::endl;}
void f(std::unsigned_integral auto t) {std::cout << 2 << std::endl;}

void Demo() {
  f(1); // 打印1
  f(1u); // 打印2
}

相信细心的读者应该已经发现了,其实这并不是「偏特化」,而是函数重载,所以这种情况下,并不要求按照前面章节所叙述的模板类的那种偏特化原理,即便两个concept是不包含的,也仍然能通过编译。

但是一定要注意,一旦某种类型同时命中多个模板函数,则会直接报二义性错误,比如:

template <typename T>
concept Small = sizeof(T) == 1;

void f(std::integral auto t) {std::cout << 1 << std::endl;}
void f(Small auto t) {std::cout << 2 << std::endl;}

void Demo() {
  f(1); // 打印1
  f('A'); // Call to 'f' is ambiguous
}

由于char类型的实例化同时命中了两个模板定义,因此无法生成两个重载函数,所以直接报二义性错误了,这一点希望大家在使用时能够注意。

总结

本篇介绍了一些concept的高级语法,以及在使用时会出现的问题。主要包括

  • concept用做类型关键字
  • concept用做函数参数类型修饰符
  • concept偏特化
  • concept函数重载

后面会有专门的章节介绍concept是如何解决一些场景下的实际问题的实例。

相关文章:

  • 【Spirng】@Component和@Configuration和@Bean的区别
  • 跟着江南一点雨学习springmvc(3)
  • 安卓手机使用Tasker实现应用级功能,屏幕翻译v9,翻译复制贴图
  • 一篇文章吃透 CSS3 属性: transition过渡 与 transform动画
  • 通讯录的动态版本
  • Docker搭建Kafka集群
  • WPS增加正则处理函数,简直如虎添翼
  • opencloudos容器镜像优化
  • 二.go语言条件与循环
  • 高阶函数1
  • 电子信息考研择校
  • 互联网数据管理平台
  • 本科行政管理毕业论文什么题目好写点?
  • kmp の 笔记
  • 最新网站证书提示风险的原因和几个解决方法
  • 2017-08-04 前端日报
  • centos安装java运行环境jdk+tomcat
  • create-react-app做的留言板
  • mysql中InnoDB引擎中页的概念
  • Next.js之基础概念(二)
  • nginx 配置多 域名 + 多 https
  • ReactNativeweexDeviceOne对比
  • Shadow DOM 内部构造及如何构建独立组件
  • 多线程事务回滚
  • 高度不固定时垂直居中
  • 利用jquery编写加法运算验证码
  • 深入体验bash on windows,在windows上搭建原生的linux开发环境,酷!
  • 验证码识别技术——15分钟带你突破各种复杂不定长验证码
  • 一起参Ember.js讨论、问答社区。
  • 3月7日云栖精选夜读 | RSA 2019安全大会:企业资产管理成行业新风向标,云上安全占绝对优势 ...
  • LIGO、Virgo第三轮探测告捷,同时探测到一对黑洞合并产生的引力波事件 ...
  • 如何在 Intellij IDEA 更高效地将应用部署到容器服务 Kubernetes ...
  • ​如何使用ArcGIS Pro制作渐变河流效果
  • (Redis使用系列) Springboot 实现Redis 同数据源动态切换db 八
  • (翻译)Quartz官方教程——第一课:Quartz入门
  • (附源码)springboot 校园学生兼职系统 毕业设计 742122
  • (利用IDEA+Maven)定制属于自己的jar包
  • (算法)求1到1亿间的质数或素数
  • (一)【Jmeter】JDK及Jmeter的安装部署及简单配置
  • (原創) 未来三学期想要修的课 (日記)
  • (转)大型网站的系统架构
  • (转)全文检索技术学习(三)——Lucene支持中文分词
  • .NET 解决重复提交问题
  • .sdf和.msp文件读取
  • @Responsebody与@RequestBody
  • [C++提高编程](三):STL初识
  • [CC2642r1] ble5 stacks 蓝牙协议栈 介绍和理解
  • [Delphi]一个功能完备的国密SM4类(TSM4)[20230329更新]
  • [EFI]Dell Latitude-7400电脑 Hackintosh 黑苹果efi引导文件
  • [ios] IOS文件操作的两种方式:NSFileManager操作和流操作【转】
  • [Java][方法引用]构造方法的引用事例分析
  • [JavaWeb学习] Spring Ioc和DI概念思想
  • [JDBC-1] JDBC Base Template
  • [leetcode 数位计算]2520. 统计能整除数字的位数
  • [MSSQL]GROUPING SETS,ROLLUP,CUBE初体验