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
2
using namespace std::string_literals;
std::pair myPair(42, "hello world"s); // 自动推断!

这可以大大地削减下边这样的复杂的构造过程:

1
2
3
4
5
6
// lock guard:
std::shared_timed_mutex mut;
std::lock_guard<std::shared_timed_mutex> lck(mut);

// array:
std::array<int, 3> arr {1, 2, 3};

现在可以变成这样:

1
2
3
4
std::shared_timed_mutex mut;
std::lock_guard lck(mut);

std::array arr { 1, 2, 3 };

注意,部分推断不会发生,你必须全部写明所有的模版参数或全部不写:

1
2
3
std::tuple t(1, 2, 3); // OK: 推断
std::tuple<int,int,int> t(1, 2, 3); // OK: 所有参数类型都提供了
std::tuple<int> t(1, 2, 3); // Error: 部分推断

有了这个特性,许多 make_Type 函数可能都不需要了——尤其那些“模拟”类的模版推断的函数。

但是仍然有一些工厂函数做了额外的工作。比如 std::make_shared ——它不只创建了 shared_ptr,同时确保了控制块(译注:指引用计数的控制块)和所指向对象在一个内存区域内被分配:

1
2
3
4
5
// 控制块和 int 对象可能在内存的不同地方
std::shared_ptr<int> p(new int{10});

// 控制块和 int 对象在连续内存区域
auto p2 = std::make_shared<int>(10);

译注:std::shared_ptrstd::make_shared 的实现细节是我特别喜欢面试的一个知识点。

类的模版参数推断是怎么工作的呢?

让我们进入“推断指引”部分。

推断指引

编译器使用名叫“推断指引”(deduction guides)的特殊规则计算模版类的类型。

这些规则又分两类:编译器生成的(隐式生成)和用户自定义的。

为了理解编译器如何使用这些指引,让我们看一个例子。

这里是一个对 std::array 的自定义推断指引:

1
2
template <class T, class... U>
array(T, U...) -> array<T, 1 + sizeof...(U)>;

语法看起来像一个带尾随返回类型的模版函数。编译器视这种“虚拟的”函数为参数推断的一个候补。如果样式匹配,本次推断就返回恰当的类型。

在我们的案例里当你写下:

1
std::array arr {1, 2, 3, 4};

时,即假设 TU... 都是同一类型,我们可以构建一个 std::array<int, 4> 类型的数组对象。

大多数情况下,你可以依赖编译器自动生成(译注:隐式的)推断指引。它们会对基本的类模版的每一个构造函数(包括拷贝/移动构造)创建。请注意,对特化或偏特化类不生效。

刚才说过,你也可以定义自己的推断指引:

需要添加自定义推断指引的一个经典例子是 std::string 取代 const char * 的推断:

1
2
3
4
5
6
7
8
9
10
template<typename T>
struct MyType
{
T str;
};

// 自定义推断指引
MyType(const char *) -> MyType<std::string>;

MyType t{"Hello World"};

没有自定义推断指引的的话 T 会被推断为 const char *

另一个自定义推断指引的例子是 overload:

1
2
3
4
5
template<class... Ts>
struct overload : Ts... { using Ts::operator()...; };

template<class... Ts>
overload(Ts...) -> overload<Ts...>; // 推断指引

overload 类继承自 Ts... 几个类,然后把它们的 operator() 暴露出去。在这里自定义推断指引被用来把一组 lambda “转换”成一组可以继承的类。

译注:lambda 本质上是一个匿名函数对象,即重写了 operator() 的类,所以 using Ts::operator()... 才可能成立。

译注:这里的 overload,不是我们认知的面向对象领域的“重载”,是一种泛化了的用到了重载的惯用手法。其表现形式就是上边代码中展示的样子,通常的应用场景是配合 std::visit 实现对 std::variant 中所有类型的访问。可以参考这里。本书后边也有类似的应用案例,并配有比较详细的解释。

扩展:本修改提案:P0091R3P0433——标准库中的推断指引

请注意:尽管编译器可能声明了已经完全支持类模板的模板参数推断,其对应的 STL 实现可能还是缺少对某些 STL 类型的自定义推断指引。参考本章最后的编译器支持章节。(译注:编译器支持这东西时效性太短,我不会翻译原文,也不会带一份本文译写时最新的支持列表,请自行查阅。)

评论