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
Should One Never Use Static Inline Function
Significance of a .Inl File in C++
Namespaces for Enum Types - Best Practices
How to Pass Std::Unique_Ptr into a Function
Improve Matching of Feature Points with Opencv
Differencebetween "::" "." and "->" in C++
Adding Multiple Executables in Cmake
Should Every Class Have a Virtual Destructor
What Is the Recommended Way to Align Memory in C++11
Differencebetween Const_Iterator and Iterator
When to Use Const Char * and When to Use Const Char []
Does C++11 Unique_Ptr and Shared_Ptr Able to Convert to Each Other's Type
String::Size_Type Instead of Int
How to Load a Bmp on Glut to Use It as a Texture
Why Does Msvc Pick a Long Long as the Type for -2147483648
How to Print a String to the Console at Specific Coordinates in C++