What is the point of `std::make_optional`
Asked Answered
T

2

34

All the std::make_ are made redundant by C++17 with the introduction of Class template argument deduction (except make_unique and make_shared).

So what is the point of std::make_optional? As far as I can tell it does the exact same thing as the deduction guides for std::optional.

Is there a scenario where std::make_optional is preferred over deduction guides?

Trinitrotoluene answered 4/7, 2020 at 22:1 Comment(4)
"except make_unique and make_shared, they are still needed for reasons of leak free memory" - that is not why those functions exist. They exist to create unique_ptr/shared_ptr objects more efficiently (especially shared_ptr) than creating them with raw pointers passed to their constructors.Wringer
@RemyLebeau correct, I didn't realised that with C++17 you can't have interleaved parameter evaluations so it can't leak anymore.Trinitrotoluene
@RemyLebeau Leak freedom is precisely why those functions exist. If you write void func(std::unique_ptr<int>, std::unique_ptr<int>); and then call it func(std::unique_ptr<int>(new int(42)), std::unique_ptr<int>(new int(13));, you can get a memory leak, because the compiler is allowed to make calls in this order: new, new, unique_ptr ctor, unique_ptr ctor. If the second new throws, the first allocation is leaked. If you call func(std::make_unique<int>(42), std::make_unique<int>(13));, the calls to new and the calls to the unique_ptr ctor can not get separated.Decoration
@Decoration since C++17 that sequence is not allowed anymore. Iirc the order of the function arguments is implementation specific, but with that order each argument evaluation is sequenced before the next.Trinitrotoluene
C
24

One example of the difference is when you want (for whatever reason) to make an optional containing an optional:

#include <optional>
#include <type_traits>

int main()
{
    auto inner=std::make_optional(325);
    auto opt2=std::make_optional(inner); // makes std::optional<std::optional<int>>
    auto opt3=std::optional(inner);      // just a copy of inner
    static_assert(std::is_same_v<decltype(opt2), std::optional<std::optional<int>>>);
    static_assert(std::is_same_v<decltype(opt3), std::optional<int>>);
}
Calicut answered 4/7, 2020 at 22:36 Comment(7)
It is unfortunate that the library solution is needed for generic code because the core language chose a default that is convenient for the non-generic case.Railhead
@DavisHerring you wanted std::optional<int> a{}; auto x = std::optional{a} to be std::optional<std::optional<int>> and std:vector<int> v1{}; auto v2 = std::vector{v1} to be std::vector<std::vector<int>>? And what would std::string s1{}; auto s2 = std::string{s1} be?Trinitrotoluene
@Trinitrotoluene The fact that vector{v1} and vector{v1, v1} are both valid and wildly different types is, I think, a mistake. string{s1} isn't the same kind of thing at all, since string is actually a type. Bonus points for tuple{x} being one of three different things dependent on x...Burdened
@Trinitrotoluene Or more so that vector{x} is either a vector containing 1 element (x) or a copy of x, depending on x.Burdened
@bolov: Yes; it’s a common (though surely not universal) opinion that “wrap by default” would have been better than “copy by default” because the latter can already be written as just auto x=a; without harming genericity.Railhead
@Trinitrotoluene I was joking, I was pointing out how his code doesn't have any * or pointers of the sort and yet you still called him a three star programming **chuckling.Bugeye
@Burdened very good points. My personal peeve is list intialization having the same syntax as a constructor. But now that I think about it that would solve the vector and tuple problem, but not the optional one.Trinitrotoluene
T
5

Another example of a use for std::make_optional() would be for constructing the object stored in a std::optional without creating a temporary for it when its constructor takes multiple arguments.

For example, consider the following Point3D class whose constructor has multiple arguments:

struct Point3D {
   Point3D(int xx, int yy, int zz): x(xx), y(yy), z(zz) {}
   int x, y, z;
};

Imagine that you want to initialize the object stored by a std::optional<Point3D> to Point3D(1, 2, 3)X, then you could proceed as:

std::optional oPoint = Point3D(1, 2, 3);

However, this would create a Point3D temporary object that is then moved into the std::optional. Instead, by using the std::make_optional() convenience function template you can avoid the creation of that temporary:

auto oPoint = std::make_optional<Point3D>(1, 2, 3);

Since we are talking about C++17 here (i.e., std::optional was introduced in C++17), guaranteed copy elision applies here, and therefore no Point3D temporary object is created.


Note that you can still avoid the creation of the temporary object without using std::make_optional() by passing std::in_place to the constructor of the std::optional<Point3D> object:

std::optional<Point3D> oPoint{std::in_place, 1, 2, 3};

This will construct the stored Point3D object in place, thus avoiding the creation of a temporary. Nevertheless, you may find this more verbose than the approach with std::make_optional().


Xstd::optional<Point3D> oPoint(1, 2, 3); doesn't compile.

Tinfoil answered 20/12, 2020 at 17:15 Comment(2)
"when its constructor takes multiple arguments"... how would this be different if the constructor took a single argument?Router
@DavidFaure In that case you can simply pass the single argument the constructor takes directly to std::optional's constructor. For example, for struct Foo{ Foo(int){} };, you can just construct a std::option<Foo> with std::optional<Foo> optFoo(7);. Note that this wouldn't compile if Foo's constructor took multiple arguments as std::in_place would need to be passed to std::option<Foo>'s constructor.Atheling

© 2022 - 2024 — McMap. All rights reserved.