添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
int




    
 add(int x, int y);
unsigned addu(unsigned x, unsigned y);
float addf(float x, float y);
// ...
struct Vector { int x; int y };
struct VectorF { float x; float y};
// ...

等等诸如此类。在现代化的高级编程语言中,我们通常使用一种称为泛型(Generic)的语法,将定义中的类型提取出来依赖调用者的输入。而在 C++ 中,我们称之为模板(Template),上述代码使用模板可以写作:

int a = add(114, 514);                  // 自动推导
double b = add<double>(19.19, 810);     // 显式指定
Vector<unsigned> v { 2, 3};

在 C++20 之前,我们所能做到的就只有这样了。但是,这种做法显然是存在很大的弊端的:对于模板提供者而言,我们无法限制使用者传入什么类型,这很可能导致使用者使用了预料之外的类型从而导致了未知的结果;而对于模板函数或模板类的调用者而言,没有一个简单的方法可以知道自己的类需要实现哪些接口才能满足模板类型的需求。在上述的例子中,我们可以简单地猜测 add 函数只需要我们实现 operator+(T) 的重载即可,但是在实际开发中,尤其是模板类,其内部复杂且通常都会嵌套许多其他的模板类,你很难通过看代码来整理出自己需要实现的接口列表。

由于上述提到的问题,C++ 为其标准库制定了一套具名要求(Named Requirement),标准库中某些模板参数会要求你传入的类型满足某一种具体的具名要求,例如,标准库中有一个函数用来随机打乱容器中的元素,其声明为:

在该函数的解释中,模板参数 RandomIt 被要求 ValueSwappable 以及 LegacyRandomAccessIterator,通过进一步查阅二者的定义,我们就能知道我们需要为自己的类实现什么接口才能将其作为该模板函数的模板参数使用了。

具名要求某种程度上已经提供了一种新语法的雏形,我们不禁会想:如果能把这种限制直接写进代码中,而不是通过文档的形式体现该有多好!

是的,在 C++20,我们所想的已经成为了现实。

让我们回到 add 函数之中,在 C++20,我们可以这样定义它:

concept Addable = requires(T x, T y) { x + y; }; template<typename T> requires Addable<T> T add(T a, T b);

相比于之前只使用模板的写法,这种写法显然复杂了许多。

概念的定义

首先,在定义函数之前,我们先定义了一个概念(Concept):事实上,“概念”这一概念几乎可以和上文所说的“具名要求”画上等号,它就可以视为以代码形式写出来的具名要求。

概念的定义以 concept 关键字开头,之后紧跟该概念的标识符,在此处我们使用 Addable,接着跟随 = 为其赋值。

为一个概念赋值的方法有很多,其中一种办法就是使用 requires 关键字引入一系列要求的集合,这些要求可以是:

表达式是一个不求值的操作数;只检查语言的正确性,例如上文中的:

只要 T 能够令 x + y 通过编译,即 T 类型有重载过 operator+(T)T 就满足概念 Addable

这种要求也可以使用函数调用的形式,用来判断类型 T 是否定义了指定名字的成员函数。同样,该函数只检查正确性,不会真的被调用。

类型要求是关键字 typename 后面接一个可选限定的类型名称。该要求是指命名的类型是有效的:

// 并且 类型 T::inner 必须存在 // 并且 *x 的结果必须可以转换为 T::inner {*x} -> std::convertible_to<typename T::inner>;

其中,std::convertible_to<From, To> 是标准库 concepts 中定义的概念之一,要求类型 From 可以转换为类型 To。在本例中,第一个参数 From 将自动填入 *x 的返回值类型。

可以在要求中指定其他要求,例如:

此例中,我们使用 Addable<T> && std::integral<T> 作为新概念 IntegralAddable 的值。Addablestd::integral 是已有的两个概念,我们使用 && 符号将它们连接,意味着要想满足概念 IntegralAddable,必须既满足 Addable 又满足 std::integral

作为特例,有一种很特殊的概念:

template <typename T>
requires requires(T a, T b) { a + b; } // 注意 requires 需要使用两次;
T add(T a, T b)
template<typename T>
T add(T a, T b)
requires requires(T a, T b) { a + b; }  // 注意 requires 需要使用两次;

在你的要求集合比较短,并且只使用一次不想专门为其定义一个概念时,可以使用这种方法。

最后还有一种方法是引入 auto 关键字:

// a 的类型由 auto 推导后作为 From 模板类型传递给 std::convertible_to<From, To>
// 而显式指定的 int 类型则作为 To 模板类型传递给 std::convertible_to<From, To>
// 对于拥有更多模板类型参数的概念也是同理,auto 推导的类型永远是第一个模板类型
int toInt(std::convertible_to<int> auto a) {
    return a;

在对多个泛型参数进行约束时,我们也同样可以使用 &&, || 以及 !,例如:

template <typename T, typename U>
requires Addable<T> && std::equality_comparable_with<T, U>
bool add_then_eq(T a, T b, U c)
    return a + b == c;

该模板函数要求 T 类型可相加,并且 T 类型和 U 类型可以比较相等性。

lambda 与概念

在 C++14 中,我们迎来了泛型 lambda,为了不引入复杂的模板语法,C++14 为我们提供了简单易懂的 auto 语法:

// 这是一个泛型 lambda,可以传入任意类型的 a、b 参数,甚至 a 和 b 可以不是同一个类型
auto add = [](auto a, auto b) {
    return a + b;

然而,在 C++20 中,为了能够让泛型 lambda 也享受到概念语法带来的福利,最终 C++ 委员会还是选择将模板列表带给了 lambda(这何尝不是一种)。

当然,我们仍然可以使用下面这种方法抗拒模板列表:

同样的,requires 子句也可以放在形参列表的后面,也可以将 Addable 替换掉 typename,或者直接将概念 Addable 的定义替换上来。

标准库 concepts

在 C++20 中,标准库新增了头文件 concepts,其中定义了许多常用的概念可以供我们直接使用,此处介绍一些常用的标准库概念:

same_as<T, U>TU 为同一类型时才满足 derived_from<Derived, Base>BaseDerived 或是 Derived 的基类时才满足 convertible_to<From, To>From 类型能够隐式和显式转换为 To 类型时才满足 integral<T>T 类型为整数类型时才满足 signed_integral<T>T 类型为有符号整数类型时才满足 unsigned_integral<T>T 类型为无符号整数类型时才满足 floating_point<T>T 类型为浮点类型时才满足 assignable_from<LHS, RHS>RHS 类型的表达式能够赋值给 LHS 左值时才满足 swappable<T>T 类型的左值可交换时才满足 swappable_with<T, U>T 类型的左值和 U 类型的左值可互相交换时才满足 constructible_from<T, Args...>T 类型可以由参数类型集 Args... 构造时才满足 default_initializable<T>T 类型能够默认构造时才满足 move_initializable<T>T 类型能够移动构造时才满足 copy_initializable<T>T 类型能够拷贝构造和移动构造时才满足 equality_comparable<T> 当运算符 ==!= 能反应 T 类型上的相等性时才满足 equality_comparable_with<T, U> 当运算符 ==!= 能反应 T 类型与 U 类型之间的相等性时才满足 totally_ordered<T> 当比较运算符在 T 类型上严格全序时才满足 movable<T>T 类型可移动,即:能移动构造、移动赋值,左值能交换时才满足 copyable<T>T 类型可拷贝,即:既可以拷贝构造,又可移动时才满足 semiregular<T>T 类型既可拷贝,又可以默认构造时才满足 regular<T>T 类型既可拷贝,又可以默认构造,并且可以比较相等性时才满足

需要注意的是,与类型相关的概念,仅仅只是检测其类型是否为标准类型,而非检查其语义。这意味着 intconst int 满足 integral<T>,但是 int &const int & 却不满足 integral<T>,例如:

template <typename T> concept IntegralArrayType = requires(T a) { { a[0] } -> std::integral; a.size(); void do_nothing(IntegralArrayType auto a) {} int main() { std::vector<int> vec; do_nothing(vec);

这样的写法是无法通过编译的,因为 a[0] 在此处返回的类型是 int &,其不满足 std::integral 的约束。

想要让这段代码工作,我们可以引入一个新的概念来表示整数类型或者整数类型的引用:

template <typename T> concept Number = std::integral<typename std::remove_reference<T>::type>; template <typename T> concept IntegralArrayType = requires(T a) { { a[0] } -> Number; a.size(); void do_nothing(IntegralArrayType auto a) {} int main() { std::vector<int> vec; do_nothing(vec);