Concept简介

谈及C++20,众多人都说这是能匹敌C++11的更新,而在20的众多更新中,Concept,Module,Corutine,Range被称为Big4,而今天文中主角便是Concept。

简单来说,Concept是用来限制模板中类型参数的一个语法糖,在C++20以前,如果我们想对模板参数的类型做一些限制,就得用一种叫做SFINAE的模板编程技巧,我对这个并没有太深入的理解,但是看到模板参数里面一段长长的enable_if,我还是深感其可怕。但是到了C++20,我们就可以有非常漂亮简洁的写法了(Concept:无所谓,我会出手)。

不过首先先推荐一下这个视频吧,算是我C++20的启蒙,How C++20 Changes the Way We Write Code。下面所举的例子也是视频中的例子,你可以在这里找到我写的测试代码。

关于Concept的一百种语法以及一个小例子

先考虑这么一个需求,我们需要判断一个数是不是2的幂,我们可以写出以下代码。

1
2
3
4
5
template <typename T>
bool is_power_of_2(T target) {
std::cout << "Integral part!\n";
return (target > 0) && (target & (target - 1)) == 0;
}

但是显而易见的一件事是位操作只对整数有效,假如我们想调用is_power_of_2(0.25), 很明显就会出错。于是我们很自然的想去为浮点数也写一个实现。

1
2
3
4
5
6
7
8
template <typename T>
bool is_power_of_2(T target) {
// 将浮点数分解为尾数和指数,然后查看尾数是不是0.5
std::cout << "Float part!\n";
int exponent;
const T mantissa = std::frexp(target, &exponent);
return mantissa == T(0.5);
}

但是有一个问题在于,这明显是模板函数重定义,所以正常来讲我们应该把其更新为模板特化实现,并且这毫无问题,但是模板特化的问题在于一次只能特化一个类型,本质上感觉就是在给模板函数写函数重载一样,比如你给float得写一个,然后你还得给double再写一个,这明显不合理。而因为有了C++20,我们便可以利用Concept来实现整个过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
template <typename T> requires std::integral<T>
bool is_power_of_2(T target) {
std::cout << "Integral part!\n";
return (target > 0) && (target & (target - 1)) == 0;
}

template <typename T> requires std::floating_point<T>
bool is_power_of_2(T target) {
std::cout << "Float part!\n";
int exponent;
const T mantissa = std::frexp(target, &exponent);
return mantissa == T(0.5);
}

在这里requires子句来帮助我们完成了模板类型的检查,并且在报错的时候,这种写法的报错也会更加可读,而不是像以前的模板错误一样,一下子出来一大堆,生怕别人弄明白是怎么回事。这里的std::integral便是Concept,它定义了一种参数约束,如果你去看看源码的话,你就会发现其定义如下:

1
2
template <class _Ty>
concept integral = is_integral_v<_Ty>;

所以正如我之前所说,通过定义Concept,我们实际上是定义了一种模板参数中类型的“约束”,只有满足这个约束,我们才会去调用这个模板函数。定义完Concept之后,实际上我们有许多调用的方式,上面的requires子句其实只是其中一种。我们还可以将Concept直接塞到模板参数定义里面:

1
2
3
4
5
6
7
8
9
10
11
12
13
template <std::integral T>
bool is_power_of_2(T target) {
std::cout << "Integral part!\n";
return (target > 0) && (target & (target - 1)) == 0;
}

template <std::floating_point T>
bool is_power_of_2(T target) {
std::cout << "Float part!\n";
int exponent;
const T mantissa = std::frexp(target, &exponent);
return mantissa == T(0.5);
}

更进一步,我们甚至可以做到模板声明都不用写,直接上伟大的auto

1
2
3
4
5
6
7
8
9
10
11
bool is_power_of_2(std::integral auto target) {
std::cout << "Integral part!\n";
return (target > 0) && (target & (target - 1)) == 0;
}

bool is_power_of_2(std::floating_point auto target) {
std::cout << "Float part!\n";
int exponent;
const T mantissa = std::frexp(target, &exponent);
return mantissa == T(0.5);
}

Concept的组合

正如之前所说,Concept是对模板参数进行约束,而方式则是对一个布尔表达式求值,所以很自然的Concept之间是可以进行组合的。例如下面这个例子,我们将算数类型定义为整数类型以及浮点数类型的混合。

1
2
3
4
5
6
7
8
template <typename T>
concept arithmetic = std::integral<T> || std::floating_point<T>;

auto test_func(arithmetic auto target) {
cout << "Arithmetic part!\n";
// do something
return;
}

进一步,requires子句还可以拿来进行Concept的定义, 我们定义hashable类型为可以用来被std::hash所操作,并且返回一个可被转换为std::size_t的结果。

1
2
3
4
5
6
7
template <typename T>
concept hashable = requires(T t) {
{std::hash<T>{}(t)}->std::convertible_to<std::size_t>;
};

template <hashable T>
class hash_map;

总结

我并不是模板编程的专家,但是Concept确实让我感觉是一个很大的进步,希望将来标准委员会能更新的快一点,不然我就真润去rust了。