Last Update:
In-Place Construction for std::any, std::variant and std::optional
Table of Contents
When you read articles or reference pages for std::any
, std::optional
or std::variant
you might notice a few helper types called in_place_*
available in constructors.
Why do we need such syntax? Is this more efficient than “standard” construction?
Intro
Chinese version here:
We have the following in_place
helper types:
std::in_place_t
type and a global valuestd::in_place
- used forstd::optional
std::in_place_type_t
type and a global valuestd::in_place_type
- used forstd::variant
andstd::any
std::in_place_index_t
type and a global valuestd::in_place_index
- used forstd::variant
The helpers are used to efficiently initialise objects “in-place” - without additional temporary copy or move operations.
Let’s see how those helpers are used.
The Series
This article is part of my series about C++17 Library Utilities. Here’s the list of the other topics that I’ll cover:
- Refactoring with
std::optional
- Using
std::optional
- Error handling and
std::optional
- About
std::variant
- Using
std::any
- In place construction for
std::optional
,std::variant
andstd::any
(this post) - Using
std::string_view
- C++17 string searchers & conversion utilities
- Working with
std::filesystem
- Extras:
Resources about C++17 STL:
- C++17 In Detail by Bartek!
- C++17 - The Complete Guide by Nicolai Josuttis
- C++ Fundamentals Including C++17 by Kate Gregory
- Practical C++14 and C++17 Features - by Giovanni Dicanio
- C++17 STL Cookbook by Jacek Galowicz
In std::optional
For a start let’s have a look at std::optional
. It’s a wrapper type, so you should be able to create optional objects almost in the same way as the wrapped object. And in most cases you can:
std::optional<std::string> ostr{"Hello World"};
std::optional<int> oi{10};
You can write the above code without stating the constructor like:
std::optional<std::string> ostr{std::string{"Hello World"}};
std::optional<int> oi{int{10}};
Because std::optional
has a constructor that takes U&&
(r-value reference to a type that converts to the type stored in the optional). In our case it’s recognised as const char*
and strings can be initialised from this.
So what’s the advantage of using std::in_place_t
in std::optional
?
We have at least two points:
- Default constructor
- Efficient construction for constructors with many arguments
Default Construction
If you have a class with a default constructor, like:
class UserName {
public:
UserName() : mName("Default") {
}
// ...
private:
std::string mName;
};
How would you create an std::optional
object that contains UserName{}
?
You can write:
std::optional<UserName> u0; // empty optional
std::optional<UserName> u1{}; // also empty
// optional with default constructed object:
std::optional<UserName> u2{UserName()};
That works but it creates additional temporary object. Here’s the output if you run the above code (augumented with some logging):
UserName::UserName('Default')
UserName::UserName(move 'Default') // move temp object
UserName::~UserName('') // delete the temp object
UserName::~UserName('Default')
The code creates a temporary object and then moves it into the object stored in std::optional
.
Here we can use more efficient constructor - by leveraging std::in_place_t
:
std::optional<UserName> opt{std::in_place};
Produces the output:
UserName::UserName('Default')
UserName::~UserName('Default')
The object stored in the optional is created in place, in the same way as you’d call UserName{}
. No additional copy or move is needed.
You can play with those examples here @Coliru
Non Copyable/Movable Types
As you saw in the example from the previous section, if you use a temporary object to initialise the contained value inside
std::optional
then the compiler will have to use move or copy construction.
But what if your type doesn’t allow that? For example std::mutex
is not movable or copyable.
In that case std::in_place
is the only way to work with such types.
Constructors With Many Arguments
Another use case is a situation where your type has more arguments in a constructor. By default optional
can work with a single argument (r-value ref), and efficiently pass it to the wrapped type. But what if you’d like to initialise std::complex(double, double)
or std::vector
?
You can always create a temporary copy and then pass it in the construction:
// vector with 4 1's:
std::optional<std::vector<int>> opt{std::vector<int>{4, 1}};
// complex type:
std::optional<std::complex<double>> opt2{std::complex<double>{0, 1}};
or use in_place
and the version of the constructor that handles variable argument list:
template< class... Args >
constexpr explicit optional( std::in_place_t, Args&&... args );
// or initializer_list:
template< class U, class... Args >
constexpr explicit optional( std::in_place_t,
std::initializer_list<U> ilist,
Args&&... args );
std::optional<std::vector<int>> opt{std::in_place_t, 4, 1};
std::optional<std::complex<double>> opt2{std::in_place_t, 0, 1};
The second option is quite verbose and omits to create temporary objects. Temporaries - especially for containers or larger objects, are not as efficient as constructing in place.
The emplace()
member function
If you want to change the stored value inside optional then you can use assignment operator or call emplace()
.
Following the concepts introduce in C++11 (emplace methods for containers), you have a way to efficiently create (and destroy the old value if needed) a new object.
std::make_optional()
If you don’t like std::in_place
then you can look at make_optional
factory function.
The code
auto opt = std::make_optional<UserName>();
auto opt = std::make_optional<std::vector<int>>(4, 1);
Is as efficient as
std::optional<UserName> opt{std::in_place};
std::optional<std::vector<int>> opt{std::in_place_t, 4, 1};
make_optional
implement in place construction equivalent to:
return std::optional<T>(std::in_place, std::forward<Args>(args)...);
And also thanks to mandatory copy elision from C++17 there is no temporary object involved.
More
std::optional
has 8 versions of constructors! So if you’re brave you can analyze them @cppreference - std::optional
constructor.
In std::variant
std::variant
has two in_place
helpers that you can use:
std::in_place_type
- used to specify which type you want to change/set in the variantstd::in_place_index
- used to specify which index you want to change/set. Types are numerated from 0.- In a variant
std::variant<int, float, std::string>
-int
has the index0
,float
has index1
and the string has index of2
. The index is the same value as returned fromvariant::index
method.
- In a variant
Fortunately, you don’t always have to use the helpers to create a variant. It’s smart enough to recognise if it can be constructed from the passed single parameter:
// this constructs the second/float:
std::variant<int, float, std::string> intFloatString { 10.5f };
For variant we need the helpers for at least two cases:
- ambiguity - to distinguish which type should be created where several could match
- efficient complex type creation (similar to optional)
Note: by default variant is initialised with the first type - assuming it has a default constructor. If the default constructor is not available, then you’ll get a compiler error. This is different from std::optional
which is initialised to an empty optional - as mentioned in the previous section.
Ambiguity
What if you have initialization like:
std::variant<int, float> intFloat { 10.5 }; // conversion from double?
The value 10.5
could be converted to int
or float
so the compiler will report a few pages of template errors… but basically, it cannot deduce what type should double
be converted to.
But you can easily handle such error by specifying which type you’d like to create:
std::variant<int, float> intFloat { std::in_place_index<0>, 10.5 };
// or
std::variant<int, float> intFloat { std::in_place_type<int>, 10.5 };
Complex Types
Similarly to std::optional
if you want to efficiently create objects that get several constructor arguments - the just use std::in_place*
:
For example:
std::variant<std::vector<int>, std::string> vecStr {
std::in_place_index<0>, { 0, 1, 2, 3 } // initializer list passed into vector
};
More
std::variant
has 8 versions of constructors! So if you’re brave you can analyze them @cppreference - std::variant
constructor.
In std::any
Following the style of two previous types, std::any
can use std::in_place_type
to efficiently create objects in place.
Complex Types
In the below example a temporary object will be needed:
std::any a{UserName{"hello"}};
but with:
std::any a{std::in_place_type<UserName>,"hello"};
The object is created in place with the given set of arguments.
std::make_any
For convenience std::any
has a factory function called std::make_any
that returns
return std::any(std::in_place_type<T>, std::forward<Args>(args)...);
In the previous example we could also write:
auto a = std::make_any<UserName>{"hello"};
make_any
is probably more straightforward to use.
More
std::any
has only 6 versions of constructors (so not 8 as variant/optional). If you’re brave you can analyze them @cppreference - std::any
constructor.
Summary
Since C++11 programmers got a new technique to initialise objects “in place” (see all .emplace()
methods for containers) - this avoids unnecessary temporary copies and also allows to work with non-movable/non-copyable types.
With C++17 we got several wrapper types - std::any
, std::optional
, std::variant
- that also allows you to create objects in place efficiently.
If you want the full efficiency of the types, it’s probably a good idea to learn how to use std::in_place*
helpers or call make_any
or make_optional
to have equivalent results.
As a reference to this topic, see a recent Jason Turner’s video in his C++ Weekly channel. You can watch it here:
I've prepared a valuable bonus if you're interested in Modern C++!
Learn all major features of recent C++ Standards!
Check it out here: