C++20 概念(concepts)入门

文章目录

  • C++20 概念(concepts)入门
    • 引入
    • 自定义限制:将概念作为对模板类的限制来使用
    • 将概念作为布尔值来使用
    • 约束表达式(require expression)
      • 约束表达式的具体要求
    • 表示概念的其他方法
      • 约束从句(require clause)
      • 概念自动变量

C++20 概念(concepts)入门

参考资料:cppreference(更多库函数提供的概念可以在这里查阅)

参考资料:c++20 concept

参考资料:C++ 20 Concept 语法

参考资料:concept for C++20用法简介

本篇文章使用的编译器是 gcc 10.1 的 g++,加上编译选项 -std=c++20

本文看上去很长,主要是因为代码有点占篇幅。希望 C++ 没事儿

引入

首先看在 C++ 17 引入的 gcd 函数(在 numeric 头文件中),一种可能的实现如下:

// 摘自 
template
  constexpr common_type_t<_Mn, _Nn>
  gcd(_Mn __m, _Nn __n)
  {
    static_assert(is_integral_v<_Mn>, "gcd arguments are integers");
    static_assert(is_integral_v<_Nn>, "gcd arguments are integers");
    static_assert(!is_same_v, bool>,
    "gcd arguments are not bools");
    static_assert(!is_same_v, bool>,
    "gcd arguments are not bools");
    return __detail::__gcd(__m, __n);
  }

可以看到,这个 gcd 函数只是个包装,真正进行计算的函数是 __gcd,这里不予展示。这个包装函数只额外做了一件事情:检查 __m__n 是否为整数类型。有无更简便的表示方法?

C++20 的概念可以做到这一点。

程序 1:第一个概念程序
#include 
#include 
template
constexpr std::common_type_t gcd(T1 a, T2 b)
{
	return b ? gcd(b, a % b) : a;
}
int main()
{
	std::cout << gcd(37u, 666u) << std::endl;
	std::cout << gcd(1, 1.0) << std::endl;
	return 0;
}

typename 被替换成了 std::unsigned_integralstd::unsigned_integral 被定义在 concepts 中,实现如下:

template 
concept integral = is_integral_v<_Ty>;
template 
concept signed_integral = integral<_Ty> && _Ty(-1) < _Ty(0);
template 
concept unsigned_integral = integral<_Ty> && !signed_integral<_Ty>;

其中 concept 是新的关键字,我们暂时不管它具体是什么意思,但可以看出,判断是否为整数类型的工作是由 C++11 就有的 type_traits 完成的,概念的引入只是为了使得代码的结构更好(可能?)。通过这样的定义,我们可以断言:程序 1 会因为 main 函数中的第二句代码编译错误。事实的确如此,编译器输出:

demo.cpp:11:28: error: use of function 'constexpr std::common_type_t<_Tp1, _Tp2> gcd(T1, T2) [with T1 = int; T2 = double; std::common_type_t<_Tp1, _Tp2> = double]' with unsatisfied constraints
   11 |     std::cout << gcd(1, 1.0) << std::endl;

这里为了简化问题,规定 ab 均为无符号整数,所以事实上这句代码的两个参数都不符合我们的要求,这也是为什么第一句代码要在整数后面加上后缀 u。然而,如果写 gcd(0u, true),可以发现仍然可以通过编译,这与 numeric 中的 gcd 要求参数不能是布尔类型的精神不符,如何做到进行同时限制?(注:如果这里两个参数都是布尔型,则不能通过编译,因为函数内部会递归调用,而在两个参数都是布尔型,那么 a % b 会被默认转换成 int 类型,而 int 类型是不能通过编译的)

自定义限制:将概念作为对模板类的限制来使用

我们可以模仿库中的语法,编写自定义的概念:

程序 2:自定义概念
#include 
#include 
template
concept gcdint = std::unsigned_integral && !std::is_same_v, bool>;

template
constexpr std::common_type_t gcd(T1 a, T2 b)
{
	return b ? gcd(b, a % b) : a;
}
int main()
{
	std::cout << gcd(37u, 666u) << std::endl;
	std::cout << gcd(0u, false) << std::endl;
	return 0;
}

现在 gcd(0u, false) 就不能通过编译了,编译器报错:

demo.cpp:13:28: error: use of function 'constexpr std::common_type_t<_Tp1, _Tp2> gcd(T1, T2) [with T1 = unsigned int; T2 = bool; std::common_type_t<_Tp1, _Tp2> = unsigned int]' with unsatisfied constraints
   13 |  std::cout << gcd(0u, false) << std::endl;

有一说一,这样写对作者的意思确实更易懂了。需要注意的是,这样的代码在 C++11 中就有完全等效的写法,但会比这更长,没有这么易懂。

将概念作为布尔值来使用

概念除了以上用法,还可以直接作为一个编译时就确定的布尔值来使用。这和前面代码中的 std::is_same_v 的性质类似。下面以 C++20 新引入的 std::same_as 概念为例作为对模板内联常量 std::is_same_v 的替代。

程序 3:是是非非
#include 
#include 
#include 
int main()
{
	std::cout << "(in C++11) int is the same as long: " << std::is_same_v << std::endl;
	std::cout << "int is the same as long: " << std::same_as << std::endl;
	std::cout << "int is the same as __int32: " << std::same_as << std::endl;
	std::cout << "long is the same as __int32: " << std::same_as << std::endl;
	return 0;
}

运行结果:

(in C++11) int is the same as long: 0
int is the same as long: 0
int is the same as __int32: 1
long is the same as __int32: 0

你便可放心大胆地用,不用担心结果变了:因为 same_as 这个概念的一个可能的内部实现就是用的 is_same_v

// 摘自 
namespace __detail
{
  template
    concept __same_as = std::is_same_v<_Tp, _Up>;
} // namespace __detail

约束表达式(require expression)

概念库还引入了一个重要的东西,叫做约束表达式(require expression)。约束表达式与 Lambda 表达式有点像,但用途和效果完全不同。Lambda 表达式的结果是一个 Lambda 对象,而约束表达式的结果是一个概念。

一个约束表达式的例子如下:

程序 4:鸭子类型
#include 
#include 
#include 
template
concept duck_type = requires(T x)
{
	x.quack();
	x.quack("quack");
};

class T1 {};
class T2
{
public:
	void quack() const {}
	void quack(const std::string& b) const { std::cout << b << std::endl; }
};
class T3 : public T1
{
public:
	int quack(const std::string& b = "quack") const { std::cout << b << std::endl; return 0; }
};

template
void quack(const T& x)
{
	x.quack();
}

int main()
{
	// quack(T1()); // error: use of function 'void quack(const T&) [with T = T1]' with unsatisfied constraints
	quack(T2());
	quack(T3());
	return 0;
}

从模板的定义看上去,如果直接使用 typename 好像也没什么不对的,甚至能得到更具体的结果:error: 'const class T1' has no member named 'quack'。但是,如果你的约束很多,使用约束表达式就能具体地告诉你到底差哪些约束,而直接使用 typename 可能会导致一大片编译错误,就没有这么直观了。

约束表达式的具体要求

事实上,约束表达式内部并不是直接写成函数的样子就可以了,必须遵循一定的规范。约束表达式的大括号内部并不叫做函数体,而叫约束列表。

约束可以大体分为四类:简单约束,类型约束,复杂约束,嵌套约束。

简单约束的形式是一个非约束表达式的表达式(可以是概念),如果这个表达式能够通过编译,或者这个概念为真,那么这个约束表达式返回的概念的布尔值可能为真,否则一定为假。比如,上例中的 x.quack()x.quack("quack") 就是两个简单约束。

类型约束的形式是 typename xxx;。如果 xxx 是一个类型,那么这个约束表达式返回的概念的布尔值可能为真,否则一定为假。

程序 5:可迭代对象检查
#include 
#include 
#include 
template
concept weak_iterable = requires(T x)
{
	typename T::iterator;
	x.begin();
	x.end();
};

int main()
{
	std::cout << "is std::string iterable: " << weak_iterable << std::endl;
	return 0;
}

运行结果:

is std::string iterable: 1

复杂约束是指形式更复杂的约束,例如,如果要求一个表达式不能抛出异常,需要用如下语法:

{xxx} noexcept;

其中 xxx 不含结尾的分号。

如果我们要求一个表达式的值可以是某种类型(可以隐式转换为某种类型),见下例:

程序 6:检查表达式类型
#include 
#include 
#include 
template
concept newable = requires(T x)
{
	{new T} -> std::same_as;
};

int main(int argn, char** argv)
{
	std::cout << "is int newable: " << newable << std::endl;
	std::cout << requires() { { main(0, nullptr) } -> std::same_as; } << std::endl;
	std::cout << requires { { main(0, nullptr) } -> std::convertible_to; } << std::endl;
	return 0;
}

运行结果:

is int newable: 1
1
1

推测,{xxx} -> yyy 的作用是将 xxx 的类型作为模板参数传入 yyy 模板的最后一个参数,其中 yyy 需要是一个概念。编译器的说法是,yyy 应该是一个 type-specifier

最后是嵌套约束。嵌套约束的意思是约束列表中存在一个形式为 requires(xxx); 的约束表达式,这种形式的约束表达式表示要求 xxx 通过静态断言。注意,这种形式的约束表达式只能出现在约束表达式中。

程序 7:嵌套约束
#include 
#include 
#include 
template
concept int64 = requires(T x)
{
	std::integral;
	requires(sizeof(x) == 8);
};

int main(int argn, char **argv)
{
	std::cout << int64 << std::endl;
	std::cout << int64 << std::endl;
	return 0;
}

运行结果:

1
0

需要注意,约束表达式是可以不用模板的,但是概念必须用到模板。

表示概念的其他方法

约束从句(require clause)

下面我们会再一次用到 requires 关键字,只不过这里的 requires 关键字与上面提到的不是同一个,也就是说 requires 的含义要视语境而定。

程序 8:约束从句
#include 
#include 
#include 
#include 

template 
requires std::integral && requires { requires(sizeof(T) == 4); } && (sizeof(T) == 4)
T func(T x) { return x + 1; }

int main(int argn, char **argv)
{
	std::cout << func(1) << std::endl;
	// std::cout << func(1.0) << std::endl;
	return 0;
}

也就是说,我们可以不提前指定 T 要满足的概念,如上例中,如果我们要提前指定,我们必须定义一个全局的概念。像这样通过一个约束从句来表示 T 需要满足的概念,可以避免定义全局概念。

如上例,约束从句的语法结构是:requires xxx。规定 xxx 是一个基本表达式(primary expression)或者由逻辑运算符连接的多个基本表达式。简言之,如果出现了你不清楚的编译错误,你可以选择加上一个括号,把一个非基本表达式变成一个基本表达式(因为 (xxx) 总是一个基本表达式)。

概念自动变量

程序 ⑨:1 + 1 = ?
#include 
#include 
#include 
#include 

std::integral auto add(std::integral auto a, std::same_as auto b)
{
	return a + b;
}

int main(int argn, char **argv)
{
	std::cout << add(1ll, 1) << std::endl;
	return 0;
}

这种写法看上去更简单,但看上去更难理解其中的行为。不过,相信大家能看懂!

如果找到错误,希望大家能高抬贵手,指出这篇文章中的错误。谢谢。

你可能感兴趣的:(C++)