Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What's the advantage of `std::optional` over `std::shared_ptr` and `std::unique_ptr`?

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?

like image 950
The Quantum Physicist Avatar asked Mar 21 '17 14:03

The Quantum Physicist


3 Answers

What is it that we win by adding std::optional over just using std::shared_ptr in general?

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, plus std::shared_ptr would maintain control block. While std::optional on another side:

If an optional contains a value, the value is guaranteed to be allocated as part of the optional object footprint, i.e. no dynamic memory allocation ever takes place. Thus, an optional object models an object, not a pointer, even though the operator*() and operator->() are defined.

so no dynamic memory allocation is involved and difference comparing even to the raw pointer could be significant.

like image 161
Slava Avatar answered Sep 20 '22 08:09

Slava


An 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 about unique_ptr is much easier, as you just have to track where it is moved around. Reasoning about optional'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 for optional and not for shared_ptr or unique_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 an optional<enum_flags> doesn't force this on the caller. The very cheapness of optional 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.

like image 34
Yakk - Adam Nevraumont Avatar answered Sep 21 '22 08:09

Yakk - Adam Nevraumont


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:

class Person
{
   mutable std::unique_ptr<std::string> name;
   size_t uuid;
public:
   Person(size_t _uuid) : uuid(_uuid){}
   std::string GetName() const
   {
      if (!name)
         name = PersonLoader::LoadName(uuid); // magic PersonLoader class knows how to read this person's name from disk
      if (!name)
         return "";
      return *name;
   }
};

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 return nullptr 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. Without std::optional, a solution to this would be to create a boolean isLoaded for the name, and indeed every optional field. (What if we "just encapsulated the flag into a struct"? Well, then you'd have implemented optional, but done a worse job of it):

class Person
{
   mutable std::optional<std::unique_ptr<std::string>> name;
   size_t uuid;
public:
   Person(size_t _uuid) : uuid(_uuid){}
   std::string GetName() const
   {
      if (!name){ // need to load name from disk
         name = PersonLoader::LoadName(uuid);
      }
      // else name's already been loaded, retrieve cached value
      if (!name.value())
         return "";
      return *name.value();
   }
};

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 scale

like image 38
AndyG Avatar answered Sep 20 '22 08:09

AndyG