C++20 concept && constraint

前言

C++20引入了concept,对模板参数提供约束。因为模板的接口都是隐式接口,模板的要求也是隐式的,需要你深入模板去查看模板究竟需要哪些限制。而concept显化了对类型的限制,在编译期就将不满足的实例化剔除。

  • 假设我们想要用可变参数实现一个sum求和函数,求若干个对象的和,像下面这样:
template <class...Args>
auto sum(Args ... args) noexcept{
	return (... + args); //C++17,折叠表达式
}
  • 表面上sum接收任意类型,没有限制。
  • 但是所有类型的对象都应该支持operator+操作。
struct notOperatorPlus{
	//该struct没有支持operator+操作。	
};
  • 显然你无法将notOperatorPlus类所产生的对象用于sum函数的实参。
  • 其次,假设你对sum函数的要求很严格,你需要所有的参数都是同一种类型
sum(1, 2, 3);     //ok, 都是int
sum(1, 2, 3.14);  //bad,不是我们想要的

C++20之前的做法,type traits

所以,现在我们知道了sum的可变参数需要两个限制,每个参数都应该是相同的类型,且每个参数的对象都应该支持operator+操作。当实例化sum时,我们希望更早的获得结果:成功或是失败。而非遇到了不能执行的操作才报错,我们想要将隐式的约束显化。

  • 在concept之前,我们使用type traits。
template <class T, class...Args> //are_same
inline constexpr bool are_same_v = std::conjunction_v<std::is_same<T, Args>...>;

template <class T, class...Args>
struct first_arg{
	using type = T;
};
template <class...Args>
struct first_arg_t = typename first_arg<Args...>::type;

template <class...Args>
enable_if_t<are_same_v<Args...>, first_arg_t<Args...>>
sum(Args...args) noexcept {
	return (... + args);
}
  • 最前面有一个are_same_v的模板别名,一个bool值,如果所有类型相同,返回true,否则返回false。我们使用std::conjunction来实现。将Args参数包中的每一个参数取出来与T做比较,如果相同,就继续萃取后面的参数,如果不同,直接返回false。如果所有的类型都相同,返回最后一种类型,即true。
  • 其次有一个first_arg,这个type traits比较简单,用来获取参数列表中的第一个类型。
  • 然后是我们的sum函数。
  • 如果enable_if的条件为真,即are_same_v返回true,那么将返回参数包的第一个参数类型作为返回值,因为所有参数类型都相同,所以无所谓选取哪一个。如果enable_if的条件为假,那么什么也不返回,就会出错。

这样做是完全ok的,但是难以理解。

requires

  • 让我们看看concept的做法,
template <class...Args>
requires are_same_v<Args...>
auto sum(Args... args) noexcept {
	return (... + args);
}
  • 我们使用新的关键字requires,后面紧跟一个类型为bool的表达式。该表达式如果返回true,满足该要求,开始实例化。如果返回false,那么就在编译期报错。requires后面也可以跟requires表达式

requires 表达式

  • C++20也引入了requires 表达式来更好的配合concept。requres表达式分为四类
  1. 简单requrements
  2. 内嵌requirements
  3. 复合requirements
  4. 类型requirements

为了能示范requires表达式,让我们来看看sum函数还应该对Args提供哪些约束:

  • 注意到我sum是noexcept的,这意味着sum的所有实现都应该是noexcept的。
  • 而且作为sum的实现者,我们清楚,参数的个数应该大于1个,否则意义不大。
  • sum的返回值也应该与Args的类型相同。
template <class...Args>
requires requires(Args ... args){
	(... + args);                    //简单requirement
	requires sizeof...(args) > 1;    //内嵌requirement
	requires are_same_v<Args...>;    //内嵌requirement
	// {(... + args)}noexcept;       //复合requirement
	// {(... + args)} -> std::same_as<first_arg_t<Args...>>;            //复合requirement
	{(... + args)} noexcept -> std::same_as<first_arg_t<Args...>>;      //复合requirement
}
auto sum(Args ... args) noexcept {
	return (... + args);
}
  • requires表达式的编写类似函数的编写,有一个可选择的参数列表,如果你不需要参数,可以省略不写。表达式体至少拥有一个语句。requires表达式可以看作返回值为bool的一个模板函数。
  • 简单requirements可以正常编译即可,即要求Args的对象可以执行加法操作。
  • 内嵌requirements就是在requires表达式的内部再次使用requires。如果你不使用内嵌requires,那么就只要求sizeof…(args) > 1可以编译过,而增加之后,不仅仅要求该表达式可以编译过,还要求返回true,即args参数包至少2个参数。
  • 我们也采用了复合requrements,使用花括号,要求某个操作是noexcept的。
  • same_as是标准concepts库中的一个concept,要求两个类型相同。我们将这个约束用于sum函数的返回值,注意,same_as的第二个模板参数就是箭头前面的返回值。

concept的位置

  • 如果我想复用上面的requires表达式怎么办?如果我还有一个模板函数,拥有和sum函数相同的约束怎么办?
  • 我们可以定义concept。
template <class...Args>
concept MyConcept = requires(Args ... args){
	(... + args);                    //简单requirement
	requires sizeof...(args) > 1;    //内嵌requirement
	requires are_same_v<Args...>;    //内嵌requirement
	// {(... + args)}noexcept;       //复合requirement        //这个和下面一个,可以分开写,也可以合在一块写。
	// {(... + args)} -> std::same_as<first_arg_t<Args...>>;            //复合requirement
	{(... + args)} noexcept -> std::same_as<first_arg_t<Args...>>;      //复合requirement,合并了上面两个concept
};      //注意这个分号!!!

template <class ... Args>
requires MyConcept<Args...>
auto sum(Args...args) noexcept {
	return (... + args);
}
  • 这样,我们可以使用concept。且函数整体看着清爽不少。
  • concept还可以放置在其他位置。
template <MyConcept ... Args>  //直接用concept声明模板参数
auto sum(Args ... args)noexcept;

template <MyConcept ... Args>
auto sum(Args ... args)noexcept requires MyConcept<Args...>; //concept放置在签名式后面
  • 除此之外,concept和返回值自动推导,缩略模板函数也有一些联动。

函数模板缩写

  • 该功能也是C++20新增的,使用auto作为占位符,这样就不同再写什么template了。我个人不是很喜欢这个语法,因为这样缺少了使用template的清晰度。
//正常模板
template <class...Args>
auto sum(Args...args)noexcept{
	return (... + args);
}

//缩略模板的写法
auto sum(auto ... args) noexcept {
	return (... + args);
}
  • 上面两种写法完全相同。auto在参数列表用作一个占位符,代表是一个模板参数类型。
  • 注意,只要参数带有auto,那么一定是缩写函数模板。
  • 我们可以对返回值的auto,和参数的auto,这两个占位符做concept约束。
std::same_as<int> auto function(std::integral auto val) noexcept{
	//...
}
  • 对于function的返回值,约束返回值和int相同,约束模板参数为整形。

其他

concept与其他bool:

constexpr bool always_true(){ return true;}
constexpr bool bool_constant = true;

template <class T>     //ok
concept MyConcept = always_true() &&
				    bool_constant &&
				    std::same_as<T,int>;
  • concept连接的东西需要在编译期求值即可。

利用static_assert测试concept:

  • 因为concept可以看作编译期返回bool类型的函数,所以理所应当可以使用static_assert。
static_assert<std::same_as<int, int>>;  
  • 我只举了一个最简单的栗子,static_assert可以用来测试你所写的concept的正确性。

标准concepts库:

  • C++也提供了concept的标准库,头文件就是< concepts >。
  • 和type_traits类似,concepts库提供了一些基础组件,一些low-level的concepts。
  • 最好使用库里面的concepts 加上&&, ||, 来组合你的concept。尽量不要自己写requires表达式。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值