What's the advantage of `std::optional` over `

2020-07-09 08:50发布

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?

5条回答
SAY GOODBYE
2楼-- · 2020-07-09 09:00

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.

查看更多
该账号已被封号
3楼-- · 2020-07-09 09:03

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 an optional you're likely to have a much better time debugging than if you were to use shared_ptr or the like. (Note that the * dereference operator on an optional still gives UB in this case; using value() 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:

(t == nullptr) ? "default" : *t

with

t.value_or("default")

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 an optional 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 empty optional.

查看更多
冷血范
4楼-- · 2020-07-09 09:08

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

查看更多
你好瞎i
5楼-- · 2020-07-09 09:15

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.

查看更多
劫难
6楼-- · 2020-07-09 09:25

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 an difference comparing even to the raw pointer could be significant.

查看更多
登录 后发表回答