c++20 模板约束
concept
在 c++20 中,提案许久的 concept 被加入到标准中了,这也意味着不用再写恼人的 SFINAE 了(除非你是一个受虐狂,喜欢对着一堆报错中找到错误的位置)。
c++20 之前
在 c++20 之前,如果需要对模板实参进行编译期检查,只能使用 SFINAE ,或者是部分使用 c++17 添加的 if constexpr 进行编译期检查。
SFINAE: Substitution Failure Is Not An Error ,替换失败不是错误,也就是在对函数模板的重载决议中,如果在模板形参替换为显示指定的类型或者是推导出的类型失败时,那么这个推导将被丢弃,并不会导致最终的编译失败。
假设我们需要实现一个输出函数,这个函数使用 std::cout ,将任何传入的参数输出,我们先看看一个基础写法是怎么写的。
template <typename T>
auto WriteLine(const T &val) -> void {
std::cout << val << '\n';
}
很简单,对吧?问题来了,我们要怎么知道 std::cout << val << '\n' 中是否可以将 T 输出?一个简单的方式是写入,然后编译,结果自然是观察编译是否会出错。然而,如果你真的这么做了,就会得到一长串的 operator<< 错误,这实际上是标志着所有的重载函数都无法匹配上你所输入的类型,编译器不知道匹配哪一个,所以全部报错了。
SFINAE 怎么写?我们现在需要请出 std::enable_if 和 std::declval 两位佬了。std::enable_if 是 SFINAE 的核心,如果满足约束条件,那么将得到一个确定的类型,否则没有类型,使得模板实例化失败。现在我们看看该怎么写。
template <typename T>
auto WriteLine(const T &val)
-> typename std::enable_if<std::is_same<decltype(std::cout << val), std::ostream&>::value, void>::type {
std::cout << val << '\n';
}
哦天,后面一长串是什么?这里面使用了 std::is_same 判断两个类型是否是一样的,然后使用 decltype 推断 std::cout << val 的返回类型。由于 std::cout << val 输出后的返回类型为 std::ostream& ,如果它能够有匹配的输出,那么最终一定会成功,std::enable_if 将 void 作为 type ,否则这里 std::is_same 实例化将失败,也带着 std::enable_if 实例化失败,最终使得没有匹配的 WriteLine 。
如果你现在编译一下,会发现现在报错不会太多了,那么这解决我们需要的编译报错问题了吗?解决了,但是你需要花费巨大心智在模板上雕出一朵花,更不用说这只是一个简单的场景了,如果这个函数具有几个模板形参,每个参数都需要很多个约束类型,最终 SFINAE 会往可读性的地狱发展。
concept 和 requires
在 c++20 及其以后,我们可以不用再写 SFINAE 的代码形式了,只需要考虑 concept 和 requires 。
我们使用这两个来对刚刚的丑陋东西重写一遍。
template <typename T>
requires requires(const T val) { std::cout << val; }
auto WriteLine(const T &val) -> void {
std::cout << val << '\n';
}
至少可读性好多了,上面的 std::enable_if 很难看出与 std::cout << val 的关系,可是 concept 呢?
template <typename T>
concept OutputAble = requires(const T val) {
std::cout << val;
};
template <typename T>
requires OutputAble<T>
auto WriteLine(const T &val) -> void {
std::cout << val << '\n';
}
然而实际上,这样还是多余了点,我们可以更简单点。
template <OutputAble T>
auto WriteLine(const T &val) -> void {
std::cout << val << '\n';
}
这样是不是直接看懂了模板需要的类型满足是什么了,对吧?但是这还能再简单。
auto WriteLine(const OutputAble auto &val) -> void {
std::cout << val << '\n';
}
直接 auto ,那么现在就变得非常简单了,我们连类型都不关心了,只关心输入的这玩意是否满足可以被输出,这就是 concept 和 requires 带来的能力。
那么我们看看 concept 和 requires 是什么?
template <typename T>
concept OutputAble = requires(const T val) {
std::cout << val;
};
auto main() -> int {
std::cout << std::boolalpha << OutputAble<int> << '\n';
}
它输出了 true 。实际上,它有点像一个模板变量,当条件满足时为 true ,否则为 false ,那么条件在哪?
requires(const T val) { std::cout << val; }
这就是它所满足的条件,它需要一个 const T val 的变量,表示检查的操作一定不会修改到 val ,然后在 {} 中,它检查了 std::cout << val 是否是合法的,这显然比 std::enable_if 自然多了。const T val 只在编译期中存在,因此完全不用担心会影响到性能,只需要等到
实际上,我们刚刚一步一步简化的,就是 concept 的一些用法,我们可以使用 concept 先抽象出一些 requires 语句,然后复用它,或者直接在模板中使用 requires 来约束需要满足的 concept ,并且这个过程是可以再组合的,不用再写晦涩难懂的 std::enable_if 了。

浙公网安备 33010602011771号