Motivation
Currently std::bad_variant_access::what() returns the literal "bad_variant_access" regardless of why the exception was thrown. This is significantly less informative than libstdc++, which has reported the failing API and failure mode (e.g. "std::get: wrong alternative for variant", "std::visit: variant is valueless"). I’d like to make libc++ better for this case.
Standard
Approach
Mirror libstdc++'s design: a const char* data member on bad_variant_access pointing at one of a small set of static string literals, selected at the throw site by an internal enum. Three reasons for now: wrong alternative in get, valueless in get, valueless in visit. Distinguishing visit from visit<R> is possible but requires threading a tag through __throw_if_valueless - skipped for now, but i’m ready to add it
Example implementation
I have working prototype which looks like that.
Exception
class _LIBCPP_EXPORTED_FROM_ABI bad_variant_access : public exception {
public:
_LIBCPP_CONSTEXPR_SINCE_CXX23 bad_variant_access() noexcept = default;
[[__nodiscard__]] const char* what() const _NOEXCEPT override;
# ifdef _LIBCPP_ABI_BAD_VARIANT_ACCESS_GOOD_WHAT_MESSAGE
private:
constexpr explicit bad_variant_access(const char* __msg) noexcept : __msg_(__msg) {}
const char* __msg_ = "bad_variant_access";
friend constexpr void __throw_bad_variant_access(__bad_variant_reason) {...}
# endif
};
__throw_bad_variant_access
enum class __bad_variant_reason : unsigned char {
__wrong_alternative,
__valueless_in_get,
__valueless_in_visit,
};
# ifdef _LIBCPP_ABI_BAD_VARIANT_ACCESS_GOOD_WHAT_MESSAGE
// Defined as in-class friend (see class above) so the body is constexpr-reachable
// and the message string is selected at the throw site.
friend constexpr void __throw_bad_variant_access(__bad_variant_reason __r) {
constexpr const char* __reasons[] = {
"std::get: wrong alternative for variant",
"std::get: variant is valueless",
"std::visit: variant is valueless",
};
# if _LIBCPP_HAS_EXCEPTIONS
throw bad_variant_access(__reasons[static_cast<unsigned char>(__r)]);
# else
_LIBCPP_VERBOSE_ABORT("%s", __reasons[static_cast<unsigned char>(__r)]);
# endif
}
# else
[[noreturn]] inline _LIBCPP_HIDE_FROM_ABI constexpr
void __throw_bad_variant_access([[maybe_unused]] __bad_variant_reason) {
# if _LIBCPP_HAS_EXCEPTIONS
throw bad_variant_access();
# else
_LIBCPP_VERBOSE_ABORT("bad_variant_access");
# endif
}
# endif
Throwers
// __generic_get
if (!std::__holds_alternative<_Ip>(__v)) {
if (__v.valueless_by_exception()) {
std::__throw_bad_variant_access(__bad_variant_reason::__valueless_in_get);
}
std::__throw_bad_variant_access(__bad_variant_reason::__wrong_alternative);
}
// __throw_if_valueless (called from visit)
if (__valueless) {
std::__throw_bad_variant_access(__bad_variant_reason::__valueless_in_visit);
}
ABI
The data member changes sizeof(bad_variant_access), so the new behavior is gated behind a new _LIBCPP_ABI_BAD_VARIANT_ACCESS_GOOD_WHAT_MESSAGE flag, following the existing pattern of _LIBCPP_ABI_BAD_FUNCTION_CALL_GOOD_WHAT_MESSAGE.
Additional details
- Same approach can be done for
bad_optional_accessandbad_expected_access- would follow the same pattern but with separate ABI flags. Intend to propose as a follow-up once this lands and the pattern is established. - I wanted to add template params to the message, for example, with
__PRETTY_FUNCTION__but it has strong side effects and i’d like to discuss it before.
Thank you. If changes are close to what we want to see in libc++ i’m ready to polish them, make comparison and open a PR.