|
6. Constraints and Concepts
概念(Concepts)是对模板实参的一些约束(Constraints)的集合,是 C++20 引入的新特性。这些约束可以被用于选择最恰当的函数模板重载和类模板偏特化。
相较于传统的技术手段,它的优势有两个:一是语法更简单的同时功能也更强大;二是编译器产生的错误信息更易理解。
在前面我们已经看到了使用 SFINAE 来选择重载和特化的用法了,对比一下我们就可以看到 Concept 的优势:
- // with SFINAE:
- template <typename T>
- static constexpr bool is_numeric_v = std::is_integral_v<T> || std::is_floating_point_v<T>;
- template <typename T>
- std::enable_if_t<is_numeric_v<T>, void> foo(T);
- // with Concept:
- template <typename T>
- concept Numeric = std::is_integral_v<T> || std::is_floating_point_v<T>;
- template <Numeric T>
- void foo(T);
复制代码
另外,由于编译时对约束规则的检查发生在模板的实例化之前,所以此时产生的错误信息更容易理解。
特别是当涉及嵌套多层的模板实例化时,错误信息基本没法看。你可以测试一下编译下面这两行代码:
- std::list<int> l = {3,-1,10};
- std::sort(l.begin(), l.end());
复制代码
在我的电脑上,它产生的错误信息有472行:
- || /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/c++/v1/algorithm:3961:40: error: invalid operands to binary expression ('std::__1::__list_iterator<int, void *>' and 'std::__1::__list_iterator<int, void *>')
- || difference_type __len = __last - __first;
- || ~~~~~~ ^ ~~~~~~~
- || /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/c++/v1/algorithm:4149:12: note: in instantiation of function template specialization 'std::__1::__sort<std::__1::__less<int> &, std::__1::__list_iterator<int, void *>>' requested here
- || _VSTD::__sort<_Comp_ref>(__first, __last, _Comp_ref(__comp));
- || ^
- || /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/c++/v1/algorithm:4157:12: note: in instantiation of function template specialization 'std::__1::sort<std::__1::__list_iterator<int, void *>, std::__1::__less<int>>' requested here
- || _VSTD::sort(__first, __last, __less<typename iterator_traits<_RandomAccessIterator>::value_type>());
- || ^
- || /Users/guoang/test/test.cpp:23:8: note: in instantiation of function template specialization 'std::__1::sort<std::__1::__list_iterator<int, void *>>' requested here
- || std::sort(l.begin(), l.end());
- || ^
- || /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/c++/v1/iterator:855:1: note: candidate template ignored: could not match 'reverse_iterator' against '__list_iterator'
- || operator-(const reverse_iterator<_Iter1>& __x, const reverse_iterator<_Iter2>& __y)
- || ^
- || /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/c++/v1/iterator:1311:1: note: candidate template ignored: could not match 'move_iterator' against '__list_iterator'
- || operator-(const move_iterator<_Iter1>& __x, const move_iterator<_Iter2>& __y)
- || ^
- || /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/c++/v1/iterator:1719:1: note: candidate template ignored: could not match '__wrap_iter' against '__list_iterator'
- || operator-(const __wrap_iter<_Iter1>& __x, const __wrap_iter<_Iter2>& __y) _NOEXCEPT
- || ^
- || /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/c++/v1/string:560:11: note: candidate template ignored: could not match 'fpos' against '__list_iterator'
- || streamoff operator-(const fpos<_StateT>& __x, const fpos<_StateT>& __y)
- ||
- || ......
复制代码
而如果使用支持 Concept 的编译器,报错信息大概长这样:
- error: cannot call std::sort with std::_List_iterator<int>
- note: concept RandomAccessIterator<std::_List_iterator<int>> was not satisfied
复制代码
Concept 的声明语句形如:
- template <...>
- concept _name_ = _constraint_expression_;
复制代码
一个约束表达式就是一个对模板形参的逻辑运算表达式(与、或、非),它指定对于模板实参的要求。
例如上面看到的 std::is_integral_v<T> || std::is_floating_point_v<T> 就是一个约束表达式。
与模板实例化中的逻辑运算表达式不同,约束表达式中的逻辑运算是短路求值的。
约束表达式可以出现在 Concept 的声明中,比如 template <typename T> Numerical = std::is_integral_v<T> || std::is_floating_point_v<T>;
也可以出现在 requires 从句中。
requires 关键字用来引入 Requires 从句(Requires Clause),requires 从句可以放在函数模板的签名里,用来表示约束。
requires 关键字后面必须跟一个常量表达式,可以是 true/false,可以是 Concept 表达式、Concept 的合取/析取,
也可以是约束表达式,还可以是 requires 表达式。
下面的这些写法都是等效的,其中第一个是在形参列表中直接使用 Concept,
第二第三个是使用 Concept 的 requires 从句,
最后两个是使用约束表达式的 requires 从句:
- template <Numeric T>
- void foo(T) {}
- template <typename T> requires Numeric<T>
- void foo(T) {}
- template <typename T>
- void foo(T) requires Numeric<T> {}
- template <typename T> requires std::is_integral_v<T> || std::is_floating_point_v<T>
- void foo(T) {}
- template <typename T>
- void foo(T) requires std::is_integral_v<T> || std::is_floating_point_v<T> {}
复制代码
requires 关键字还可以用来引入一个 Requires 表达式(Requires Expression)。
它是一个 bool 类型的纯右值表达式,描述一些对模板实参的约束。
若约束满足(表达式良构)则返回 true;否则返回 false。requires 表达式形如:requires (parameters) { requirement-sequences }。
下面是用 requires 表达式来声明 Concept 的例子:
- template <typename T> concept Incrementable = requires(T v) { ++v; };
- template <typename T> concept Decrementable = requires(T v) { --v; };
- template <typename From, typename To>
- concept ConvertibleTo = std::is_convertible_v<From, To> &&
- requires(std::add_rvalue_reference_t<From> (&f)()) {
- static_cast<To>(f());
- };
- template <typename T, typename U = T>
- concept Swappable = requires(T&& t, U&& u) {
- swap(std::forward<T>(t), std::forward<U>(u));
- swap(std::forward<U>(u), std::forward<T>(t));
- };
复制代码
另外,有些写法是 requires 表达式独有的,比如下面的 Hashable,
它判断 a 能否传给 std::hash,以及 std::hash 的返回值类型能否转型为 std::size_t:
- template<typename T>
- concept Hashable = requires(T a) {
- { std::hash<T>{}(a) } -> ConvertibleTo<std::size_t>;
- };
复制代码
除了用在 Concept 的声明中外,requires 表达式还可以直接用在 requires 从句中:
- template<typename T> concept Addable = requires (T x) { x + x; }; // requires expression
- template<typename T> requires Addable<T> // requires clause
- T add(T a, T b) { return a + b; }
- template<typename T> requires requires (T x) { x + x; } // requires expression in requires clause
- T add(T a, T b) { return a + b; }
复制代码
最后,约束出现的顺序决定了编译器检查的顺序,所以下面两个函数模板虽然在逻辑上是等效的,但他们拥有不同的约束,不算是重定义:
- template <Incrementable T>
- void g(T) requires Decrementable<T> {};
- template <Decrementable T>
- void g(T) requires Incrementable<T> {};
复制代码
但是,我在这里更想讨论的,不是 concept 的语法,而是它的意义。
concept 虽然最近(C++20)才被写入标准里,但它其实是一个历史非常悠久的东西,可以说它是伴随着泛型编程而生的。
concept 其实一直都存在,只是 C++ 直到 C++20 才在语法上支持了 concept。
对 concept 的讨论触及元编程的一个核心问题,就是我们为什么需要元编程、什么情况下需要用到元编程。
我在 1.4 Why should we Learn TMP? 中列举了 4 条原因,其中第二条是我们有时需要对类型做计算,我们在这里进一步讨论这个问题。
你可能已经听说过一些 concept,比如 Iterator Categories,
标准库中一共有 6 种 Iterator,分别是:InputIterator, OutputIterator, ForwardIterator, BidirectionalIterator, RandomAccessIterator, 和 ContiguousIterator(since C++17)。
它们的关系是这样的:
你可能也已经知道标准库中的泛型排序函数 std::sort 只接受 RandomAccessIerator 的迭代器参数:
- template< typename RandomIt >
- void sort( RandomIt first, RandomIt last );
复制代码
这里的 RandomAccessIterator 实际上就是一个概念(concept),它描述了 std::sort 对其参数类型的要求。
这样的要求在标准库中有很多,它们被统称为具名要求(Named Requirements)。
这些东西本质上都是 concept,但它们早在 concept 的语法出现之前,就已经存在很久了。
因为对 concept 的需求是与泛型编程伴生的:我们希望泛型,但又不是完全的泛型,对传入的类型仍有一定的要求。
比如 std::sort 就要求它的参数类型是支持随机访问的迭代器,而这个要求是源自于快排算法的,是 std::sort 自身本质的要求,是一种必然的要求。
从这个层面来说,C++ 对 concept 的语法的支持,来得太晚了。
在没有这种语法支持的时候,我们只能通过类似 enable_if 或者其他比较原始的 TMP 手段来实现对具名要求的检查,concept 语法的出现,大大简化了泛型编程和元编程的难度。
我们再进一步从软件设计的角度讨论这个问题。
concept 其实代表了我们在设计中对某一类实体的抽象。
假如说我们想实现一种接口与实现分离的设计,接口是统一的,而实现有多种,甚至用户可以自定义实现,传统的做法是怎样的呢?
我们会实现一个纯虚的基类 "Interface",在里面定义所有纯虚的接口,然后所有的实现都继承这个基类,在派生类里提供具体实现。
这带来两个问题,一是你必须通过基类指针来操作接口,通过运行时多态的机制访问实现,这是有成本的,而有时候你并不需要在运行时变换实现,
在编译时就能确定你想要用哪个实现,但你扔避免不了运行时的成本;
二是这种约束太强了,不仅约束了实现类的类型,还约束了所有接口的参数类型和返回值类型。
但是有了 concept 后,我们不需要基类,只需要通过 concept 声明一系列对类型和接口的约束就可以了,比如我们可以约束这个类型必须包含一个名为 "work" 的接口,这个接口接受一个数值类型参数,返回一个数值类型参数。
所有的实现不论是什么类型,只需要满足这个约束,就可以拿来使用。
这种对类型的约束有点像 Python 的 Duck Type:当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。
并且这种对接口的约束可以是严格的,也可以是松散的,比如我可以要求一个接口使用 int 型的参数,也可以要求它接受所有数值类型的参数。
所以从这个层面来说,concept 的出现对于软件设计也是有积极意义的。
- // dynamic polymorphic
- struct WorkerInterface {
- virtual int work(int) = 0;
- };
- struct WorkerImpl : WorkerInterface {
- int work(int) override { return 1; }
- };
- int do_work(WorkerInterface* w) { return w->work(1); }
- // static polymorphic
- template <typename T>
- concept worker = requires(T a) {
- { a.work(int()) } -> std::same_as<int>;
- };
- template <worker T>
- int do_work(T&& w) { return w.work(1); }
复制代码
|
|