How Should One Use Std::Optional

How should one use std::optional?

The simplest example I can think of:

std::optional<int> try_parse_int(std::string s)
{
//try to parse an int from the given string,
//and return "nothing" if you fail
}

The same thing might be accomplished with a reference argument instead (as in the following signature), but using std::optional makes the signature and usage nicer.

bool try_parse_int(std::string s, int& i);

Another way that this could be done is especially bad:

int* try_parse_int(std::string s); //return nullptr if fail

This requires dynamic memory allocation, worrying about ownership, etc. - always prefer one of the other two signatures above.


Another example:

class Contact
{
std::optional<std::string> home_phone;
std::optional<std::string> work_phone;
std::optional<std::string> mobile_phone;
};

This is extremely preferable to instead having something like a std::unique_ptr<std::string> for each phone number! std::optional gives you data locality, which is great for performance.


Another example:

template<typename Key, typename Value>
class Lookup
{
std::optional<Value> get(Key key);
};

If the lookup doesn't have a certain key in it, then we can simply return "no value."

I can use it like this:

Lookup<std::string, std::string> location_lookup;
std::string location = location_lookup.get("waldo").value_or("unknown");

Another example:

std::vector<std::pair<std::string, double>> search(
std::string query,
std::optional<int> max_count,
std::optional<double> min_match_score);

This makes a lot more sense than, say, having four function overloads that take every possible combination of max_count (or not) and min_match_score (or not)!

It also eliminates the accursed "Pass -1 for max_count if you don't want a limit" or "Pass std::numeric_limits<double>::min() for min_match_score if you don't want a minimum score"!


Another example:

std::optional<int> find_in_string(std::string s, std::string query);

If the query string isn't in s, I want "no int" -- not whatever special value someone decided to use for this purpose (-1?).


For additional examples, you could look at the boost::optional documentation. boost::optional and std::optional will basically be identical in terms of behavior and usage.

What are the advantages/disadvantages of std::optional over nullptr?

The sole job of std::optional is to extend the type domain by an additional "null" value. Every pointer type T* already has a value considered "null" - nulltpr.

Thus, it's not a good idea to compare those two directly, because they answer different questions. Sometimes it's important to differentiate between "no result" and "null result"1 (which is one of the possible interpretations), sometimes it's not. You should use whichever fits your needs.

Now if the only reason the code returned a pointer was to make use of the implicit pointer nullability, then the proper solution would be to change it to return std::optional<SomePointer> (or perhaps std::optional<std::reference_wrapper<SomePointer>>) instead, but that's not what you asked about.


1 Of course at this point it's also worthwhile to consider something like e.g. struct NoResult {}; using Result = std::variant<NoResult, SomePointer*>; to make it even more explicit.

how does std::optional work under the hood?

return {};

will simply call the default constructor of the class.

By giving the class a conversion operator to bool it can be implicitly converted to a bool when needed.

It would look something along the lines of

template <typename T>
class optional {
public:
optional() {}
optional(T t) : has_value(true), value(std::move(t)) {}

operator bool() {
return has_value;
}

private:
bool has_value = false;
T value;
}

Very simplified, missing assignement operators and more.

Take value out of std::optional

operator*/value() returns a reference to the value held by the optional, so you can simply use std::move to move it to a temporary variable

std::optional<std::string> opt = "abc";
// "take" the contained value by calling operator* on a rvalue to optional
auto taken = *std::move(opt);

This will invoke the rvalue reference overload of operator*() of the optional, which returns an rvalue reference to the contained value.

You can also directly perform std::move on the return value of the operator*() of the lvalue optional, which will convert the lvalue reference of the contained value into an rvalue

auto taken = std::move(*opt);

How does std::optional delay initialization? / How is std::optional implemented?

The "obvious" way to represent an std::optional<T> is to use an indication whether the value is set together with a union containing a T, i.e., something like this:

template <typename T>
class optional {
bool isSet = false;
union { T value; };
public:
// ...
};

By default the members in the union are not initialized. Instead, you'll need to use placement new and manual destruction to manage the life-time of the entity within the union. Conceptually that is similar to using an array of bytes but the compiler handles any alignment requirements.

Here a program with some of the operations shown:

#include <iostream>
#include <memory>
#include <string>
#include <utility>
#include <cassert>

template <typename T>
class optional {
bool isSet = false;
union { T value; };
void destroy() { if (this->isSet) { this->isSet = true; this->value.~T(); } }

public:
optional() {}
~optional() { this->destroy(); }
optional& operator=(T&& v) {
this->destroy();
new(&this->value) T(std::move(v));
this->isSet = true;
return *this;
}

explicit operator bool() const { return this->isSet; }
T& operator*() { assert(this->isSet); return this->value; }
T const& operator*() const { assert(this->isSet); return this->value; }
};

int main()
{
optional<std::string> o, p;
o = "hello";
if (o) {
std::cout << "optional='" << *o << "'\n";
}
}

What to use std::optional or std::unique_ptr

std::optional<A> guarantees that no auxiliary memory allocation will take place. This means that the raw buffer for the potential object of type A is embedded into std::optional<A>. It is an integral part of std::optional's memory footprint. This means that the memory size of std::optional<A> will always be at least sizeof(A), regardless of whether that optional A object currently lives or not. That is how much std::optional<A> will contribute to the total size of B.

std::unique_ptr<A> is a pointer. Its size is about the same as the size of a regular naked pointer. That is how much memory std::unique_ptr<A> itself occupies inside B. In order to make it point to a valid A object you will have to allocate that A elsewhere, independently. When A exists, it occupies memory. When A does not exist it does not occupy memory.

The above is something to take into account when making your decision. std::optional<A> does not involve dynamic memory allocation/deallocation, but the price you pay for that is potentially "wasted" memory inside your std::optional<A>. Using std::optional for massively instantiated and/or large objects might prove to be quite wasteful, especially if the object spends most of its lifetime in empty state.

This means that the purpose of std::optional is not exactly aimed at optional long-term storage. std::optional is something to be used locally: e.g. as optional local values, optional parameters of functions, optional return values. Long-term use is also OK, as long as you are not instantiating such objects in massive numbers.

std::unique_ptr<A> does not waste memory, but the price you pay for that is dynamic memory allocation/deallocation.

Of course, ownership semantics is also quite different. std::optional is copyable. std::unique_ptr is movable, but not copyable.

Does passing an std::optionalT by reference actually save copying?

If you pass actual std::optional<std::string> then yes, there would be no copy. But if you pass just std::string then temporary optional has to be constructed first, resulting in a copy of the string.



Related Topics



Leave a reply



Submit