作为TotW#198最初发表于2021年8月12日
由Alex Konradi创作
假设我们有一个类Foo:
class Foo {
public:
explicit Foo(int x, int y);
Foo& operator=(const Foo&) = delete;
Foo(const Foo&) = delete;
};
Foo既不可移动也不可复制,但它是可构造的,例如Foo f(1, 2);。由于它有一个公共构造函数,我们可以很容易地创建一个包装在std::optional中的实例:
std::optional<Foo> maybe_foo;
maybe_foo.emplace(5, 10);
这很棒,但是如果std::optional被声明为const,那么我们无法调用emplace()怎么办? 幸运的是,std::optional有一个这样的构造函数:
// 传递 std::in_place作为第一个参数, 后面是构造函数的参数
const std::optional<Foo> maybe_foo(std::in_place, 5, 10);
等等,std::in_place是什么意思?如果我们查看std::optional的文档,可以看到其中一个重载函数接受std::in_place_t作为第一个参数,以及其他参数的列表。由于std::in_place是std::in_place_t的一个实例,编译器选择了emplace构造函数,它通过将其余的参数转发给Foo的构造函数,在std::optional中实例化了Foo。
std::in_place_t是一类被称为"标签类型"的松散类的成员。这些类的作用是通过给重载函数(通常是构造函数)提供一个适当的标签类的实例来向编译器传递信息,从而在重载集中"标记"特定的重载。通过将适当的标签类的实例提供给重载函数,我们可以使用编译器的普通解析规则来选择所需的重载。在我们的std::optional构造中,编译器看到第一个参数的类型是std::in_place_t,因此选择了匹配的emplace构造函数。
虽然std::in_place_t是在C++17中引入的,但在标准库中使用空类作为标记重载的方式自C++11中引入std::piecewise_construct_t以来一直很普遍,用于选择std::pair的emplace构造函数。C++17在标准库中显著扩展了标记类型的集合。
除了消除重载的歧义之外,标记类型的另一个常见用途是将类型信息传递给模板化的构造函数。考虑以下两个结构体:
struct A { A(); /* internal members */ };
struct B { B(); /* internal members */ };
如果我们仔细查看std::in_place_t的文档,我们会发现它是一个空结构体。没有特殊的限定符或魔术声明。唯一稍微特殊的地方是标准库包含了一个命名实例std::in_place。
// 如果A和B是可复制或可移动构造的,则可以工作,但会产生额外的复制或移动构造的性能开销。
std::variant<A, B> with_a{A()};
std::variant<A, B> with_b{B()};
// 这些不是有效的C++代码;语言不支持显式提供构造函数模板参数。
std::variant<A, B> try_templating_a<A>{};
std::variant<A, B><B> try_templating_b{};
std::in_place_type是类模板std::in_place_type_t的一个实例,而std::in_place_type_t
标签类型在与通用类模板交互时偶尔会出现,尤其是标准库中的类模板。标签类型的一个缺点是,其他技术(如工厂函数)通常会导致更易读的代码。看下面的例子:
// 这种标签写法要求读者了解std::optional与std::in_place的交互方式。
std::optional<Foo> with_tag(std::in_place, 5, 10);
// 这种写法更清晰:通过提供这些参数来创建一个可选的Foo。
std::optional<Foo> with_factory = std::make_optional<Foo>(5, 10); 这两个std::optional<Foo>对象由于C++17的强制复制省略而保证构造相同。
那么为什么要使用标签呢?因为工厂函数并不总是适用:
// 这段代码无法工作,因为Foo没有可移动构造函数。
std::optional<std::optional<Foo>> foo(std::make_optional<Foo>(5, 10));
上述示例无法编译,因为Foo的移动构造函数被删除了。为了使其工作,我们使用std::in_place来选择std::optional构造函数,以转发剩余的参数。
// 这样就可以在原地构造所有对象,从而只调用一次Foo的构造函数。
std::optional<std::optional<Foo>> foo(std::in_place, std::in_place, 5, 10);
除了在工厂函数无法使用的地方可以使用标签类型外,标签类型还具有其他一些好处:
std::unordered_map<int, Foo> int_to_foo;
// std::piecewise_construct是std::pair构造函数的重载解析的标签。
// 在map中以100 -> Foo(5, 10)的形式进行就地构造。 int_to_foo.emplace(std::piecewise_construct, std::forward_as_tuple(100), std::forward_as_tuple(5, 10));
标签类型是向编译器提供附加信息的一种强大方式。虽然乍一看它们可能看起来很神奇,但它们使用的是与C++的其他部分相同的重载解析和模板类型推导规则。标准库使用类标签来消除构造函数调用的歧义,您也可以使用相同的机制为自己的需求定义标签。