In-Place Construction for std::any, std::variant and std::optional

(From C++ Stories)

We have the following in_place helper types:

These helpers are used to efficiently initialise objects in-place, that is, without additional temporary copy or move operations.

Let's see how those helpers are used.

In std::optional

std::optional being a wrapper type, 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 aspects:

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.

The code creates a temporary object and then moves it into the object stored in std::optional.


We can use more efficient constructor by leveraging std::in_place_t:

std::optional<UserName> opt{std::in_place};

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.

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:

// either simply
template< class... Args >
constexpr explicit optional( std::in_place_t, Args&&... args );

// or an 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.

std::make_optional()

If you don't like std::in_place then you can look at std::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 implements 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.

In std::variant*

std::variant has two in_place helpers that you can use:

Fortunately, you don't always have to use the helpers to create a variant. A variant is 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 in at least two cases:

Ambiguity

What if you have an 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.

Still, 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
};

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)...);

Instead we could write:

auto a = std::make_any<UserName>{"hello"};

Using make_any is probably more straightforward.