每周技巧 #198:标签类型
本节阅读量:本文翻译自 Abseil 官网的 Tip of the Week #198: Tag Types。
原文最初作为 TotW #198 发布于 2021 年 8 月 12 日。
作者:Alex Konradi
更新于 2022 年 1 月 24 日。
快捷链接:abseil.io/tips/198
假设我们有一个类 Foo:
|
|
Foo 既不可移动也不可拷贝,但它可以构造,例如 Foo f(1, 2);。由于它有公共构造函数,我们可以很容易地创建一个包在 std::optional 中的实例:
|
|
这很好,但如果 std::optional 被声明为 const,因此不能调用 emplace() 呢?幸运的是,std::optional 为此提供了一个构造函数:
|
|
等等,std::in_place 是怎么回事?如果查看 std::optional 构造的文档,可以看到其中一个重载把 std::in_place_t 作为第一个实参,并接受一串额外实参。由于 std::in_place 是 std::in_place_t 的一个实例,编译器会选择原位构造构造函数,它通过把剩余实参转发给 Foo 的构造函数,在 std::optional 内部实例化 Foo。
如果更仔细地看 std::in_place_t 的文档,会发现它……是一个空结构体。没有特殊限定符,也没有魔法声明。唯一稍微特殊的是,标准库包含了它的一个具名实例 std::in_place。
通过标签类型进行重载解析
std::in_place_t 属于一类松散的类,通常称为“标签类型”。这些类的作用是通过“标记”重载集中的特定重载,向编译器传递信息。向重载函数(通常是构造函数)提供合适标签类的实例后,我们就可以利用编译器普通的解析规则,让它选择期望的重载。在我们的 std::optional 构造例子中,编译器看到第一个实参类型是 std::in_place_t,于是选择匹配的原位构造构造函数。
虽然 std::in_place_t 在 C++17 中引入,但标准库中用空类标记重载的做法早在 C++11 引入 std::piecewise_construct_t 时就已经很常见了,它用于为 std::pair 选择原位构造构造函数。C++17 大幅扩展了标准库中的标签类型集合。
当然,还有模板
除了消除重载歧义,标签类型的另一个常见用途是向模板化构造函数传递类型信息。考虑这两个结构体:
|
|
让我们尝试用它们分别构造 std::variant<A, B> 的实例:
|
|
我们需要一种方式告诉 std::variant 构造函数:想要实例化 A 或 B,但不真的提供二者中任何一个实例。为此,可以使用 std::in_place_type!
|
|
std::in_place_type<T> 是类模板 std::in_place_type_t<T> 的一个实例,而后者(到这里你大概不会惊讶)是空的。通过把类型为 std::in_place_type_t<A> 的值传给 std::variant 构造函数,编译器可以推导出构造函数模板参数就是我们的类 A。
使用
与泛型类模板交互时,标签类型偶尔会出现,尤其是标准库中的模板。标签类型的一个缺点是,工厂函数等其他技术通常会产生更可读的代码。看下面这个例子:
|
|
得益于 C++17 的强制拷贝消除,这两个 std::optional<Foo> 对象保证以相同方式构造。
那为什么还要使用标签?因为工厂函数不总是可行:
|
|
上面的例子无法编译,因为 Foo 的移动构造函数已删除。为了让它工作,我们使用 std::in_place 选择会转发剩余实参的 std::optional 构造函数。
|
|
除了能在工厂函数不可用的地方工作,标签类型还有一些不错的性质:
- 它们是字面类型,这意味着我们可以声明
constexpr实例,甚至可以在头文件中声明,例如std::in_place。 - 因为空类可以被编译器优化掉,所以它们没有运行时开销。
虽然标准库使用了标签类型,但在野外遇到空标签类型相对少见。如果你发现自己在使用它,考虑添加注释帮助读者:
|
|
结论
标签类型是一种向编译器提供额外信息的强大方式。乍看之下它们可能像魔法,但它们使用的仍是 C++ 其他部分相同的重载解析和模板类型推导规则。标准库用类标签消除构造函数调用歧义,你也可以使用同样机制按自己的需要定义标签。
相关阅读
std::integer_sequence,用于元组的编译期索引- 用于
std::make_shared的 passkey 使用标签控制访问