请选择 进入手机版 | 继续访问电脑版

C++模板元编程(六):约束和概念

发表于 2023-1-23 22:12:36 显示全部楼层 0 1320

6. Constraints and Concepts
概念(Concepts)是对模板实参的一些约束(Constraints)的集合,是 C++20 引入的新特性。这些约束可以被用于选择最恰当的函数模板重载和类模板偏特化。
相较于传统的技术手段,它的优势有两个:一是语法更简单的同时功能也更强大;二是编译器产生的错误信息更易理解。

在前面我们已经看到了使用 SFINAE 来选择重载和特化的用法了,对比一下我们就可以看到 Concept 的优势:


  1. // with SFINAE:
  2. template <typename T>
  3. static constexpr bool is_numeric_v = std::is_integral_v<T> || std::is_floating_point_v<T>;

  4. template <typename T>
  5. std::enable_if_t<is_numeric_v<T>, void> foo(T);

  6. // with Concept:
  7. template <typename T>
  8. concept Numeric = std::is_integral_v<T> || std::is_floating_point_v<T>;

  9. template <Numeric T>
  10. void foo(T);
复制代码


另外,由于编译时对约束规则的检查发生在模板的实例化之前,所以此时产生的错误信息更容易理解。
特别是当涉及嵌套多层的模板实例化时,错误信息基本没法看。你可以测试一下编译下面这两行代码:


  1. std::list<int> l = {3,-1,10};
  2. std::sort(l.begin(), l.end());
复制代码


在我的电脑上,它产生的错误信息有472行:


  1. || /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 *>')
  2. ||         difference_type __len = __last - __first;
  3. ||                                 ~~~~~~ ^ ~~~~~~~
  4. || /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
  5. ||     _VSTD::__sort<_Comp_ref>(__first, __last, _Comp_ref(__comp));
  6. ||            ^
  7. || /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
  8. ||     _VSTD::sort(__first, __last, __less<typename iterator_traits<_RandomAccessIterator>::value_type>());
  9. ||            ^
  10. || /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
  11. ||   std::sort(l.begin(), l.end());
  12. ||        ^
  13. || /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'
  14. || operator-(const reverse_iterator<_Iter1>& __x, const reverse_iterator<_Iter2>& __y)
  15. || ^
  16. || /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'
  17. || operator-(const move_iterator<_Iter1>& __x, const move_iterator<_Iter2>& __y)
  18. || ^
  19. || /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'
  20. || operator-(const __wrap_iter<_Iter1>& __x, const __wrap_iter<_Iter2>& __y) _NOEXCEPT
  21. || ^
  22. || /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/c++/v1/string:560:11: note: candidate template ignored: could not match 'fpos' against '__list_iterator'
  23. || streamoff operator-(const fpos<_StateT>& __x, const fpos<_StateT>& __y)
  24. ||
  25. || ......
复制代码


而如果使用支持 Concept 的编译器,报错信息大概长这样:


  1. error: cannot call std::sort with std::_List_iterator<int>
  2. note:  concept RandomAccessIterator<std::_List_iterator<int>> was not satisfied
复制代码


Concept 的声明语句形如:


  1. template <...>
  2. 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 从句:


  1. template <Numeric T>
  2. void foo(T) {}

  3. template <typename T> requires Numeric<T>
  4. void foo(T) {}

  5. template <typename T>
  6. void foo(T) requires Numeric<T> {}

  7. template <typename T> requires std::is_integral_v<T> || std::is_floating_point_v<T>
  8. void foo(T) {}

  9. template <typename T>
  10. 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 的例子:


  1. template <typename T> concept Incrementable = requires(T v) { ++v; };
  2. template <typename T> concept Decrementable = requires(T v) { --v; };

  3. template <typename From, typename To>
  4. concept ConvertibleTo = std::is_convertible_v<From, To> &&
  5.     requires(std::add_rvalue_reference_t<From> (&f)()) {
  6.   static_cast<To>(f());
  7. };

  8. template <typename T, typename U = T>
  9. concept Swappable = requires(T&& t, U&& u) {
  10.     swap(std::forward<T>(t), std::forward<U>(u));
  11.     swap(std::forward<U>(u), std::forward<T>(t));
  12. };
复制代码


另外,有些写法是 requires 表达式独有的,比如下面的 Hashable,
它判断 a 能否传给 std::hash,以及 std::hash 的返回值类型能否转型为 std::size_t:


  1. template<typename T>
  2. concept Hashable = requires(T a) {
  3.     { std::hash<T>{}(a) } -> ConvertibleTo<std::size_t>;
  4. };
复制代码


除了用在 Concept 的声明中外,requires 表达式还可以直接用在 requires 从句中:


  1. template<typename T> concept Addable = requires (T x) { x + x; };  // requires expression

  2. template<typename T> requires Addable<T>   // requires clause
  3. T add(T a, T b) { return a + b; }

  4. template<typename T> requires requires (T x) { x + x; }   // requires expression in requires clause
  5. T add(T a, T b) { return a + b; }
复制代码


最后,约束出现的顺序决定了编译器检查的顺序,所以下面两个函数模板虽然在逻辑上是等效的,但他们拥有不同的约束,不算是重定义:


  1. template <Incrementable T>
  2. void g(T) requires Decrementable<T> {};

  3. template <Decrementable T>
  4. 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)。
它们的关系是这样的:
v2-6866ca2d701fa24b7bfd1d99a0b1e86c_r.jpg

你可能也已经知道标准库中的泛型排序函数 std::sort 只接受 RandomAccessIerator 的迭代器参数:


  1. template< typename RandomIt >
  2. 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 的出现对于软件设计也是有积极意义的。


  1. // dynamic polymorphic
  2. struct WorkerInterface {
  3.   virtual int work(int) = 0;
  4. };

  5. struct WorkerImpl : WorkerInterface {
  6.   int work(int) override { return 1; }
  7. };

  8. int do_work(WorkerInterface* w) { return w->work(1); }

  9. // static polymorphic
  10. template <typename T>
  11. concept worker = requires(T a) {
  12.   { a.work(int()) } -> std::same_as<int>;
  13. };

  14. template <worker T>
  15. int do_work(T&& w) { return w.work(1); }

复制代码

回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

shop_da_admin

管理员

39

主题

40

帖子

247

积分
Ta的主页 发消息

网友分享更多 >

  • 机器学习的统计学知识
  • 漳州盛泰水产
  • 玉川茶家