轉自:https://www.bfilipek.com/2018/05/using-optional.html
轉貼於此,以備查閱。
Let’s take a pair of two types <YourType, bool>
- what can you do with such composition?
In this article, I’ll describe std:optional
- a new helper type added in C++17. It’s a wrapper for your type and a flag that indicates if the value is initialized or not. Let’s see where it can be useful and how you can use it.
Intro
By adding the boolean flag to other types, you can achieve a thing called “nullable types”. As mentioned, the flag is used to indicate whether the value is available or not. Such wrapper represents an object that might be empty in an expressive way (so not via comments :))
While you can achieve “null-ability” by using unique values (-1, infinity, nullptr
), it’s not as clear as the separate wrapper type. Alternatively, you could even use std::unique_ptr<Type>
and treat the empty pointer as not initialized - this works, but comes with the cost of allocating memory for the object.
Optional types - that come from functional programming world - bring type safety and expressiveness. Most of other languages have something similar: for example std::option
in Rust, Optional<T>
in Java, Data.Maybe
in Haskell.
std::optional
was added in C++17 and brings a lot of experience from boost::optional
that was available for many years. Since C++17 you can just #include <optional>
and use the type.
Such wrapper is still a value type (so you can copy it, via deep copy). What’s more,std::optional
doesn’t need to allocate any memory on the free store.
std::optional
is a part of C++ vocabulary types along with std::any
, std::variant
and std::string_view
.
When to use
Usually, you can use an optional wrapper in the following scenarios:
- If you want to represent a nullable type nicely.
- Rather than using unique values (like
-1
,nullptr
,NO_VALUE
or something) - For example, user’s middle name is optional. You could assume that an empty string would work here, but knowing if a user entered something or not might be important. With
std::optional<std::string>
you get more information.
- Rather than using unique values (like
- Return a result of some computation (processing) that fails to produce a value and is not an error.
- For example finding an element in a dictionary: if there’s no element under a key it’s not an error, but we need to handle the situation.
- To perform lazy-loading of resources.
- For example, a resource type has no default constructor, and the construction is substantial. So you can define it as
std::optional<Resource>
(and you can pass it around the system), and then load only if needed later.
- For example, a resource type has no default constructor, and the construction is substantial. So you can define it as
- To pass optional parameters into functions.
I like the description from boost optional which summarizes when we should use the type:
From the boost::optional
documentation: When to use Optional
It is recommended to use
optional<T>
in situations where there is exactly one, clear (to all parties) reason for having no value of typeT
, and where the lack of value is as natural as having any regular value ofT
While sometimes the decision to use optional might be blurry, you shouldn’t use it for error handling. As it best suits the cases when the value is empty and it’s a normal state of the program.
Basic Example
Here’s a simple example of what you can do with optional:
std::optional<std::string> UI::FindUserNick() { if (nick_available) return { mStrNickName }; return std::nullopt; // same as return { }; } // use: std::optional<std::string> UserNick = UI->FindUserNick(); if (UserNick) Show(*UserNick);
In the above code we define a function that returns optional containing a string. If the user’s nickname is available, then it will return a string. If not, then it returns nullopt
. Later we can assign it to an optional and check (it converts to bool
) if it contains any value or not. Optional defines operator*
so we can easily access the contained value.
In the following sections you’ll see how to createstd::optional
, operate on it, pass around and even what is the performance cost you might want to consider.
The Series
This article is part of my series about C++17 Library Utilities. Here’s the list of the other topics that I’ll cover:
- Refactoring with
std::optional
- Using
std::optional
(this post) - Error handling and
std::optional
- About
std::variant
- About
std::any
- In place construction for
std::optional
,std::variant
andstd::any
std::string_view
Performance- C++17 string searchers & conversion utilities
- Working with
std::filesystem
- Something more?
Resources about C++17 STL:
- C++17 - The Complete Guide by Nicolai Josuttis
- C++ Fundamentals Including C++ 17 by Kate Gregory
- Practical C++14 and C++17 Features - by Giovanni Dicanio
- C++17 STL Cookbook by Jacek Galowicz
OK, so let’s move to std::optional
.
std::optional
Creation
There are several ways to create std::optional
:
// empty: std::optional<int> oEmpty; std::optional<float> oFloat = std::nullopt; // direct: std::optional<int> oInt(10); std::optional oIntDeduced(10); // deduction guides // make_optional auto oDouble = std::make_optional(3.0); auto oComplex = make_optional<std::complex<double>>(3.0, 4.0); // in_place std::optional<std::complex<double>> o7{std::in_place, 3.0, 4.0}; // will call vector with direct init of {1, 2, 3} std::optional<std::vector<int>> oVec(std::in_place, {1, 2, 3}); // copy/assign: auto oIntCopy = oInt;
As you can see in the above code sample, you have a lot of flexibility with the creation of optional. It’s very simple for primitive types and this simplicity is extended for even complex types.
The “in_place” construction is especially interesting, and the tag std::in_place
is also supported in other types like any
and variant
.
For example, you can write:
// https://godbolt.org/g/FPBSak struct Point { Point(int a, int b) : x(a), y(b) { } int x; int y; }; std::optional<Point> opt{std::in_place, 0, 1}; // vs std::optional<Point> opt{{0, 1}};
This saves the creation of a temporary Point
object.
I’ll address std::in_place
later in a separate post, so stay tuned.
Returning std::optional
If you return an optional from a function, then it’s very convenient to return just std::nullopt
or the computed value.
std::optional<std::string> TryParse(Input input) { if (input.valid()) return input.asString(); return std::nullopt; }
In the above example you can see that I return std::string
computed from input.asString()
and it’s wrapped in optional
. If the value is unavailable then you can just return std::nullopt
.
Of course, you can also declare an empty optional at the beginning of your function and reassign if you have the computed value. So we could rewrite the above example as:
std::optional<std::string> TryParse(Input input) { std::optional<std::string> oOut; // empty if (input.valid()) oOut = input.asString(); return oOut; }
It probably depends on the context which version is better. I prefer short functions, so I’d chose the first option (with multiple returns).
Accessing The Stored Value
Probably the most important operation for optional (apart from creation) is the way how you can fetch the contained value.
There are several options:
operator*
andoperator->
- similar to iterators. If there’s no value the behaviour is undefined!value()
- returns the value, or throws std::bad_optional_accessvalue_or(defaultVal)
- returns the value if available, ordefaultVal
otherwise.
To check if the value is present you can use has_value()
method or just check if (optional)
as optional is automatically converted to bool
.
Here’s an example:
// by operator* std::optional<int> oint = 10; std::cout<< "oint " << *opt1 << '\n'; // by value() std::optional<std::string> ostr("hello"); try { std::cout << "ostr " << ostr.value() << '\n'; } catch (const std::bad_optional_access& e) { std::cout << e.what() << "\n"; } // by value_or() std::optional<double> odouble; // empty std::cout<< "odouble " << odouble.value_or(10.0) << '\n';
So the most useful way is probably just to check if the value is there and then access it:
// compute string function: std::optional<std::string> maybe_create_hello(); // ... if (auto ostr = maybe_create_hello(); ostr) std::cout << "ostr " << *ostr << '\n'; else std::cout << "ostr is null\n";
std::optional
Operations
Let’s see what are other operations on the type:
Changing the value
If you have existing optional object, then you can easily change the contained value by using several operations like emplace
, reset
, swap
, assign. If you assign (or reset) with a nullopt
then if the optional contains a value its destructor will be called.
Here’s a little summary:
#include <optional> #include <iostream> #include <string> class UserName { public: explicit UserName(const std::string& str) : mName(str) { std::cout << "UserName::UserName(\'"; std::cout << mName << "\')\n"; } ~UserName() { std::cout << "UserName::~UserName(\'"; std::cout << mName << "\')\n"; } private: std::string mName; }; int main() { std::optional<UserName> oEmpty; // emplace: oEmpty.emplace("Steve"); // calls ~Steve and creates new Mark: oEmpty.emplace("Mark"); // reset so it's empty again oEmpty.reset(); // calls ~Mark // same as: //oEmpty = std::nullopt; // assign a new value: oEmpty.emplace("Fred"); oEmpty = UserName("Joe"); }
The code is available here: @Coliru
Comparisons
std::optional
allows you to compare contained objects almost “normally”, but with a few exceptions when the operands are nullopt
. See below:
#include <optional> #include <iostream> int main() { std::optional<int> oEmpty; std::optional<int> oTwo(2); std::optional<int> oTen(10); std::cout << std::boolalpha; std::cout << (oTen > oTwo) << "\n"; std::cout << (oTen < oTwo) << "\n"; std::cout << (oEmpty < oTwo) << "\n"; std::cout << (oEmpty == std::nullopt) << "\n"; std::cout << (oTen == 10) << "\n"; }
The above code generates:
true // (oTen > oTwo) false // (oTen < oTwo) true // (oEmpty < oTwo) true // (oEmpty == std::nullopt) true // (oTen == 10)
The code is available here: @Coliru
Examples of std::optional
Here are two a few longer examples where std::optional
fits nicely.
User name with an optional nickname and age
#include <optional> #include <iostream> class UserRecord { public: UserRecord (const std::string& name, std::optional<std::string> nick, std::optional<int> age) : mName{name}, mNick{nick}, mAge{age} { } friend std::ostream& operator << (std::ostream& stream, const UserRecord& user); private: std::string mName; std::optional<std::string> mNick; std::optional<int> mAge; }; std::ostream& operator << (std::ostream& os, const UserRecord& user) { os << user.mName << ' '; if (user.mNick) { os << *user.mNick << ' '; } if (user.mAge) os << "age of " << *user.mAge; return os; } int main() { UserRecord tim { "Tim", "SuperTim", 16 }; UserRecord nano { "Nathan", std::nullopt, std::nullopt }; std::cout << tim << "\n"; std::cout << nano << "\n";