C++20新特性之concept
C++20的concept特性极大增强了C++的模板功能。
本文简单介绍了为什么要使用concept以及concept的基本用法。
Concepts 是 C++ 模板功能的一种扩展。他被设计成编译期的一种检查措施,用于约束和限制传入模板的类型。
为什么需要 concept
有的时候会有类似这样的需求,即希望传入模板的类型不是任意类型,而是包含某个特定成员的类型。
比如下面这个 GetLength
函数,它希望传入的参数的类型(即类型T)包含 iterator
类型。
进一步考虑这个函数的实现,这个函数还会期待类型T包含begin
成员函数和end
成员函数。
1 | template<typename T> |
这个 GetLength
函数的正确使用方法是:
1 | vector<int> test({1,2,3}); |
vector<int>
类和 string
类中都包含 iterator
类型,而且包含 begin
和 end
成员函数,所以上面的代码可以通过编译。
由于GetLength
这个模板函数没有做出限制,所以你还可以传入其他类型的参数:
1 | cout << GetLength(123) << endl; // 错误!而且是一大堆错误! |
上面的这个例子是无法通过编译的,因为字面量123
的类型是int
。int
类型没有成员类型iterator
,也没有成员函数begin
和end
。
你会得到类似下面这些错误:
1 | error C2825: 'T': 当后面跟“::”时必须为类或命名空间 |
这些错误实在是不友好,令人摸不着头脑。
出现这些错误的原因是编译器会根据模板生成一个GetLength<int>(int v)
函数。很显然,不存在int::iterator
,也不存在v.begin()
和v.end()
,编译这个函数的时候肯定会报错。
这就是需要 concept 的原因之一。 使用 concept 可以约束传入模板的类型,对于不满足条件的类型,就不进行模板实例化,可以避免复杂的报错,从而方便定位错误。在介绍 concept 之前,先介绍一种不使用 concept,但是可以达到同样目的的编程技巧。(这个部分可以跳过,不影响理解 concept)
利用SFINAE原则的技巧
SFINAE 是 Substitution Failure Is Not An Error 的缩写,是C++编译模板时的一个原则。这个原则的意思是:在解析模板重载时,如果无法替换模板的参数,则寻找下一个重载,而不是抛出编译错误。
1 | struct Test { |
虽然 Definition #1 和 Call #2 不匹配(因为没有int::foo),但是编译器没有就此抛出错误,而是继续尝试另一个模板重载,即 Definition #2。
SFINAE 原则最初是应用于上述的模板编译过程。后来被C++开发者发现可以用于做编译期的决策,配合sizeof可以进行一些判断:类是否定义了某个内嵌类型、类是否包含某个成员函数等。
考虑下面这个例子:
1 |
|
类型 foo
定义了内嵌类型 iterator
,与Definition #1匹配。而Definition #1 的返回值类型为yes
,因此value
的值为sizeof(yes) == sizeof(yes)
,即 true
。
类型int
没有定义内嵌类型 iterator
,与Definition #1 不匹配。根据 SFINAE 原则,此时不会抛出编译错误,而是尝试另一个模板重载,即Definition #2。因为Definition #2的返回值类型为 no
, value
的值为sizeof(no) == sizeof(yes)
,即 false
。
现代C++可以用更少的代码实现上文提到的
has_typedef_iterator
,参考资料[2]这篇文章提到了这一点。
enable_if
enable_if 是标准库中定义的一个模板。实际上 enable_if 的原理也是 SFINAE 原则。通过 enable_if 可以按条件约束、限制模板类型T。
下面使用 enable_if 和 has_typedef_iterator 改进前文提到的 GetLength 函数。
1 | template<typename T, |
使用方法也没有任何变化:
1 | vector<int> test({1,2,3}); |
唯一的区别是错误使用时,编译器的错误变成了:
1 | error C2672: “GetLength”: 未找到匹配的重载函数 |
改进之前,如果错误使用GetLength
函数,编译器仍然会实例化模板,然后进行编译,从而导致一连串错误。
改进之后,错误使用GetLength
函数,编译器将停止实例化模板,然后提示 未找到匹配的重载函数。
从报错的友好程度来看,这个改进简直进步巨大!
那么这是如何做到的呢?
enable_if 的定义非常简单。标准库的代码往往都是晦涩难懂的代码,但 enable_if 的代码却很简单,下面是VC++标准库中 enable_if 的实现:
1 | // STRUCT TEMPLATE enable_if |
如果 _Test
的值为 false
,那么enable_if是一个空的struct。反之,如果 _Test
为 true
,enable_if 中会定义一个成员type,默认值为void,或等于传入的_Ty。
考虑改进后的GetLength中关键的一行:
1 | typename = std::enable_if<has_typedef_iterator<T>::value>::type |
当 has_typedef_iterator<T>::value
为 true
时,enable_if 包含 type 成员,因此这一句代码可以被编译器实例化。
而当 has_typedef_iterator<T>::value
为 false
时,enable_if 不包含任何成员,但是这里又调用了 enable_if::type
,出现了不匹配,无法继续实例化。
下面的内容尚未完成… \
可能含有错误
concept如何使用
前面介绍了利用 SFINAE 原则的编程技巧,下面开始介绍C++20引入的concept特性。
concept可以完全取代SFINAE技巧,而且写出来的代码更加简洁、易读。concept在编译期被计算、对模板进行约束。
定义一个concept
定义 concept 的标准语法是:
1 | template < template-parameter-list > |
比如约束类型T是类型U的派生类:
1 | template <class T, class U> |
将前文提到的has_typedef_iterator
用concept的形式改写:
1 | template<typename T> |
has_iterator是concept的名称。requires(T v) { /*...*/ };
这部分可以看作是一个函数。
这个函数包含对模板的约束。
在本例中,对模板的约束为:
- 类型T包含一个内嵌的类型
iterator
- 类型T的对象
v
包含名称为begin()
的成员函数 - 类型T的对象
v
包含名称为end()
的成员函数
使用concept
concept有下面这三类使用方式:
方式1,将 requires 写在函数后面:
1 | // 方式1 |
方式2,将 requires 写在 template 下方:
1 | // 方式2 |
方式3,使用concept名称取代模板关键词typename/class:
1 | // 方式3 |
此外,concept还可以使用逻辑运算符 &&
和 ||
。例如:
1 | template <class T> |
使用concept对模板进行约束后,如果错误使用模板,会出现类似下面的错误:
1 | error C2672: “GetLength”: 未找到匹配的重载函数 |
模板的报错更加友好了。
requires 关键词
requires 关键词总共有两个作用,一个是定义requires-expressions,用来描述和模板参数有关的约束条件;另一个是引入模板需要的约束。
1 | template<typename T> |
特别的,上面的 GetLength 函数可以写成这样:
1 | template<typename T> |
标准库的concepts
标准库中已经提供了一些常用的concept,位于concepts头文件中,如derived_from
、integral
等。
总结
略
参考资料
- Concepts (C++), https://en.wikipedia.org/wiki/Concepts_(C%2B%2B)
- C++模板技术之SFINAE与enable_if的使用 - Ying’s Blog, https://izualzhy.cn/SFINAE-and-enable_if
- std::enable_if - cppreference.com, https://en.cppreference.com/w/cpp/types/enable_if
- Templated check for the existence of a class member function? - Stack Overflow, https://stackoverflow.com/questions/257288/templated-check-for-the-existence-of-a-class-member-function
C++20新特性之concept