The reasoning of std::optional
is made by saying that it may or may not contain a value. Hence, it saves us the effort of constructing a, probably, big object, if we don't need it.
For example, a factory here, will not construt the object if some condition is not met:
#include <string>
#include <iostream>
#include <optional>
std::optional<std::string> create(bool b)
{
if(b)
return "Godzilla"; //string is constructed
else
return {}; //no construction of the string required
}
But then how is this different from this:
std::shared_ptr<std::string> create(bool b)
{
if(b)
return std::make_shared<std::string>("Godzilla"); //string is constructed
else
return nullptr; //no construction of the string required
}
What is it that we win by adding std::optional
over just using std::shared_ptr
in general?
@Slava mentioned the advantage of performing no memory allocations, but that's a fringe benefit (OK, it may be a significant benefit in some cases, but my point is, it is not the main one).
The main benefit is (IMHO) clearer semantics:
Returning a pointer usually means (in modern C++) "allocates memory", or "handles memory", or "knows the address in memory of this and that".
Returning an optional value means "not having a result of this computation, is not an error": the name of the return type, tells you something about how the API was conceived (the intent of API, instead of implementation).
Ideally, if your API doesn't allocate memory, it shouldn't return a pointer.
Having optional type available in the standard, ensures you can write more expressive APIs.
Importantly, you get a known, catchable, exception instead of undefined behaviour if you try to access the
value()
from an optional when it's not there. Thus, if things go wrong with anoptional
you're likely to have a much better time debugging than if you were to useshared_ptr
or the like. (Note that the*
dereference operator on anoptional
still gives UB in this case; usingvalue()
is the safer alternative).Also, there's the general convenience of methods such as
value_or
, which allow you to specify a "default" value quite easily. Compare:with
The latter is both more readable and slightly shorter.
Finally, the storage for an item in an
optional
is within the object. This means that anoptional
requires more storage that a pointer if the object is not present; however, it also means that no dynamic allocation is required to put an object into an emptyoptional
.A pointer may or may not be NULL. Whether that means something to you is entirely up to you. In some scenarios,
nullptr
is a valid value that you deal with, and in others it can be used as a flag to indicate "no value, move along".With
std::optional
, there is an explicit definition of "contains a value" and "doesn't contain a value". You could even use a pointer type with optional!Here's a contrived example:
I have a class named
Person
, and I want to lazily-load their data from the disk. I need to indicate whether some data has been loaded or not. Let's use a pointer for that:Great, I can use the
nullptr
value to tell whether the name has been loaded from disk yet.But what if a field is optional? That is,
PersonLoader::LoadName()
may returnnullptr
for this person. Do we really want to go out to disk every time someone requests this name?Enter
std::optional
. Now we can track if we've already tried to load the name and if that name is empty. Withoutstd::optional
, a solution to this would be to create a booleanisLoaded
for the name, and indeed every optional field. (What if we "just encapsulated the flag into a struct"? Well, then you'd have implementedoptional
, but done a worse job of it):Now we don't need to go out to disk each time;
std::optional
allows us to check for that. I've written a small example in the comments demonstrating this concept on a smaller scaleAn optional is a nullable value type.
A
shared_ptr
is a reference counted reference type that is nullable.A
unique_ptr
is a move-only reference type that is nullable.What they share in common is that they are nullable -- that they can be "absent".
They are different, in that two are reference types, and the other is a value type.
A value type has a few advantages. First of all, it doesn't require allocation on the heap -- it can be stored along side other data. This removes a possible source of exceptions (memory allocation failure), can be much faster (heaps are slower than stacks), and is more cache friendly (as heaps tend to be relatively randomly arranged).
Reference types have other advantages. Moving a reference type does not require that the source data be moved.
For non-move only reference types, you can have more than one reference to the same data by different names. Two different value types with different names always refer to different data. This can be an advantage or disadvantage either way; but it does make reasoning about a value type much easier.
Reasoning about
shared_ptr
is extremely hard. Unless a very strict set of controls is placed on how it is used, it becomes next to impossible to know what the lifetime of the data is. Reasoning aboutunique_ptr
is much easier, as you just have to track where it is moved around. Reasoning aboutoptional
's lifetime is trivial (well, as trivial as what you embedded it in).The optional interface has been augmented with a few monadic like methods (like
.value_or
), but those methods often could easily be added to any nullable type. Still, at present, they are there foroptional
and not forshared_ptr
orunique_ptr
.Another large benefit for optional is that it is extremely clear you expect it to be nullable sometimes. There is a bad habit in C++ to presume that pointers and smart pointers are not null, because they are used for reasons other than being nullable.
So code assumes some shared or unique ptr is never null. And it works, usually.
In comparison, if you have an optional, the only reason you have it is because there is the possibility it is actually null.
In practice, I'm leery of taking a
unique_ptr<enum_flags> = nullptr
as an argument, where I want to say "these flags are optional", because forcing a heap allocation on the caller seems rude. But anoptional<enum_flags>
doesn't force this on the caller. The very cheapness ofoptional
makes me willing to use it in many situations I'd find some other work around if the only nullable type I had was a smart pointer.This removes much of the temptation for "flag values", like
int rows=-1;
.optional<int> rows;
has clearer meaning, and in debug will tell me when I'm using the rows without checking for the "empty" state.Functions that can reasonably fail or not return anything of interest can avoid flag values or heap allocation, and return
optional<R>
. As an example, suppose I have an abandonable thread pool (say, a thread pool that stops processing when the user shuts down the application).I could return
std::future<R>
from the "queue task" function and use exceptions to indicate the thread pool was abandoned. But that means that all use of the thread pool has to be audited for "come from" exception code flow.Instead, I could return
std::future<optional<R>>
, and give the hint to the user that they have to deal with "what happens if the process never happened" in their logic."Come from" exceptions can still occur, but they are now exceptional, not part of standard shutdown procedures.
In some of these cases,
expected<T,E>
will be a better solution once it is in the standard.Let's say you need to return a symbol from a function with flag "not a value". If you would use
std::shared_ptr
for that you would have huge overhead -char
would be allocated in dynamic memory, plusstd::shared_ptr
would maintain control block. While std::optional on another side:so no dynamic memory allocation is involved an difference comparing even to the raw pointer could be significant.