Well, they were, but not any more. Here is what I have realized since.
std::optional<T>
Is much more “normal” and usable of the two. It is actually very nice and useful single instance standard & compliant kind-of-a container. And most importantly
You do not need to keep the type of T to use std::optional
That is the primary confusion with optional. The value()
method and the std::bad_optional_access
lurking inside it.
1 2 3 4 5 6 7 8 9 10 |
// give_me_my_int int n; try { // opt is an optional<T> definition // coming from somewhere n = opt.value(); } catch(const std::bad_optional_access& e) { std::cout << e.what() << '\n'; } ctd::cout << n << '\n'; |
Seeing something like above, a lot of C++ students, have been wondering what is the real worth of using std:: optional, at all.
Let me try and explain step by step
First the basics.
1 2 3 4 5 6 7 8 9 10 11 |
// from now on we do not need std:: using namespace std ; // optional<T> is a template template <typename T> class optional ; // template is *not* a type // template instance // aka "template definition" *is* a type optional<int> ; // we can declare type alias // as we almost always do with template instances using optint_type = optional<int> ; |
In other words, we must have the complete type T, at the moment of ‘marriage, with std::optional<T>
:
We can use any type T as long as it is fully defined at the moment of usage.
The fact we have optional<T>
defined with some type T
, means there is a type T
, declared and defined “somewhere” in the same program, and crucially in the same scope. And it has to exist in the same scope as its user aka optional<T>
exists, at the moment of ‘marriage’ :
1 2 3 |
// definition site // compilation error, using non existent type using string_opt = optional<std::string>; |
to fix that we must introduce T into the scope:
1 2 3 4 5 |
// definition site #include <string> using namespace std ; // here we combine optional<T> and T using string_opt = optional<string>; |
For each template optional<T>
, If the compile-time environment has both T
and optional<T>
in the same scope at the site of definition, we will have no problems whatsoever. The same applies to the usage of the instance of the particular defined optional type.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
// create string_opt str_opt // give it some value str_opt = "Hello!"; // send it to some magical function far-far away // but in the same program // compiles, links and runs // *only* ! if both T and optional<T> // are in the same scope as the function template< typename OPT> void magical_function (OPT some_opt ) { // get to the value if (some_opt.has_value()) { // compiler replaces auto with // the actual type of the // value inside the optional<T> type auto opt_val = some_opt.value(); std::cout << opt_val ; } } // the usage std::optional<int> optint{23} ; int my_int = magical_function(optin) ; |
Above must compile and work as long as there is type T
in the same scope as there was when the optional<T>
was created.
And of course, with the auto
keyword, we do not have to know the type T
in advance. Which is a rather big deal. Without auto
one would need a separate version of magical_function
, for each type returned.
The tool
For what it’s worth, I will give you one simple but very useful little utility, I am using always with optional’s.
1 2 3 4 5 6 7 8 9 |
// (c) 2019-2021 by dbj.org -- free to use // https://godbolt.org/z/97ea5da1K template<typename T> inline T optival (std::optional<T> opt, T dflt_ = T{}) noexcept // daring { return opt.value_or(dflt_); } |
With the above, I do not have to worry about (and code around ) the issue of ’empty’ optional’s. Namely freshly minted optional will throw the exception if you ask it for a value and it has none yet. Behind that link, you will find an example of how to code around this. But who wants to retype this each time value is needed from optional.
In contrast to that, in my API, by design, the fallback value is default constructed type T. If the user does not provide one, that is.
Caveat Emptor (Let the buyers beware): This works (nicely) only for types with default constructors. Thus if no default constructor, the second argument must be provided by callers.
Other than that this is one very comfortable “minuscule” API one might say.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// https://godbolt.org/z/97ea5da1K // optional<int> op has come from somewhere // empty, into this scope // returns 0 as that is int{} auto v1 = optival(op) ; // returns user defined // fallback val 43 // as op is still empty auto v2 = optival(op, 43) ; op = 123 ; // returns 123, as op is not empty // and fallback value is ignored auto v2 = optival(op, 43) ; |
No exception is thrown. An especially welcome attribute in the embedded programming world. And now the troubled kid on the std block.
std::any
Quite a few authors have tried to articulate the reason for its existence. I personally can perhaps see its value in one and a half, situations.
First (the half) is where you have a limited number of types to pass internally using std::any. And this is how you code around getting the value from std::any :
1 2 3 4 5 6 7 8 9 10 11 12 |
// std::any a1 has come from somewhere into this scope // what is inside it? //let's try and find out // is it an int? if (auto ptr = std::any_cast<int>(&a1)) { // yes it is an int } else // is it a string? if (auto ptr = std::any_cast<std::string>(&a1)) { // yes it is a string } else { //what here? } |
That is ugly as hell. Not feasible for more than a handful of possible types your design will define. But then one must ask why not just using the union
. Sigh.
The second and last, and I think the only reasonable situation I can imagine is using std::any
for serialization/de-serialization. I.e for sending object’s “over a wire”, for example, through messaging middleware, for storing uniformly into the database and a such.
1 2 3 4 5 6 7 8 9 10 11 12 |
// application A std::any a_("Hello!"); // 123 is the key int key = 123 ; store_any_to_sqlite(key,a_) ; // application B // has been somehow given the key:123 std::any a_ ; read_any_from_sqlite(key,a_) ; // will contain "Hello!" auto rezult = std::any_cast<string>(a_); |
As you might imagine the above SQLite
scenario is far from simple software. And very few teams or individuals spend their afternoons trying to write persistent objects management, in C++, when there are few very mature libraries for that, some even decades old and in widespread use.
Assuming we are all convinced we found a use case that might spare std::any
from deprecation here is how I might use it.
I have to say, I do not use, at the moment. I simply use optional<T>
. But that is not a replacement?
Just any hack
Let’s dive in head first, worry later.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// https://godbolt.org/z/cb41vYKW9 template<typename T> auto make_any(T const & v_ = 0) { using optitype = std::optional< T >; return std::make_pair( [ ](std::any any_) -> optitype { if (!any_.has_value()) return std::nullopt; return optitype{ std::any_cast<T>(any_) }; }, std::any{ v_ } ); } |
The intent: Have one factory method that returns a std::pair
. First of the pair is the lambda function that can extract the value for the given type. The second of the pair is the instance of std::any.
Usage is simple:
1 2 3 |
// https://godbolt.org/z/cb41vYKW9 auto[get_, any_] = make_any(324); assert( 324 == get_(any_) ) ; |
The above call makes a pair that contains std::any that contains integer 324. And a lambda that can extract from that instance of std::any (the any_
) with no problems whatsoever.
1 2 3 4 5 6 |
// https://godbolt.org/z/cb41vYKW9 // lambda returns optional<int> // as 324 is int auto optnl = get_(any_); // we use here our new friend 'optival' assert( 324 == optival( optnl )); |
As we can see above the “magical lambda” returns optional. Why not. As std::any instance can be empty we use the optional to signal that result is empty. That is it equals std::nullopt
.
That certainly works, as long as you keep that pair together, but then somewhat, it defeats the purpose of having std::any in the first place. In this scenario, you have the instance of std::any and you have the lambda to safely get its value out.
You decide.