According to Cppreference, optional::value() on std::nullopt will throw, while dereferencing an optional which is std::nullopt is UB.
I have tests, and seems the UB may not panic in some compilers, which is more horrible then just panic, so it is very harmful if mistakenly used. I am not saying UB is OK or meaningful, it is a bug. However, if it happens, a wrong result is harder to debug than a panic.
struct I {
int get() { return 1; }
};
void f() {
try {
std::optional<std::shared_ptr<I>> x = std::nullopt;
printf("%d\n", (*x)->get()); // -----> UB
}
catch (std::bad_optional_access & e) {
printf("it throws in f\n");
}
}
My question is why std::optional is designed to result in a UB rather than also throw an exception like value() does in operator*/operator->?
Because C++ does not make you pay for things that you do not need.
The check if std::optional holds a value has a cost. It's only a simple condition, and it's only one branch, but when you are certain that there is a value and when you need to access the value often, that condition and branch can be significant.
If you know for sure that the optional contains a value, there is no need to check if it contains a value.
// ...
std::optional<int> x = 42;
for (int i=0;i<100000; ++i) {
foo(*x);
}
It would be a waste of resources to check in every single iteration whether x does contain an int. Other times you want an exception being thrown and then you can use value().
The core reason is the Zero overhead principle which has underpinned the development and evolution of every C++ standard.
As stated in the link, this principle requires:
You (the developer) don't pay for what you don't use.
What you do use is just as efficient as what you could reasonably write by hand.
When it comes to evolution of the C++ standard library and features in it - including std::optional - this principle often has the effect of there being two ways of achieving some things
There is an "unsafe" option which, if misused, results in undefined behaviour but this, if used skillfully, costs less (e.g. less performance hit, less memory usage).
There is also a "safe" option that avoids undefined behaviour, but additional cost (e.g. additional runtime checks and throwing an exception if needed) in doing so.
The only times when a library feature does not provide both the "safe" and "unsafe" options is if "safety" comes with zero cost. In practice, when there is a cost, the C++ standard allows a developer the freedom to decide whether to code for maximum safety or for minimum cost.
In other words, the C++ standard requires the developer to deliberately make an engineering trade-off, rather than imposing any specific choice on the developer.
This is a different philosophy from some other programming languages that mandate "safety" (in various forms) but (almost) always force a developer to accept the overheads (e.g. resource usage, runtime performance).
In the context of this question,
std::optional::operator *() is the "unsafe" option that avoids the overhead (of checking and throwing an exception) but requires the developer accept and manage the potential impact of introducing undefined behaviour (e.g. somehow ensure, by other means, that the object is notstd::nullopt).
std::optional::value() is the "safe" option in which there is no undefined behaviour, but the implementation of std::optional::value() must explicitly do a runtime check in order to throw an exception on std::nullopt. The need to do a check, and the throwing of an exception, is the cost for avoiding undefined behaviour. A developer who elects to use this option must accept and/or manage the associated costs of safety.
As I said above, it is the developer's freedom (and, with freedom comes responsibility) to make the choice of approach they use. The C++ standard allows the choice, but does not impose one option or the other on the developer.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With