[RFC] Improve bad_variant_access::what() messages

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

  1. Same approach can be done for bad_optional_access and bad_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.
  2. 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.

cc @ldionne @mordante

I’m not really convinced that a slightly improved what() message is worth the complexity of multiple layouts. If we want to improve the message we can also simply create an internal class derived from bad_variant_access and throw that instead. That would result in some availability shenanigans, but at least that will be removed at some point. Re. __throw_bad_variant_access I don’t really understand why you don’t just want to give it a const char* argument. That seems significantly simpler than your current proposal.

Thanks for the response — agree on both points.

To confirm your idea, you’re suggesting something like:

class __bad_variant_access_valueless_in_visit : public bad_variant_access {
public:
    
    _LIBCPP_CONSTEXPR_SINCE_CXX26 const char* what() const _NOEXCEPT override {
        return "std::visit: variant is valueless";
    }
};

Two questions I have:

  1. C++26 requires what() to be constexpr. Do you want that addressed in this
    PR, or kept as a separate change? It affects whether the override on
    bad_variant_access itself moves inline.

  2. I’m planning header-only with _LIBCPP_HIDE_FROM_ABI on these internal types
    rather than exporting from the dylib — they’re never named by user code, so
    the dylib export and availability annotations don’t seem to buy anything.

Definitely not. That’s quite complex to achieve - possibly unimplementable at the moment for libc++.

They do. If we only defined the type in the headers we’d generate vtables and RTTI in every TU that uses them.

I’ probably also go with a const char* (or maybe __libcpp_refstring) instead of a special class for every message.

Okay. Got it.

Is that cost significant? If so, we can go with

class _LIBCPP_EXPORTED_FROM_ABI _LIBCPP_AVAILABILITY_BAD_VARIANT_ACCESS_MSG
__bad_variant_access_with_msg : public bad_variant_access {
    const char* __msg_;
public:
    _LIBCPP_HIDE_FROM_ABI explicit __bad_variant_access_with_msg(const char* __m) noexcept
        : __msg_(__m) {}
    const char* what() const noexcept override;  // out-of-line in variant.cpp
};

And throw with throw __bad_variant_access_with_message("std::visit: variant is valueless");
I would not use refstring here. It is non const and i don’t see any benefit of using it.

PR has been opened.
Scope of changes and approach are discussed above.