Let's take a pair of two types of <YourType, bool>
- what can you do with a composition of this kind?
In this article I will tell you about std::optional
- a new auxiliary type added in C ++ 17. This is a wrapper for your type and the flag indicates whether your value is initialized or not. Let's see where this can be useful.
By adding logical flags to other types, you can achieve what is called "Nullable types". As mentioned earlier, the flag is used to indicate whether a value is available or not. Such a wrapper expressively represents an object that can be empty (not through comments :).
You can reach the null value of an object by using unique identifiers (-1, infinity, nullptr
), but this is not as precisely a thought as a separate wrapper type. You can even use std::unique_ptr<Type>
and treat the null pointer as an uninitialized object - this will work, but at the same time you will have to accept the cost of allocating memory for the object where it is not necessary.
Optional types are what came from the world of functional programming, bringing with them type safety and expressiveness. Most other languages have something similar: for example std::option
in Rust
, Optional<T>
in Java
, Data.Maybe
in Haskell
.
std::optional
was added to C ++ 17 from boost::optional
, where it has been available for many years. Starting in C ++ 17, you can simply write #include <optional>
to use this type.
This type is a value-type (you can copy it this way). Moreover, for std::optional
not necessary to allocate memory separately.
std::optional
is part of C ++ dictionary types along with std::any
, std::variant
and std::string_view
.
Usually, an optional type can be used in the following scenarios:
-1
, nullptr
, NO_VALUE
or something like that).std::optional<std::string>
you can get more information.std::optional<Resource>
, and pass this object on to the system, and perform the download later if necessary.I like the definition of the optional type of boost
, which sums up the situations when we should use it. From the boost documentation :
The template class std :: optional controls an optional value, i.e., a value that may or may not be represented.
A common example of using an optional data type is the return value of a function, which may return an erroneous result during execution. Unlike other approaches, such as std :: pair <T, bool>, the optional data type is well managed with heavy objects for construction and is more readable, since it clearly expresses the intentions of the developer.
Although it is sometimes difficult to decide whether to use an optional type, you definitely should not use it to handle errors. It is best suited for cases where the lack of meaning is the normal behavior of the program.
Below you can see a simple example of what can be done using the optional type:
std::optional<std::string> UI::FindUserNick() { if (nick_available) return { mStrNickName }; return std::nullopt; // , { }; } // : std::optional<std::string> UserNick = UI->FindUserNick(); if (UserNick) Show(*UserNick);
In the code above, we declared a function that returns an optional string. If the username is available, it will return a string. If not, it will return std::nullopt
. Later we can assign this value to an optional type and check it ( std::optional
has a cast operator to type bool
) whether it contains a real value or not. The std::optional
also overloads operator*()
for easier access to the contained value.
In the following paragraphs, you can see how to create std::optional
, work with it, pass it, and even its performance, which you are probably interested in seeing.
This article is part of my C ++ 17 library utility series. Here is a list of other topics that I’m talking about:
C ++ 17 STL Resources:
OK, now let's work with std::optional
.
std::optional
There are several options for creating std::optional
:
// : std::optional<int> oEmpty; std::optional<float> oFloat = std::nullopt; // : 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}; // vector {1, 2, 3} std::optional<std::vector<int>> oVec(std::in_place, {1, 2, 3}); // /: auto oIntCopy = oInt;
As you can see in the example above, you have the amazing flexibility to create an object. Creating an object is very simple for both primitive types and more complex ones.
Creating in place
especially interesting and the std::in_place
also supported in other types, such as std::any
and std::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 tell you about std::in_place
later, do not switch the channel and stay with us.
std::optional
from functionIf you return an optional value from a function, then it is very convenient to return either std::nullopt
, or the resulting value:
std::optional<std::string> TryParse(Input input) { if (input.valid()) return input.asString(); return std::nullopt; }
In the example above, you can see that I am returning a std::string
, obtained from input.asString()
and wrapping it in std::optional
. If the value is not available, the function will simply return std::nullopt
.
Of course, you can also simply declare an empty optional object at the beginning of your function and assign it a calculated value if it is correct. Thus, we can rewrite the code above as follows:
std::optional<std::string> TryParse(Input input) { std::optional<std::string> oOut; // empty if (input.valid()) oOut = input.asString(); return oOut; }
Which version is better depends on the context. I prefer short functions, so my choice is version number 1 (with multiple return
).
Perhaps the most important operation for an optional type (other than creating it) is how you can get the stored value.
There are several options for this:
operator*()
and operator->()
in the same way as in iterators. If the object does not contain a real value, then the behavior is undefined !value()
- returns the value or throws an exception std::bad_optional_access
.value_or(default)
- returns the value if available, or returns default
.To check if there is a real value in the object, you can use the has_value()
method or simply check the object with if (optional) {...}
, since the optional type is overloaded with the cast operator to bool
.
For example:
// operator*() std::optional<int> oint = 10; std::cout<< "oint " << *opt1 << '\n'; // 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"; } // value_or() std::optional<double> odouble; // std::cout<< "odouble " << odouble.value_or(10.0) << '\n';
Thus, it will be most convenient to check if there is a real value in an optional object, and then use it:
// : 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
Let's see what other options the optional type has:
If you already have an optional object, you can easily change its value using the emplace
, reset
, swap
and assign
methods. If you assign (or nullify) the object std::nullopt
, then a real object that is stored in an optional one will have a destructor called.
Here is a small example:
#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"); // ~Steve Mark: oEmpty.emplace("Mark"); // oEmpty.reset(); // ~Mark // : //oEmpty = std::nullopt; // : oEmpty.emplace("Fred"); oEmpty = UserName("Joe"); }
This code is available here: @Coliru .
std::optional
allows you to compare the objects contained in it almost "normally", but with a few exceptions, when the operands are std::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"; }
When executing the code above, it will display:
true // (oTen > oTwo) false // (oTen < oTwo) true // (oEmpty < oTwo) true // (oEmpty == std::nullopt) true // (oTen == 10)
This code is available here: @Coliru .
std::optional
Below you will find two examples where std::optional
fits perfectly.
#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"; }
This code is available here: @Coliru .
#include <optional> #include <iostream> #include <string> std::optional<int> ParseInt(char*arg) { try { return { std::stoi(std::string(arg)) }; } catch (...) { std::cout << "cannot convert \'" << arg << "\' to int!\n"; } return { }; } int main(int argc, char* argv[]) { if (argc >= 3) { auto oFirst = ParseInt(argv[1]); auto oSecond = ParseInt(argv[2]); if (oFirst && oSecond) { std::cout << "sum of " << *oFirst << " and " << *oSecond; std::cout << " is " << *oFirst + *oSecond << "\n"; } } }
This code is available here: @Coliru .
The code above uses an optional data type to indicate whether the conversion was successful. Note that we actually wrapped the exceptions that C ++ might throw into the optional data type, so we will skip all the errors associated with this. This moment is quite controversial, since we usually have to report errors to the user.
std::optional<Key>
than to leave comments like: // 0xDEADBEEF,
or something like that.When you use std::optional
, you pay for it with increased memory usage. At least one additional byte.
In the abstract, your version of STL may implement the optional data type as:
template <typename T> class optional { bool _initialized; std::aligned_storage_t<sizeof(T), alignof(T)> _storage; public: // };
In short, std::optional
simply wraps your type, prepares a place for it, and adds one boolean parameter. This means that it will increase the size of your type according to the alignment rules.
There is one comment for this construction : "No standard library can implement std::optional
like this (it must use union
because of constexpr
)". Therefore, the code above simply demonstrates an example, not a real implementation.
Alignment rules are important, as the standard says:
Template class optional [optional.optional]:
The contained value must be located in the memory region, appropriately aligned for typeT
For example:
// sizeof(double) = 8 // sizeof(int) = 4 std::optional<double> od; // sizeof = 16 bytes std::optional<int> oi; // sizeof = 8 bytes
While bool
usually takes one byte, the optional data type is forced to follow the alignment rules. Thus, the size of std::optional<T>
larger than sizeof(T) + 1
.
For example, if you have this type:
struct Range { std::optional<double> mMin; std::optional<double> mMax; };
It will take more space than if you used your type instead of std::optional
:
struct Range { bool mMinAvailable; bool mMaxAvailable; double mMin; double mMax; };
In the first case, the size of the structure is 32 bytes! In the second case, only 24.
Test case for Compiler Explorer .
The link is a great explanation about performance and memory usage, taken from the boost documentation: performance issues .
And in the article "Effective optional values" the author discusses how to write a wrapper for an optional type, which can be a little faster.
I wonder if there is a chance to use at least some magic of the compiler and reuse some space to put this additional object initialization flag inside an optional type. Then no additional space would be necessary.
std::optional<bool>
and std::optional<T*>
While you can use std::optional
for any type you want, you need to take special care when using an optional type with logical type and pointers.
std::optional<bool> ob
- what does it say? With this construct, you have a three-state boolean type. Therefore, if you really need it, it is probably better to use a real triple type - std :: tribool boost :: tribool ( edit : Antervis) .
Moreover, the use of this type can be confusing, because ob
converted to bool
if there is a value inside it and *ob
returns a stored value (if available).
A similar situation may occur with pointers:
// ! ! std::optional<int*> opi { new int(10) }; if (opi && *opi) { std::cout << **opi << std::endl; delete *opi; } if (opi) std::cout << "opi is still not empty!";
A pointer to an int
is actually a nullable
type, so wrapping it in an optional type will only complicate its use.
Whew! Yes, it was a lot of text about the optional type, but that's not all.
Nevertheless, we considered the main use, creation and operation of these with a convenient type. I believe that we have many cases where the optional type fits much better than using some predefined values to represent nullable
types.
I would like to remind the following things about the optional type:
std::optional
is a wrapper for expressing the nullable
type.std::optional
does not use dynamic memory allocation.std::optional
may contain a value or be empty.operator*()
, operator->()
, value()
, value_or()
to get the real value.std::optional
implicitly cast to bool
, so you can easily check if it contains any value or not.Source: https://habr.com/ru/post/372103/
All Articles