C++17 详解 11
本文为 《C++17 in detail》 一书的中文渣中渣译文,不足之处还望指正。
4. 模版
你是否用过模版或元编程?
如果你的回答是“是”,那你可能会对本次 C++17 的更新感到非常开心。
新标准引入了许多使模版编程更简单更富有表现力的加强措施。
本章你将学到:
- 类模板的模版参数推断
template<auto>
- 折叠表达式
if constexpr
——编译时if
!- 其它一些小的、细致的改进和修复
4.1 类模板的模版参数推断
你经常用 make_
Type 函数构建模版对象(比如 std::make_pair
)吗?
通过 C++17 你可以把它们忘掉然后只使用一个普通形式的构造函数了。C++17 填补了一个模版推断规则的缺口。现在模版推断对类模版也会触发,而不只是对函数。这也意味着你的许多代码——那些 make
_Type 函数现在可以被删掉了。
举例来说,为了创建一个 pair
,以前这么写会更方便:
1 | auto myPair = std::make_pair(42, "hello world"); |
而不是:
1 | std::pair<int, std::string> myPair(42, "hello world"); |
因为 std::make_pair
是一个模版函数,编译器可以对函数的模版参数执行自动推断,所以没必要这么写:
1 | auto myPair = std::make_pair<int, std::string>(42, "hello world"); |
现在,C++17 起,合规的编译器可以很好地推断类模版的模版参数类型了。
在我们的例子里,现在你可以这么写:
1 | using namespace std::string_literals; |
这可以大大地削减下边这样的复杂的构造过程:
1 | // lock guard: |
现在可以变成这样:
1 | std::shared_timed_mutex mut; |
注意,部分推断不会发生,你必须全部写明所有的模版参数或全部不写:
1 | std::tuple t(1, 2, 3); // OK: 推断 |
有了这个特性,许多 make_
Type 函数可能都不需要了——尤其那些“模拟”类的模版推断的函数。
但是仍然有一些工厂函数做了额外的工作。比如 std::make_shared
——它不只创建了 shared_ptr
,同时确保了控制块(译注:指引用计数的控制块)和所指向对象在一个内存区域内被分配:
1 | // 控制块和 int 对象可能在内存的不同地方 |
译注:
std::shared_ptr
和std::make_shared
的实现细节是我特别喜欢面试的一个知识点。
类的模版参数推断是怎么工作的呢?
让我们进入“推断指引”部分。
推断指引
编译器使用名叫“推断指引”(deduction guides)的特殊规则计算模版类的类型。
这些规则又分两类:编译器生成的(隐式生成)和用户自定义的。
为了理解编译器如何使用这些指引,让我们看一个例子。
这里是一个对 std::array
的自定义推断指引:
1 | template <class T, class... U> |
语法看起来像一个带尾随返回类型的模版函数。编译器视这种“虚拟的”函数为参数推断的一个候补。如果样式匹配,本次推断就返回恰当的类型。
在我们的案例里当你写下:
1 | std::array arr {1, 2, 3, 4}; |
时,即假设 T
和 U...
都是同一类型,我们可以构建一个 std::array<int, 4>
类型的数组对象。
大多数情况下,你可以依赖编译器自动生成(译注:隐式的)推断指引。它们会对基本的类模版的每一个构造函数(包括拷贝/移动构造)创建。请注意,对特化或偏特化类不生效。
刚才说过,你也可以定义自己的推断指引:
需要添加自定义推断指引的一个经典例子是 std::string
取代 const char *
的推断:
1 | template<typename T> |
没有自定义推断指引的的话 T
会被推断为 const char *
。
另一个自定义推断指引的例子是 overload:
1 | template<class... Ts> |
overload
类继承自 Ts...
几个类,然后把它们的 operator()
暴露出去。在这里自定义推断指引被用来把一组 lambda “转换”成一组可以继承的类。
译注:lambda 本质上是一个匿名函数对象,即重写了
operator()
的类,所以using Ts::operator()...
才可能成立。
译注:这里的 overload,不是我们认知的面向对象领域的“重载”,是一种泛化了的用到了重载的惯用手法。其表现形式就是上边代码中展示的样子,通常的应用场景是配合
std::visit
实现对std::variant
中所有类型的访问。可以参考这里。本书后边也有类似的应用案例,并配有比较详细的解释。
扩展:本修改提案:P0091R3 和 P0433——标准库中的推断指引。
请注意:尽管编译器可能声明了已经完全支持类模板的模板参数推断,其对应的 STL 实现可能还是缺少对某些 STL 类型的自定义推断指引。参考本章最后的编译器支持章节。(译注:编译器支持这东西时效性太短,我不会翻译原文,也不会带一份本文译写时最新的支持列表,请自行查阅。)