参考资料: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 的概念可以做到这一点。
#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_integral
。std::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;
这里为了简化问题,规定 a
和 b
均为无符号整数,所以事实上这句代码的两个参数都不符合我们的要求,这也是为什么第一句代码要在整数后面加上后缀 u
。然而,如果写 gcd(0u, true)
,可以发现仍然可以通过编译,这与 numeric
中的 gcd
要求参数不能是布尔类型的精神不符,如何做到进行同时限制?(注:如果这里两个参数都是布尔型,则不能通过编译,因为函数内部会递归调用,而在两个参数都是布尔型,那么 a % b
会被默认转换成 int
类型,而 int
类型是不能通过编译的)
我们可以模仿库中的语法,编写自定义的概念:
#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
的替代。
#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)。约束表达式与 Lambda 表达式有点像,但用途和效果完全不同。Lambda 表达式的结果是一个 Lambda 对象,而约束表达式的结果是一个概念。
一个约束表达式的例子如下:
#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
是一个类型,那么这个约束表达式返回的概念的布尔值可能为真,否则一定为假。
#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
不含结尾的分号。
如果我们要求一个表达式的值可以是某种类型(可以隐式转换为某种类型),见下例:
#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
通过静态断言。注意,这种形式的约束表达式只能出现在约束表达式中。
#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
需要注意,约束表达式是可以不用模板的,但是概念必须用到模板。
下面我们会再一次用到 requires
关键字,只不过这里的 requires
关键字与上面提到的不是同一个,也就是说 requires
的含义要视语境而定。
#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)
总是一个基本表达式)。
#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;
}
这种写法看上去更简单,但看上去更难理解其中的行为。不过,相信大家能看懂!
如果找到错误,希望大家能高抬贵手,指出这篇文章中的错误。谢谢。