2020-11-20 发表 2022-08-22 更新 C++20新特性之concept C++20的concept特性极大增强了C++的模板功能。 本文简单介绍了为什么要使用concept以及concept的基本用法。 Concepts 是 C++ 模板功能的一种扩展。他被设计成编译期的一种检查措施,用于约束和限制传入模板的类型。 为什么需要 concept 有的时候会有类似这样的需求,即希望传入模板的类型不是任意类型,而是包含某个特定成员的类型。 比如下面这个 GetLength 函数,它希望传入的参数的类型(即类型T)包含 iterator 类型。 进一步考虑这个函数的实现,这个函数还会期待类型T包含 begin 成员函数和 end 成员函数。 1234567891011121314 template<typename T>size_t GetLength(const T& v){ // 这里用到了类型T中的iterator类型 // 如果T类型中不包含iterator类型,会生成一大堆编译错误 typedef T::iterator iterator; // 这里用到了T类型中的begin函数 iterator it = v.begin(); // 这里用到了T类型中的end函数 iterator end = v.end(); size_t result = 0; for (; it != end; ++it, ++result); return result;} 这个 GetLength 函数的正确使用方法是: 123 vector<int> test({1,2,3});cout << GetLength(test) << endl; // 输出 3cout << GetLength("Hello"s) << endl; // 输出 5 vector<int> 类和 string 类中都包含 iterator 类型,而且包含 begin 和 end 成员函数,所以上面的代码可以通过编译。 由于 GetLength 这个模板函数没有做出限制,所以你还可以传入其他类型的参数: 1 cout << GetLength(123) << endl; // 错误!而且是一大堆错误! 上面的这个例子是无法通过编译的,因为字面量 123 的类型是 int 。 int 类型没有成员类型 iterator ,也没有成员函数 begin 和 end 。 你会得到类似下面这些错误: 12345678910 error C2825: 'T': 当后面跟“::”时必须为类或命名空间message : 查看对正在编译的函数 模板 实例化“size_t GetLength<int>(T)”的引用with[ T=int]error C2510: “T”:“::”的左边必须是类/结构/联合error C4430: 缺少类型说明符 - 假定为 int。注意: C++ 不支持默认 interror C2146: 语法错误: 缺少“;”(在标识符“iterator”的前面)warning C4091: “”: 没有声明变量时忽略“int”的左侧 这些错误实在是不友好,令人摸不着头脑。 出现这些错误的原因是编译器会根据模板生成一个 GetLength<int>(int v) 函数。很显然,不存在 int::iterator ,也不存在 v.begin() 和 v.end() ,编译这个函数的时候肯定会报错。 这就是需要 concept 的原因之一。 使用 concept 可以约束传入模板的类型,对于不满足条件的类型,就不进行模板实例化,可以避免复杂的报错,从而方便定位错误。在介绍 concept 之前,先介绍一种不使用 concept,但是可以达到同样目的的编程技巧。(这个部分可以跳过,不影响理解 concept) 利用SFINAE原则的技巧 SFINAE 是 Substitution Failure Is Not An Error 的缩写,是C++编译模板时的一个原则。这个原则的意思是:在解析模板重载时,如果无法替换模板的参数,则寻找下一个重载,而不是抛出编译错误。 123456789101112131415 struct Test { typedef int foo;};template <typename T>void f(typename T::foo) {} // Definition #1template <typename T>void f(T) {} // Definition #2int main() { f<Test>(10); // Call #1. f<int>(10); // Call #2. Without error (even though there is no int::foo) // thanks to SFINAE.} 虽然 Definition #1 和 Call #2 不匹配(因为没有int::foo),但是编译器没有就此抛出错误,而是继续尝试另一个模板重载,即 Definition #2。 SFINAE 原则最初是应用于上述的模板编译过程。后来被C++开发者发现可以用于做编译期的决策,配合sizeof可以进行一些判断:类是否定义了某个内嵌类型、类是否包含某个成员函数等。 考虑下面这个例子: 123456789101112131415161718192021222324252627282930313233 #include <iostream>#include <vector>template <typename T>struct has_typedef_iterator { // Types "yes" and "no" are guaranteed to have different sizes, // specifically sizeof(yes) == 1 and sizeof(no) == 2. typedef char yes[1]; typedef char no[2]; template <typename C> static yes& test(typename C::iterator*); // Definition #1 template <typename> static no& test(...); // Definition #2 // If the "sizeof" of the result of calling test<T>(nullptr) is equal to sizeof(yes), // the first overload worked and T has a nested type named iterator. static const bool value = sizeof(test<T>(nullptr)) == sizeof(yes);};struct foo { typedef float iterator;};int main() { std::cout << std::boolalpha; std::cout << has_typedef_iterator<foo>::value << std::endl;//true std::cout << has_typedef_iterator<int>::value << std::endl;//false std::cout << has_typedef_iterator<std::vector<int> >::value << std::endl;//true return 0;} 类型 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 函数。 1234567891011 template<typename T, typename = std::enable_if<has_typedef_iterator<T>::value>::type> // 这一行是关键size_t GetLength(T v){ typedef T::iterator iterator; iterator it = v.begin(); iterator end = v.end(); size_t result = 0; for (; it != end; ++it, ++result); return result;} 使用方法也没有任何变化: 1234 vector<int> test({1,2,3});cout << GetLength(test) << endl; // 输出 3cout << GetLength("Hello"s) << endl; // 输出 5cout << GetLength(1234) << endl; // 这一行是错误的! 唯一的区别是错误使用时,编译器的错误变成了: 123 error C2672: “GetLength”: 未找到匹配的重载函数error C2783: “size_t GetLength(T)”: 未能为“<unnamed-symbol>”推导 模板 参数message : 参见“GetLength”的声明 改进之前,如果错误使用 GetLength 函数,编译器仍然会实例化模板,然后进行编译,从而导致一连串错误。 改进之后,错误使用 GetLength 函数,编译器将停止实例化模板,然后提示 未找到匹配的重载函数 。 从报错的友好程度来看,这个改进简直进步巨大! 那么这是如何做到的呢? enable_if 的定义非常简单。标准库的代码往往都是晦涩难懂的代码,但 enable_if 的代码却很简单,下面是VC++标准库中 enable_if 的实现: 12345678 // STRUCT TEMPLATE enable_iftemplate <bool _Test, class _Ty = void>struct enable_if {}; // no member "type" when !_Testtemplate <class _Ty>struct enable_if<true, _Ty> { // type is _Ty for _Test using type = _Ty;}; 如果 _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 的标准语法是: 12 template < template-parameter-list >concept concept-name = constraint-expression; 比如约束类型T是类型U的派生类: 12 template <class T, class U>concept Derived = std::is_base_of<U, T>::value; 将前文提到的 has_typedef_iterator 用concept的形式改写: 1234567 template<typename T>concept has_iterator = requires(T v){ T::iterator; v.begin(); v.end();}; has_iterator是concept的名称。 requires(T v) { /*...*/ }; 这部分可以看作是一个函数。 这个函数包含对模板的约束。 在本例中,对模板的约束为: 类型T包含一个内嵌的类型 iterator 类型T的对象 v 包含名称为 begin() 的成员函数 类型T的对象 v 包含名称为 end() 的成员函数 使用concept concept有下面这三类使用方式: 方式1,将 requires 写在函数后面: 1234567891011 // 方式1template<typename T>size_t GetLength(T v) requires has_iterator<T>{ typedef T::iterator iterator; iterator it = v.begin(); iterator end = v.end(); size_t result = 0; for (; it != end; ++it, ++result); return result;} 方式2,将 requires 写在 template 下方: 123456789101112 // 方式2template<typename T>requires has_iterator<T>size_t GetLength(T v) { typedef T::iterator iterator; iterator it = v.begin(); iterator end = v.end(); size_t result = 0; for (; it != end; ++it, ++result); return result;} 方式3,使用concept名称取代模板关键词typename/class: 1234567891011 // 方式3template<has_iterator T>size_t GetLength(T v){ typedef T::iterator iterator; iterator it = v.begin(); iterator end = v.end(); size_t result = 0; for (; it != end; ++it, ++result); return result;} 此外,concept还可以使用逻辑运算符 && 和 || 。例如: 12345678 template <class T>concept Integral = std::is_integral<T>::value;template <class T>concept SignedIntegral = Integral<T> && std::is_signed<T>::value;template <class T>concept UnsignedIntegral = Integral<T> && !SignedIntegral<T>; 使用concept对模板进行约束后,如果错误使用模板,会出现类似下面的错误: 123 error C2672: “GetLength”: 未找到匹配的重载函数error C7602: “GetLength”: 未满足关联约束message : 参见“GetLength”的声明 模板的报错更加友好了。 requires 关键词 requires 关键词总共有两个作用,一个是定义requires-expressions,用来描述和模板参数有关的约束条件;另一个是引入模板需要的约束。 123456 template<typename T>concept has_iterator = requires(T v){/*...*/}; // 定义 requires-expressionstemplate<typename T>requires has_iterator<T> // 引入约束条件size_t GetLength(T v) {} 特别的,上面的 GetLength 函数可以写成这样: 12345678910111213141516 template<typename T>requires requires(T v) // 注意,这里有两个 requires{ T::iterator; v.begin(); v.end();}size_t GetLength2(T v) { typedef T::iterator iterator; iterator it = v.begin(); iterator end = v.end(); size_t result = 0; for (; it != end; ++it, ++result); return result;} 标准库的concepts 标准库中已经提供了一些常用的concept,位于 concepts 头文件中,如 derived_from 、 integral 等。