If std::optional
's value()
member function is called when the optional
has no actual value initialized, a std::bad_optional_access
is thrown. As it is derived directly from std::exception
, you need either catch (std::bad_optional_access const&)
or catch (std::exception const&)
for dealing with the exception. However, both options seem sad to me:
std::exception
catches every single exceptionstd::bad_optional_access
exposes implementation details. Consider the following example:Placement Item::get_placement() const {
// throws if the item cannot be equipped
return this->placement_optional.value();
}
void Unit::equip_item(Item acquisition) {
// lets the exception go further if it occurs
this->body[acquisition.get_placement()] = acquisition;
}
// somewhere far away:
try {
unit.equip_item(item);
} catch (std::bad_optional_access const& exception) { // what is this??
inform_client(exception.what());
}
So, to catch the exception you need to be well-informed about the usage of std::optional
in the Item
's implementation, being led to a list of already known issues. Neither I want to catch and rewrap std::bad_optional_access
because (for me) the key part of exceptions is the possibility of ignoring them until needed. This is how I see the right approach:
std::exception
<- std::logic_error
<- std::wrong_state (doesn't really exist)
<- std::bad_optional_access (isn't really here)
So, the "far away" example could be written like this:
try {
unit.equip_item(item);
} catch (std::wrong_state const& exception) { // no implementation details
inform_client(exception.what());
}
Finally,
std::bad_optional_access
designed like it is?Note: boost::bad_optional_access
derives from std::logic_error
. Nice!
Note 2: I know about catch (...)
and throwing objects of types other than std::exception
family. They were omitted for brevity (and sanity).
Update: unfortunately, I can't accept two answers, so: if you're interested in the topic, you can read Zuodian Hu's answer and their comments.
You say that the key appeal of exceptions is that you can ignore them for as deep of a call stack as you can. Presumably, given your ambition of avoiding to leak implementation details, you no longer can let an exception propagate beyond the point where that exception cannot be understood and fixed by its handler. That seems to be a contradiction with your ideal design: it punts fixing the exception to the user, but bad_optional_access::what
has exactly no context on what just happened–leaking implementation details to the user. How do you expect a user to take meaningful action against a failure to equip an item when all they see is, at best, "could not equip item: bad_optional_access"?
You have obviously made an over-simplification, but the challenge remains. Even with a "better" exception hierarchy, std::bad_optional_access
simply does not have enough context that anything beyond extremely close callers might know what to do with it.
There are several fairly distinct cases in which you might want to throw:
if
blocks.When you run into issues with exceptions not feeling right, it's usually because you're trying to handle an error meant for a different level than the one you wish it was operating at. Wishing for changes to the exception hierarchy is just trying to bring that exception in line for your specific use, which causes tensions with how other people use it.
Clearly, the C++ committee believes that bad_optional_access
belongs to the first category, and you're asking why it's not part of the third category. Instead of trying to ignore exceptions until you "need" to do something with them, I believe that you should flip the question around and ask yourself what is intended to catch the exception.
If the answer truly is "the user", then you should throw something that's not a bad_optional_access
and that instead has high-level features like localized error messages and enough data on it that inform_user
is able to bring up a dialog with a meaningful title, main text, subtext, buttons, an icon, etc.
If the answer is that this is a general game engine error and that it might happen in the regular course of the game, then you should throw something that says that equipping the item failed, not that there was a state error. It's more likely that you'll be able to recover from failing to equip an item than from having a non-descript state error, including if, down the road, you need to produce a pretty error for the user.
If the answer is that you might try to equip 25 items in a row and you want to stop as soon as something goes wrong, then you need no changes to bad_optional_access
.
Also note that different implementations make different uses more or less convenient. In most modern C++ implementations, there is no performance overhead on code paths that do not throw, and a huge overhead on paths that do throw. This often pushes against the use of exceptions for the first category of errors.