C++20新特性之concept

  1. 1. 为什么需要 concept
    1. 1.1. 利用SFINAE原则的技巧
    2. 1.2. enable_if
  2. 2. concept如何使用
    1. 2.1. 定义一个concept
    2. 2.2. 使用concept
    3. 2.3. requires 关键词
    4. 2.4. 标准库的concepts
  3. 3. 总结
  4. 4. 参考资料

C++20的concept特性极大增强了C++的模板功能。
本文简单介绍了为什么要使用concept以及concept的基本用法。

Concepts 是 C++ 模板功能的一种扩展。他被设计成编译期的一种检查措施,用于约束和限制传入模板的类型。

为什么需要 concept

有的时候会有类似这样的需求,即希望传入模板的类型不是任意类型,而是包含某个特定成员的类型。

比如下面这个 GetLength 函数,它希望传入的参数的类型(即类型T)包含 iterator 类型。

进一步考虑这个函数的实现,这个函数还会期待类型T包含begin成员函数和end成员函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 函数的正确使用方法是:

1
2
3
vector<int> test({1,2,3});
cout << GetLength(test) << endl; // 输出 3
cout << GetLength("Hello"s) << endl; // 输出 5

vector<int> 类和 string 类中都包含 iterator 类型,而且包含 beginend 成员函数,所以上面的代码可以通过编译。

由于GetLength这个模板函数没有做出限制,所以你还可以传入其他类型的参数:

1
cout << GetLength(123) << endl;    // 错误!而且是一大堆错误!

上面的这个例子是无法通过编译的,因为字面量123的类型是intint类型没有成员类型iterator,也没有成员函数beginend

你会得到类似下面这些错误:

1
2
3
4
5
6
7
8
9
10
error C2825: 'T': 当后面跟“::”时必须为类或命名空间
message : 查看对正在编译的函数 模板 实例化“size_t GetLength<int>(T)”的引用
with
[
T=int
]
error C2510: “T”:“::”的左边必须是类/结构/联合
error C4430: 缺少类型说明符 - 假定为 int。注意: C++ 不支持默认 int
error 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++编译模板时的一个原则。这个原则的意思是:在解析模板重载时,如果无法替换模板的参数,则寻找下一个重载,而不是抛出编译错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct Test {
typedef int foo;
};

template <typename T>
void f(typename T::foo) {} // Definition #1

template <typename T>
void f(T) {} // Definition #2

int 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可以进行一些判断:类是否定义了某个内嵌类型、类是否包含某个成员函数等。

考虑下面这个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#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 函数。

1
2
3
4
5
6
7
8
9
10
11
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;
}

使用方法也没有任何变化:

1
2
3
4
vector<int> test({1,2,3});
cout << GetLength(test) << endl; // 输出 3
cout << GetLength("Hello"s) << endl; // 输出 5
cout << GetLength(1234) << endl; // 这一行是错误的!

唯一的区别是错误使用时,编译器的错误变成了:

1
2
3
error C2672: “GetLength”: 未找到匹配的重载函数
error C2783: “size_t GetLength(T)”: 未能为“<unnamed-symbol>”推导 模板 参数
message : 参见“GetLength”的声明

改进之前,如果错误使用GetLength函数,编译器仍然会实例化模板,然后进行编译,从而导致一连串错误。

改进之后,错误使用GetLength函数,编译器将停止实例化模板,然后提示 未找到匹配的重载函数

从报错的友好程度来看,这个改进简直进步巨大!

那么这是如何做到的呢?

enable_if 的定义非常简单。标准库的代码往往都是晦涩难懂的代码,但 enable_if 的代码却很简单,下面是VC++标准库中 enable_if 的实现:

1
2
3
4
5
6
7
8
// STRUCT TEMPLATE enable_if
template <bool _Test, class _Ty = void>
struct enable_if {}; // no member "type" when !_Test

template <class _Ty>
struct enable_if<true, _Ty> { // type is _Ty for _Test
using type = _Ty;
};

如果 _Test 的值为 false,那么enable_if是一个空的struct。反之,如果 _Testtrue,enable_if 中会定义一个成员type,默认值为void,或等于传入的_Ty。

考虑改进后的GetLength中关键的一行:

1
typename = std::enable_if<has_typedef_iterator<T>::value>::type

has_typedef_iterator<T>::valuetrue 时,enable_if 包含 type 成员,因此这一句代码可以被编译器实例化。

而当 has_typedef_iterator<T>::valuefalse 时,enable_if 不包含任何成员,但是这里又调用了 enable_if::type,出现了不匹配,无法继续实例化。

下面的内容尚未完成… \
可能含有错误

concept如何使用

前面介绍了利用 SFINAE 原则的编程技巧,下面开始介绍C++20引入的concept特性。

concept可以完全取代SFINAE技巧,而且写出来的代码更加简洁、易读。concept在编译期被计算、对模板进行约束。

定义一个concept

定义 concept 的标准语法是:

1
2
template < template-parameter-list >
concept concept-name = constraint-expression;

比如约束类型T是类型U的派生类:

1
2
template <class T, class U>
concept Derived = std::is_base_of<U, T>::value;

将前文提到的has_typedef_iterator用concept的形式改写:

1
2
3
4
5
6
7
template<typename T>
concept has_iterator = requires(T v)
{
T::iterator;
v.begin();
v.end();
};

has_iterator是concept的名称。requires(T v) { /*...*/ };这部分可以看作是一个函数。
这个函数包含对模板的约束。

在本例中,对模板的约束为:

  1. 类型T包含一个内嵌的类型 iterator
  2. 类型T的对象v包含名称为begin()的成员函数
  3. 类型T的对象v包含名称为end()的成员函数

使用concept

concept有下面这三类使用方式:

方式1,将 requires 写在函数后面:

1
2
3
4
5
6
7
8
9
10
11
// 方式1
template<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 下方:

1
2
3
4
5
6
7
8
9
10
11
12
// 方式2
template<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:

1
2
3
4
5
6
7
8
9
10
11
// 方式3
template<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还可以使用逻辑运算符 &&||。例如:

1
2
3
4
5
6
7
8
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对模板进行约束后,如果错误使用模板,会出现类似下面的错误:

1
2
3
error C2672: “GetLength”: 未找到匹配的重载函数
error C7602: “GetLength”: 未满足关联约束
message : 参见“GetLength”的声明

模板的报错更加友好了。

requires 关键词

requires 关键词总共有两个作用,一个是定义requires-expressions,用来描述和模板参数有关的约束条件;另一个是引入模板需要的约束。

1
2
3
4
5
6
template<typename T>
concept has_iterator = requires(T v){/*...*/}; // 定义 requires-expressions

template<typename T>
requires has_iterator<T> // 引入约束条件
size_t GetLength(T v) {}

特别的,上面的 GetLength 函数可以写成这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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_fromintegral等。

总结

参考资料

  1. Concepts (C++), https://en.wikipedia.org/wiki/Concepts_(C%2B%2B)
  2. C++模板技术之SFINAE与enable_if的使用 - Ying’s Blog, https://izualzhy.cn/SFINAE-and-enable_if
  3. std::enable_if - cppreference.com, https://en.cppreference.com/w/cpp/types/enable_if
  4. 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