Checking if an optional is engaged and then invoking code over it is the whole point of an optional. Doing it _safely_ without being exceedingly easy to misuse is also a very key functionality, something which today requires a good static analyzer to use.
This is asking for essential limited pattern matching in pure-library form for optional. Pattern matching being something just about everyone wants; see
https://github.com/solodon4/Mach7 for instance.
The interface in C++17 is unsafe. It goes against the many lessons learned in the languages that pioneered the very concept of optionals/variants/algebraic-types. With C++ optional, you can just *opt and get a thrown exception; now we have Java's NullPointerException ported into C++, essentially.
Better use of an optional _mandates_ the use of conditional unpacking. Traditionally, this was done with a full-weight pattern matching system:
// painfully verbose... but always safe, guaranteed (invented syntax, of course)
match (opt) {
case some(&val): use(val);
case none: ignore_or_something();
}
More modern improvements allow this to be done in a simpler conditional statement:
// short and simple... not possible in C++ as a library
if (auto& val = some(opt))
use(val);
C++ doesn't have support for that in the language directly; the above would check the truth-iness of the unpacked value, not whether the value can be unpacked in the first place. The only ways to emulate it would be as unsafe as optional is today (in fact would just _be_ what optional is today). What we do have in C++ that can be used to emulate this, however, is the for-range statement:
// semantic abuse of for-range, but it works in C++ today with no language changes,
// and doesn't suffer the problems of lambdas ... but it can't support an else clause
for (auto& val : opt)
use(val);
// negate the check - none returns a one-element range of std::ignore or something if the optional is disengaged
for (auto : none(opt))
ignore_or_something();
We could also use lambdas/functions as you suggested, but that breaks in some fun ways; namely, it disables the use of return/break from the body. The best you can do would be a signature like the below. It's also just a pain to use reliably in loops, meaning you have to resort to _really_ ugly uses of find_if instead.
template <class A, class U, class V>
common_type_t<U, V> match(optional<A> value, function<U(A)> engaged, function<V()> disengaged);
// usable in the tail position of a function only
return match(val,
[](T& val){ return use(val); },
[]{ return ignore_or_something(); });
// this won't actually work...
for (auto& opt : optionals_container)
match(opt,
[](T& val){
if (condition(val))
return val; // oops, this isn't useful, it "continues" in the loop and doesn't return/break
}, []{ something_else(); });
// find_if suffers from the same general problem: you are able to use the return value incorrectly/unsafely
auto it = std::find_if(optionals_container.begin(), optionals_container.end(), [](optional<T>& opt){
if (opt && conditional(*opt))
return true;
else {
something_else();
return false;
}
});
auto& val = **opt; // what if it == end()? are you _sure_ the returned value is an engaged optional? the compiler can't help check your assumption
return val;
Now, all that said, I don't in my heart _like_ the idea of abusing the for-range statement for this purpose. But, as a library-only implementation, it's really the best option *ahem* that we have right now. I'd prefer real pattern matching, but the specifics aside, the claim that we don't need this or that it's an uncommon need is mistaken.
On a related note, the lack of a ::map method or its like on optional is also a huge shortcoming. Such a method would actually help remove a ton of the uses of the for-range usage discussed here:
// val becomes optional<B>, and is engaged iff opt was engaged
auto val = opt.map([](auto& v){ return B(v); });
// could be even simpler if we had a std::construct which I'll include below
auto val = opt.map(construct<B>);
std::construct<T>
template <class T>
struct constructor_impl {
template <class... Args>
constexpr T operator()(Args&&... args) const { return T(std::forward<Args>(args)...); }
};
template <class T>
constexpr auto construct = constructor_impl<T>{};