The Curious Case of std::in_place [Outdated]
UPDATE: This post is no longer relevant to the C++ standard as of proposal paper
p0504r0,
which changed the definitions of the std::in_place
tag and its friends. This
post will remain in-tact for posterity, but be aware that it no longer
corresponds with the actual contents C++ standard library. Nevertheless, the
information contained herein may be useful to someone some day, and is a good
excersize in understanding some interesting metaprogramming techniques.
Preface
I was recently dabbling in implementing std::variant
and std::optional
, and
got caught up on the emplacement constructors
(Seen here and
here). I did not
have an implementation of std::in_place
to use, so I needed to build that as
well. I presumed it would be as simple as implementing std::nullopt
and
std::nullopt_t
, but it turned out to be quite a bit hairier than that.
What it actually required was some interesting interactions between alias templates, functions, function references, overload resolution, and template argument deduction. The resulting implementation techniques are very enlightening to anyone wishing to understand some of the more subtle - and powerful - details of the C++ language.
What is std::in_place
?
C++17 introduced - among other things - two incredibly useful new class
templates: std::variant
and std::optional
.
#include <variant>
#include <optional>
#include <string>
int main() {
using namespace std;
variant<int, string> v = 12; // Put an int in the variant
assert(holds_alternative<int>(v));
v = "Some string"; // Puts a std::string in the variant
assert(holds_alternative<string>(v));
optional<string> maybe_a_string;
assert(!maybe_a_string); // By default, there is no string in the optional
}
These two class templates feature several constructors that would do the
reasonable things that one would expect. When constructed from a T
, an
optional<T>
will implicitly become engaged and construct itself by copying
or moving the value from which it is constructed. When constructed from some
value U
, variant<T0, T1, ... Tn>
will construct itself to hold a value of
the type in Ts...
for which the best candidate conversion would take place
if calling a function overloaded with each type in Ts...
. In the example
above, we assign a char
array "Some string"
to the variant. Since the
variant’s alternative type std::string
is constructible from a char
array,
the variant selects std::string
as the alternative to store. Sounds good.
This begs the question: What if I want to construct the value in-place with no copy or move construction taking place? What if I want to construct it using zero or more than one argument?
The answer is std::in_place
, which is used to tell variant
or optional
to
construct a value in place, meaning it performs no copies or moves to get the
value in there.
For example, with optional
:
optional<string> make_from_chars(const char* ptr, size_t length) {
if (ptr) {
return { in_place, ptr, ptr + length };
} else {
return nullopt;
}
}
Or with variant
:
using int_or_string = variant<int, string>;
int_or_string get_int_or_string(bool get_int) {
if (get_int) {
return int_or_string{ in_place<int>, 42 };
} else {
return int_or_string{ in_place<string> }; // Constructs an empty string
}
}
Variant also has an index-based constructor, with allows constructing the Nth
type which is available in the variant
:
using int_or_string = variant<int, string>;
int_or_string get_int_or_string(bool get_int) {
if (get_int) {
return int_or_string{ in_place<0>, 42 };
} else {
return int_or_string{ in_place<1> }; // Constructs an empty string
}
}
std::in_place
is a construct used for tag dispatch, an extremely powerful
C++ idiom.
Tag Dispatch?
In the tag dispatch idiom, a function is overloaded on a tag type, which is
a (usually empty) class
/struct
or template thereof. Callers of the overloaded
function will pass in an instance of the tag type which will tell the compiler to
select a particular overload of the function. This is useful because it allows
generic code to call an overload set and pass in some unknown tag instance,
which dispatches to a particular overload without branching or needing to
instantiate (and verify the semantics of) overloads which are not of particular
interest.
One of the most useful cases of tag dispatch is to dispatch to differing
algorithm implementations based on the
iterator_category
of an iterator
given to the algorithm as a parameter.
For example, suppose we are implementing an algorithm frombulate
which has a
linear complexity for ForwardIterators
(such as in a linked-list) and
logarithmic complexity for RandomAccessIterators
. Using tag-dispatch, our code
would resemble something like this:
namespace impl {
template <typename Iter>
Iter do_frombulate(forward_iterator_tag, Iter first, Iter last) {
// Do slow implementation
}
template <typename Iter>
Iter do_frombulate(random_access_iterator_tag, Iter first, Iter last) {
// Do super fast implementation
}
}
template <typename Iterator>
Iterator frombulate(Iterator first, Iterator last) {
// Get the category type
using category_tag = typename iterator_traits<Iterator>::iterator_category;
// Dispatch on that tag
return impl::do_frombulate(category_tag{}, first, last);
}
Above, we have two overloads for impl::do_frombulate
. The only difference in
their signatures is the first parameter, which is our tag type. We don’t give
the tag parameter a name because we don’t really need to do anything with the
object itself. It is solely there to help the compiler select the proper
overload for frombulate
.
In frombulate
, we get the iterator category tag type using
iterator_traits
,
then call do_frombulate
, passing in an instance of the iterator category tag
as the first parameter. The empty braces {}
call the default constructor of
the tag type. It is idiomatic to construct tag types using the empty braces as
a way to differentiate them from a function call using empty parenthesis ()
.
Could we have used a regular if
branch to choose between our two
implementations? The answer is probably not. For example:
// NOTE: BAD:
template <typename Iterator>
Iterator frombulate(Iterator first, Iterator last) {
// Get the category type
using category_tag = typename iterator_traits<Iterator>::iterator_category;
if (is_convertible<category_tag, random_access_iterator_tag>()) {
// Do fast implementation
} else if (is_convertible<category_tag, forward_iterator_tag>()) {
// Do slow implementation
} else {
// ??? (See below)
}
}
A regular if
branch does not work for two primary reasons:
- All branches must compile properly for any passed in
Iterator
type. This is problematic because not all operations ofRandomAccessIterators
are available toForwardIterators
. Thus, if we pass in aForwardIterator
to this branching implementation, we will get a compile error because the unused firstif
block is ill-formed, even if that branch can be statically proven to never be executed. - What do we put in the final
else
branch? If the user passes in anInputIterator
, which is neither aRandomAccessIterator
nor aForwardIterator
, what do we do? We could use astatic_assert
, but a naive use ofstatic_assert(false)
will simply cause the code to unconditionally fail even when passed in proper iterator types. We could callterminate
, but that’s annoying to callers, since we can statically determine when such code is invalid. The only thing to do would be to be redundant and check thatis_convertible<Iterator, ForwardIterator>()
, istrue
.
All around, this won’t work
Note that as-of C++17, we could actually do something like the code above, using
if constexpr
to branch at compile time:
// NOTE: C++17, GOOD:
template <typename Iterator>
Iterator frombulate(Iterator first, Iterator last) {
// Get the category type
using category_tag = typename iterator_traits<Iterator>::iterator_category;
static_assert(is_convertible<category_tag, forward_iterator_tag>());
if constexpr (is_convertible<category_tag, random_access_iterator_tag>()) {
// Do fast implementation
} else {
// Do slow implementation
}
}
It may be that the code above becomes more idiomatic as compilers start to roll out support for C++17. It feels a little more verbose that the tag-dispatching equivalent, but is definitely easier to reason about for someone unfamiliar with tag dispatch. Time will tell.
Either way, tag dispatch still has its place in C++17, as can be seen by the
addition of std::in_place
.
Something’s… Different?
std::in_place
is different from other tag dispatch code that has been common
in the past. For one we can see that std::in_place_tag
exists in the standard library,
but it seems to be inconstructible (having only a delete
‘d constructor).
Furthermore, we can see that no constructors for optional
nor variant
actually mention either std::in_place
or std::in_place_tag
. Instead, they
reference some other constructs: std::in_place_t
, std::in_place_type_t<T>
,
and std::in_place_index_t<I>
. How do these relate to std::in_place
?
One may initially think that std::in_place
may be a global tag instance of some
type in the same way that nullopt
is a global instance of the nullopt_t
tag type. The nullopt_t
constructors for optional
are also tag-dispatch
based constructors, and nullopt
is the singleton instance of nullopt_t
that
is used to initialize an empty optional
, but this is not what std::in_place
is.
The primary reason std::in_place
is not a tag instance is that we wish to be
able to give it template arguments, such as when we use std::in_place<T>
to
construct a variant holding an instance of T
. Such syntax is not supported on
variables. Another good guess is that std::in_place
may be a
variable template
which, when instantiated, becomes is an instance of some tag type.
Close again, but this presents two problems:
-
We wish to be able to pass either a type or an index in as a template parameter to
std::in_place
for constructingvariant
s. A variable which is templated on both a type and a non-type is not legal!We can specialize variable templates, but not in this manner:
template <typename T> int foo = sizeof(T); template <int I> int foo = I; // error: redeclaration of 'template<int I> int foo'
-
We wish to be able to pass
std::in_place
with no template arguments, such as withoptional
’s emplacing constructor, but it is not legal to reference a variable template without providing template arguments. We could provide default template arguments to our variable template, but that would still require empty angle brackets to instantiate such a construct. There is also the problem of what would be a good default argument?void
?monostate
?42
?
There’s another oddity about std::in_place
that you may have noticed: We never
call a constructor on the thing. With the iterator_category
example above, we
need to construct an instance of the tag type when we pass it to a function,
which I do using the empty braces {}
to call the default constructor. You will
see that with std::in_place
, we do not call it or construct it, we simply
write it with or without some template arguments.
So, we need some construct which can be used as a run-time value like a tag instance, but can also be given template arguments which are either types or non-types.
Does such a construct exist?
Function Pointers and References
An unlikely language feature comes to the rescue. Indeed, std::in_place
is
not a type, not a variable, but a function. In fact, std::in_place
is three
different things: A function, a function template parameterized by a single type,
and a function template parameterized by a std::size_t
. We can see the signatures
of these functions here.
It is not immediately obvious: Why is it a function? What arguments can I pass
to it? What is the return value? What does this have to do with std::in_place_tag
?
How does this work with tag dispatch?
A few can be answered right off the bat:
- No, you cannot call
std::in_place
. It is explicitly undefined behavior to call these functions. - Calling the functions may prove difficult, as the actual parameter types are
completely unspecified. Obtaining an instance of the unspecified type is not
portable, and may not even be possible depending on the implementation. The
parameters are only there to assist the compiler later when it performs
template argument deduction for
in_place_type_t
andin_place_index_t
. We’ll get to that shortly. - The return value of the functions is
std::in_place_tag
. Of course, we cannot call the function, and this tag type is completely inconstructible anyway.
The remaining questions are why and how.
Why Use a Function?
The why is actually pretty simple. The language allows a non-template function to be overloaded by templates with disparate template parameters. Like this:
// Non-template function
void foo() {}
// Template parameterized on a type
template <typename T>
void foo() {}
// Template parameterized on an int
template <int I>
void foo() {}
When we reference the function foo
, we may provide or omit template parameters
to change which overload is selected by the compiler.
void bar() {
foo(); // Calls non-template foo
foo<int>(); // Calls foo with type template parameter
foo<42>(); // Calls foo with int template parameter
}
We can also store the function in a function pointer or function reference like so:
void bar() {
using void_fn_ptr = void(*)();
void_fn_ptr fn = foo;
fn(); // Calls non-template foo
fn = foo<int>;
fn(); // Calls foo<int>
fn = foo<42>;
fn(); // Calls foo<42>
}
Even though the different foo
overloads look completely different, once we
provide the template parameters, they all collapse to the same function type:
void(&)()
. (Note that a function reference with the ampersand &
will
implicitly convert to a function pointer with the asterisk *
.)
The ability to capture these overloads as a parameter is important in how this works.
How Does This Work?
As I show above, a function template, once specialized, becomes a regular
function, and can be stored and passed via function pointers and references. This
is the basis of std::in_place
. The difference between std::in_place
and the
example I wrote above is that std::in_place
has a parameter.
The type of the parameter is unspecified, but is still relevant when forming the type of pointers or references to the function itself. For exposition, I will define some imaginary tag types which are not constructible:
namespace magic {
struct empty_tag { empty_tag() = delete; };
template <typename T>
struct type_tag{ type_tag() = delete; };
template <std::size_t I>
struct index_tag { index_tag() = delete; };
}
The default constructors are deleted, and the types themselves cannot be constructed, but they can still be referenced:
// We can declare a function which takes our magic type
void foo(magic::type_tag<int>) {}
void bar() {
// We can even get a pointer to this function!
using fn_ptr = void(*)(magic::type_tag<int>);
fn_ptr foo_ptr = foo;
// But calling is still not possible!
// foo_ptr(???) // Where do we get an instance of magic::type_tag<int> ?
}
We could also declare a function which takes a reference to a function that takes an instance our magic tag type:
// We use a function reference to prevent users from passing in nullptr
using magic_int_fn = void(&)(magic::type_tag<int>);
void do_thing(magic_int_fn) {}
Note that the magic_int_fn
is constructible, even if not callable. This is
important!
But what if we don’t want type_tag<int>
, but any type_tag<T>
for any given
T
? We can do that as well, using type deduction and by making do_thing
a
function template:
template <typename T>
void do_thing(void(&)(magic::type_tag<T>)) {}
Now we can pass in any type tag. How would we create these function types? Well, we aren’t actually going to call it, so it doesn’t matter what the function itself does. So we can write a dummy function with the signature that we want:
template <typename T>
void dummy_fn(magic::type_tag<T>) {}
And now we can just specialize that function to get a reference to it, and pass
that in to do_thing
:
void bar() {
do_thing(dummy_fn<int>); // Compiler deduces "int", calls do_thing<int>
do_thing(dummy_fn<void>); // Compiler deduces "void", calls do_thing<void>
}
We could also write a do_thing
which takes a magic::index_tag<I>
for any I
:
template <std::size_t I>
void do_thing(void(&)(magic::index_tag<I>)){}
And we can make a dummy_fn
function template that is parameterized on a
std::size_t
:
template <std::size_t I>
void dummy_fn(magic::index_tag<I>) {}
Usage of this is similar, and doesn’t collide with our dummy_fn
nor do_thing
that are parameterized on a type:
void bar() {
do_thing(dummy_fn<int>);
do_thing(dummy_fn<void>);
// Now pass in integers!
do_thing(dummy_fn<42>); // Compiler deduces "42", calls do_thing<42>
}
We can also define a non-template dummy_fn
that takes our empty_tag
:
void dummy_fn(magic::empty_tag) {}
Now define a non-template do_thing
that takes uses this empty_tag
:
void do_thing(void(&)(magic::empty_tag)) {}
Again, this plays nicely with all our other overloads:
void bar() {
do_thing(dummy_fn<int>);
do_thing(dummy_fn<void>);
do_thing(dummy_fn<42>);
// No template parameters at all!
do_thing(dummy_fn); // Calls non-template dummy_fn
}
We’ve got a very flexible way to do tag dispatch based on types, integers, or the lack of any parameter at all, and the usage doesn’t look too bad either. There’s still one problem: Library developers who wish to use our fancy tag dispatch need to parameterize their functions on a function reference using a “magic” type that they should not actually need to care about. This is where alias templates can help us.
We can define an alias template for the three function tag types:
template <typename T>
using type_tag_t = void(&)(magic::type_tag<T>);
template <std::size_t I>
using index_tag_t = void(&)(magic::index_tag<I>);
using empty_tag_t = void(&)(magic::empty_tag);
Now, library developers can use these tag types which don’t live in a magic
namespace, and they don’t even need to worry about why these things are function
pointers. See our new declarations of do_thing
:
// Equivalent to
// template <typename T>
// void do_thing(void(&)(magic::type_tag<T>)) {}
template <typename T>
void do_thing(type_tag_t<T>) {}
// Equivalent to
// template <std::size_t I>
// void do_thing(void(&)(magic::index_tag<I>)) {}
template <std::size_t I>
void do_thing(index_tag_t<I>) {}
// Equivalent to
// void do_thing(void(&)(magic::empty_tag)) {}
void do_thing(empty_tag_t) {}
And we’re done! This is how std::in_place
works and how it might be
implemented.
Finishing Up
The only thing left is to name things correctly. Our dummy_fn
? Well, that’s
just std::in_place
. Our alias templates? Those correspond to
std::in_place_t
, std::in_place_type_t
, and std::in_place_index_t
.
Here’s a complete implementation of std::in_place
and it’s related aliases:
namespace std {
namespace detail {
// Was: magic::empty_tag
struct in_place_empty_arg { in_place_empty_arg() = delete; };
// Was: magic::type_tag
template <typename T>
struct in_place_type_arg { in_place_type_arg() = delete; };
// Was: magic::index_tag
template <size_t I>
struct in_place_index_arg { in_place_index_arg() = delete; };
}
struct in_place_tag { in_place_tag() = delete; };
// Was: void dummy_fn(magic::empty_tag)
inline in_place_tag in_place(detail::in_place_empty_arg) { terminate(); }
// Was: template <typename T> void dummy_fn(magic::type_tag<T>)
template <typename T>
in_place_tag in_place(detail::in_place_type_arg<T>) { terminate(); }
// Was: template <size_t I> void dummy_fn(magic::index_tag<I>)
template <size_t I>
in_place_tag in_place(detail::in_place_index_arg<I>) { terminate(); }
// Was: empty_tag_t
using in_place_t = in_place_tag(&)(detail::in_place_empty_arg);
// Was: type_tag_t
template <typename T>
using in_place_type_t = in_place_tag(&)(detail::in_place_type_arg<T>);
// Was: index_tag_t
template <size_t I>
using in_place_index_t = in_place_tag(&)(detail::in_place_index_arg<I>);
}
Above, we can see that the /* Unspecified */
shown in the standard are
replaced with our *_arg
tags in the detail
namespace. The actual types here
are not specified in the API because users should use the alias templates
instead of declaring the function references themselves.
If we want to write our own function that works with std::in_place
, we use the
alias templates and template argument deduction when we declare our function:
template <typename T>
void discombobulate(std::in_place_type_t<T>) {
// ...
}
After alias expansion, our declaration becomes:
template <typename T>
void discombobulate(std::in_place_tag(&)(std::detail::in_place_type_arg<T>)) {
// ...
}
When we call discombobulate
like so:
discombobulate(std::in_place<int>);
The compiler sees us passing a reference to a function as the first argument,
where the reference type is written as
std::in_place_tag(&)(std::detail::in_place_type_arg<int>)
. The compiler will
deduce T
to be int
for discombobulate
, and we’re done!
Of course, we don’t have to worry about all this magic going on behind the
scenes, and we thus get a terse, flexible, and easy-to-use API for
std::in_place
.
Moving Forward
Using these seemingly unrelated rules of the language, we get std::in_place
,
which helps us do some pretty nice things and build some pretty nice APIs.
What else could we do with these language constructs? Anything similar to
std::in_place
?
Other than the alias templates, std::in_place
can be constructed using only
features available in C++03. With the addition of more language features like
constexpr
, lambdas, variable templates, variadic templates, fold expressions,
etc., it will be exciting to see what delicious new metaprogramming techniques
are discovered in the years to come.
std::variant
’s converting constructor also requires some very interesting
trickery to make work, and may be the subject of a future post.
Stay tuned, and remember: 'class std::vector<bool>' has no member named 'data'
.