string_view
operator+
vs. StrCat()
absl::Status
std::bind
absl::optional
and std::unique_ptr
absl::StrFormat()
make_unique
and private
Constructors.bool
explicit
= delete
)switch
Statements Responsibly= delete
AbslHashValue
and Youcontains()
std::optional
parametersif
and switch
statements with initializersinline
Variablesstd::unique_ptr
Must Be MovedAbslStringify()
vector.at()
auto
for Variable DeclarationsOriginally posted as TotW #198 on August 12, 2021
By Alex Konradi
Updated 2022-01-24
Quicklink: abseil.io/tips/198
Suppose we have a class Foo
:
class Foo { public: explicit Foo(int x, int y); Foo& operator=(const Foo&) = delete; Foo(const Foo&) = delete; };
Foo
is neither movable nor copyable, but it is constructible, e.g. Foo f(1,
2);
. Since it has a public constructor, we can easily make an instance wrapped
in a std::optional
:
std::optional<Foo> maybe_foo; maybe_foo.emplace(5, 10);
That’s great, but what if the std::optional
is declared const
, so we can’t
call emplace()
? Luckily for us, std::optional
has a constructor for this:
// Pass std::in_place as the first argument, followed by the arguments for Foo's // constructor. const std::optional<Foo> maybe_foo(std::in_place, 5, 10);
Wait, what’s going on with std::in_place
? If we look at
the documentation
for std::optional
construction, we can see that one of the overloads takes a
std::in_place_t
as the first argument, along with a list of additional
arguments. Since
std::in_place
is an
instance of std::in_place_t
, the compiler chooses the emplacing constructor,
which instantiates Foo
within the std::optional
by forwarding the rest of
the arguments to Foo
’s constructor.
If we look a little closer at the documentation for std::in_place_t
, we see
that it’s… an empty struct. No special qualifiers or magic declarations. The
only thing even mildly special about it is that the standard library includes a
named instance of it, std::in_place
.
std::in_place_t
is a member of a loose category of classes sometimes referred
to as “tag types”. The function of these classes is to pass information to the
compiler by “tagging” specific overloads in an overload set. By providing an
instance of an appropriate tag class to an overloaded function (often a
constructor), we can use the compiler’s ordinary resolution rules to make it
select the desired overload. In the case of our std::optional
construction,
the compiler sees that the first argument is of type std::in_place_t
and so
chooses the matching emplacing constructor.
Though std::in_place_t
was introduced in C++17, the use of empty classes for
tagging overloads in the standard library has been prevalent since
std::piecewise_construct_t
,
was introduced in C++11 to select the emplacing constructor for std::pair
.
C++17 significantly expanded the set of tag types in the standard library.
Besides disambiguating overloads, another common use case for tag types is to pass type information to templated constructors. Consider these two structs:
struct A { A(); /* internal members */ }; struct B { B(); /* internal members */ };
Let’s try to construct an instance of
std::variant<A, B>
with
each:
// These work if A and B are copy- or move-constructible, but incur the // performance cost of an extra copy or move construction. std::variant<A, B> with_a{A()}; std::variant<A, B> with_b{B()}; // These aren't valid C++; the language doesn't support providing constructor // template parameters explicitly. std::variant<A, B> try_templating_a<A>{}; std::variant<A, B><B> try_templating_b{};
What we need is a way to tell the std::variant
constructor that we want to
instantiate A
or B
without actually providing an instance of either. For
that, we can use
std::in_place_type
!
std::variant<A, B> with_a{std::in_place_type<A>}; std::variant<A, B> with_b{std::in_place_type<B>};
std::in_place_type<T>
is an instance of the class template
std::in_place_type_t<T>
, which is (unsurprisingly, at this point) empty. By
passing a value of type std::in_place_type_t<A>
to std::variant
’s
constructor, the compiler can deduce that the constructor template parameter is
our class A
.
Tag types show up occasionally when interacting with generic class templates, especially ones from the standard library. One of the shortcomings of tag types is that other techniques, such as factory functions, often result in more readable code. Take the following example:
// This tag spelling requires the reader to know how std::optional interacts // with std::in_place. std::optional<Foo> with_tag(std::in_place, 5, 10); // Here the intent is clearer: make an optional Foo by providing these argments. std::optional<Foo> with_factory = std::make_optional<Foo>(5, 10);
These two std::optional<Foo>
objects are guaranteed to be constructed
identically thanks to C++17’s mandatory copy elision.
So why use tags? Because factory functions don’t always work:
// This doesn't work because Foo isn't move-constructible. std::optional<std::optional<Foo>> foo(std::make_optional<Foo>(5, 10));
The above example doesn’t compile because Foo
’s move constructor is deleted.
To make this work, we use std::in_place
to select the std::optional
constructor that forwards the remaining arguments.
// This constructs everything in place, resulting in a single call to Foo's // constructor. std::optional<std::optional<Foo>> foo(std::in_place, std::in_place, 5, 10);
In addition to working in places where factory functions don’t, tag types have some other nice properties:
constexpr
instances, even in header files, like
std::in_place
;Though they’re used in the standard library, encountering empty tag types in the wild is relatively uncommon. If you find yourself using one, consider adding a comment to help your readers:
std::unordered_map<int, Foo> int_to_foo; // std::piecewise_construct is a tag for overload resolution of std::pair's // constructor. Emplaces 100 -> Foo(5, 10) in the map. int_to_foo.emplace(std::piecewise_construct, std::forward_as_tuple(100), std::forward_as_tuple(5, 10));
Tag types are a powerful way to give additional information to the compiler. While at first glance they might seem magical, they use the same overload resolution and template type deduction rules as the rest of C++. The standard library uses class tags for disambiguating constructor calls, and you can use the same mechanisms to define tags for your own needs.