Nullable if
After getting somewhat used to treating std::optional as a range for easy conditional unwrapping, I started to think that the solution in use is less than optimal and somewhat misleading. For those unaware, a proposal extends std::optional to provide both begin and end, which makes it possible to write code like
auto getOpt(int value)
-> std::optional<int>
{
return (value < 5)? value: std::nullopt;
}
auto func(int value)
-> std::string
{
for (const auto value : getOpt(value)) {
return std::format("The value is {}"sv, value);
}
return "No value"s;
}
Hopefully this shows the gist: we check the optional and only unwrap it if holds a value all in one simple statement. However it is very unconventional and confusing at first. This also creates two semantically different for statements: loop over each element in the range, and conditionally unwrap this option-type.
The solution
What I would really want to write is a simple if statement, as that is what I’m really trying to do:
auto func(int value)
-> std::string
{
if (const auto value : getOpt(value)) {
return std::format("The value is {}"sv, value);
}
return "No value"s;
}
This is simple syntactic sugar for checking the optional and only if it holds a value, unwrapping it and entering the block. In a more pure form this would work as follows:
if ( <item-declaration> : <expression> )
<statement>
Would expand to
if ( auto&& /*nullable*/ = <expression>; /*nullable*/ ) {
<item-declaration> = * /*nullable*/ ;
<statement>
}
Which in turn expands to
{
auto&& /*nullable*/ = <expression> ;
if ( /*nullable*/ ) {
<item-declaration> = * /*nullable*/ ;
<statement>
}
}
This makes the new type of statement a syntactic sugar extension over existing functionality.
Why is it better?
The proposed solution is better than the alternatives currently(ish) available in a few ways. Firstly, there is less moving parts the programmer has access to, as the option-type is not necessarily accessible and we only have a reference to (or copy of) the value if it exists, which is much harder to misuse than the option-type variable itself. We need not use the dereference operator or arrow operator ourselves, making for much nicer code and less room for future changes to introduce undefined behaviour (calling * on an empty optional).
Normally we’re not interested in the option-type itself, but the value within or if there is no value. The option-type is just an implementation detail. One that allows us to invoke undefined behaviour, throw exceptions, and clutter our code. The nullable if cleans up a lot of it.
Secondly, this solution is a language extension, not a class extension, so it works on nullable types, which include pointers. Yes, this makes non-owning pointers less dangerous to use.
Thirdly, this allows for an else-branch, which the range-based solution does not. We could write
void func()
{
if (const auto value : getOpt()) {
std::println("The value is {}"sv, value);
}
else {
std::println("We got no value :("sv);
}
std::println("Next common thing"sv);
}
Which woudl not be possible without holding the optional in a named variable and manually checking if it holds value or not.
Fourthly, this allows for normal code that doesn’t need tricks to silence warnings. The for-loop variation doesn’t work well on MSVC, since the following snippet emits a warning:
auto func()
-> value
{
for (const auto value : getOpt()) {
return value;
}
return 0;
}
The warning is for unreachable code, since the increment at the end of the loop is never reached.
The simple workaround is to add a if (true)
to the return statement, but that is just more
clutter.
So the range optional is useless?
Not quite. For example iterating over a range of optionals can be done with a simple
std::ranges::join_view
instead of a std::ranges::filter_view
and std::ranges::transform_view
combo. There are certainly other uses, but this would make one of the more interesting use cases
redundant. However said use case is already somewhat abusing the range-for construct to avoid
making changes to the language. This idea instead changes language but not any type.