folly-clib-20260203.1245: folly/folly/result/rich_exception_ptr.h
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include <folly/Portability.h> // FOLLY_HAS_RESULT
#include <folly/lang/Exception.h>
#include <folly/result/detail/rich_exception_ptr_storage.h>
#include <folly/result/rich_error_base.h>
#if FOLLY_HAS_RESULT
namespace folly {
struct OperationCancelled;
template <typename, typename, auto...>
class immortal_rich_error_t;
namespace detail {
// These are in `detail` since they're opt-build fallbacks for a debug-fatal.
struct bad_result_access_error : public std::exception {};
struct empty_result_error : public std::exception {};
// Stub types that will be replaced by integrations with other folly/ types.
//
// Will be replaced by real types from OperationCancelled.h, which will both
// derive from `OperationCancelled`.
struct StoppedNoThrow {}; // NOT an `std::exception`
struct StoppedMayThrow : std::exception {
const char* what() const noexcept override {
return "operation stopped (cancelled)";
}
};
// Will be replaced by the real types that are currently in Try.h
struct StubUsingUninitializedTry : std::exception {};
struct StubTryException : std::exception {};
// Free function to get a singleton exception_ptr for bad_result_access
const std::exception_ptr& bad_result_access_singleton();
template <typename T>
using detect_folly_detail_base_of_rich_error =
typename T::folly_detail_base_of_rich_error;
// Tag types for non-public `rich_exception_ptr` interfaces
struct force_slow_rtti_t {};
struct try_rich_exception_ptr_private_t {};
// Passkey for `rich_exception_ptr::format_to` optimization.
// Lets `rich_error_base.cpp` skip a redundant maybe-RTTI "is rich?" check.
class format_to_skip_rich_t {
friend class ::folly::rich_error_base;
friend class ::folly::rich_exception_ptr;
bool skip_;
explicit format_to_skip_rich_t(bool skip) : skip_(skip) {}
public:
format_to_skip_rich_t() : skip_(false) {}
};
template <typename Derived, typename B>
// `protected` inheritance keeps the members of the storage classes as
// implementation details, while allowing the derived `rich_exception_ptr`
// (which is `final`) to access them for out-of-line member functions.
class rich_exception_ptr_impl : protected B {
protected:
// Make unused code dead, while verifying that small-value code paths compile.
static constexpr bool kImplementsSmallValue = false;
constexpr const detail::immortal_exception_storage* get_immortal_storage()
const {
if (!std::is_constant_evaluated()) {
B::debug_assert("get_immortal_storage", !B::get_bits());
}
return B::get_immortal_storage_or_punned_uintptr();
}
private:
// This allows comparing REPs with different storage. Only tests need it,
// since they explicitly cover both "separate" and "packed" storage, whereas
// `underlying_ptr()` always uses the default storage.
template <typename, typename>
friend class rich_exception_ptr_impl;
using typename B::bits_t;
constexpr void set_immortal_storage_and_bits(
const detail::immortal_exception_storage* p, typename B::bits_t bits) {
B::apply_bits_after_setting_data_with(
[p](auto& d) { d.immortal_storage_ = p; }, bits);
}
// Future: Could implement this overload via `folly::copy()`, if you find
// that `extract_exception_ptr` always optimizes away.
void assign_owned_eptr_and_bits(
const std::exception_ptr& ep, typename B::bits_t bits) {
B::apply_bits_after_setting_data_with(
[&ep](auto& d) {
new (&d.eptr_ref_guard_.ref()) std::exception_ptr{ep};
},
bits);
}
void assign_owned_eptr_and_bits(
std::exception_ptr&& ep, typename B::bits_t bits) {
B::apply_bits_after_setting_data_with(
[&ep](auto& d) {
new (&d.eptr_ref_guard_.ref())
std::exception_ptr{detail::extract_exception_ptr(std::move(ep))};
},
bits);
}
template <typename Ex>
consteval static void assert_operation_cancelled_queries() {
static_assert(
!std::is_same_v<const Ex, const StoppedNoThrow> &&
!std::is_same_v<const Ex, const StoppedMayThrow>,
"User code may only test for `OperationCancelled`; its derived classes "
"are private implementation details.");
}
template <typename MaybeConstEx>
MaybeConstEx* get_exception_from_nothrow_oc() const {
B::debug_assert(
"get_exception_from_nothrow_oc",
B::NOTHROW_OPERATION_CANCELLED_eq == B::get_bits());
const auto& epg = B::get_eptr_ref_guard();
// Since `assert_operation_cancelled_queries` forbids testing for
// the private derived classes of OC, we don't need `derived_from`.
if constexpr (std::
is_same_v<const MaybeConstEx, const OperationCancelled>) {
return static_cast<MaybeConstEx*>(exception_ptr_get_object(epg.ref()));
}
return nullptr; // Stored OC, but queried for other type.
}
template <typename MaybeConstEx>
MaybeConstEx* get_exception_from_owned_eptr() const {
B::debug_assert(
"get_exception_from_owned_eptr",
B::OWNS_EXCEPTION_PTR_and & B::get_bits());
const auto& epg = B::get_eptr_ref_guard();
const auto& ep = epg.ref();
const auto fast_path_bits = B::mask_FAST_PATH_TYPES & B::get_bits();
// Fast path for `result::has_stopped()`. This is exhaustive since
// `OperationCancelled` is the base for the only "official" cancellation
// signals -- user derived classes are forbidden via
// `assert_operation_cancelled_queries()`.
if constexpr (std::
is_same_v<const MaybeConstEx, const OperationCancelled>) {
if (fast_path_bits == B::IS_OPERATION_CANCELLED_masked_eq) {
return static_cast<MaybeConstEx*>(exception_ptr_get_object(ep));
} else if (fast_path_bits != B::owned_eptr_UNKNOWN_TYPE_masked_eq) {
return nullptr; // Known type, but not `MaybeConstEx`
}
// ... fall through
} else if constexpr (
std::is_same_v<const MaybeConstEx, const rich_error_base>) {
// Fast path for `rich_error_base`. We currently don't try to accelerate
// lookup of its derived classes, except for the "miss" case below.
//
// Future: See if knowing that the pointed-to type derives from
// `rich_error_base` gives a faster `dynamic_cast` than our fallback.
if (fast_path_bits == B::IS_RICH_ERROR_BASE_masked_eq) {
// The owned eptr ctor checks `rich_error_base` has offset 0
return static_cast<MaybeConstEx*>(exception_ptr_get_object(ep));
} else if (fast_path_bits != B::owned_eptr_UNKNOWN_TYPE_masked_eq) {
return nullptr; // Known type, but not `MaybeConstEx`
}
// ... fall through
}
if constexpr (std::derived_from<MaybeConstEx, rich_error_base>) {
if (fast_path_bits != B::IS_RICH_ERROR_BASE_masked_eq &&
fast_path_bits != B::owned_eptr_UNKNOWN_TYPE_masked_eq) {
return nullptr;
}
}
return exception_ptr_get_object_hint<MaybeConstEx>(ep);
}
static constexpr auto with_underlying_impl(auto* me, auto fn)
-> decltype(fn(me));
// Calls `fn` with the underlying exception, bypassing any stack of epitaph
// wrappers that `this` might represent, and returns the result.
//
// Note that `fn` must be generic, ready to accept either of:
// - The `rich_exception_ptr_impl<...>` sliced from `rich_exception_ptr`
// (default params!) returned by `rich_error_base::underlying_error()`, or
// - ... this `rich_exception_ptr_impl*`, if that's a different type.
constexpr decltype(auto) with_underlying(auto fn) const {
return with_underlying_impl(this, fn);
}
constexpr decltype(auto) with_underlying(auto fn) {
return with_underlying_impl(this, fn);
}
// Pre-condition: `rich_exception_ptr` is in any valid state.
//
// Post-condition: Any stored owned object (i.e. eptr) is destructed, and
// all member vars of the object are in an undefined state. To reuse it,
// they must be re-set as-if constructing it from scratch.
constexpr void destroy_owned_state_and_bits_caller_must_reset() {
if (B::OWNS_EXCEPTION_PTR_and & B::get_bits()) {
B::get_eptr_ref_guard().ref().~exception_ptr();
}
}
// IMPORTANT: Must not read `this`, it may be after-dtor, or before-ctor
constexpr void copy_unowned_pointer_sized_state(
const rich_exception_ptr_impl& other) {
if (!std::is_constant_evaluated()) {
// NB: Technically, this works for !B::bits_store_eptr(B::get_bits())),
// but we expect to use it in a subset of those cases.
B::debug_assert(
"copy_unowned_pointer_sized_state",
B::IS_IMMORTAL_RICH_ERROR_OR_EMPTY_eq == other.get_bits() ||
B::SIGIL_eq == other.get_bits() ||
// We use this code path to copy / move small values uintptrs,
// the copy/move ctors must be called by whatever contains this.
B::SMALL_VALUE_eq == other.get_bits());
}
// Here, we might have an immortal error, or a sigil, a small value, etc.
// All of these require copying "bits + uintptr". One way to achieve this:
// set_immortal_storage_and_bits(
// other.get_immortal_storage_or_punned_uintptr(), other.get_bits());
// However, that debug-asserts the first arg has clean low bits, which
// would only be true for immortals, and not for the other cases. Rather
// than conditionally relax the assertion, or add other branches, let's
// just copy all bits of the storage state. This is a bit wasteful on
// Windows, where eptr is 2 pointers, but it's the simplest code for now.
//
// Copy bit state storage, bypassing `rich_exception_ptr_impl::operator=`
B::operator=(other);
}
// IMPORTANT: Must not read `this`, it may be after-dtor, or before-ctor
constexpr void copy_from_impl(const rich_exception_ptr_impl& other) {
if (B::OWNS_EXCEPTION_PTR_and & other.get_bits()) {
assign_owned_eptr_and_bits(
other.get_eptr_ref_guard().ref(), other.get_bits());
} else if (B::NOTHROW_OPERATION_CANCELLED_eq == other.get_bits()) {
// Copy bit state storage, bypassing `rich_exception_ptr_impl::operator=`.
// DO NOT run the `std::exception_ptr` copy ctor; immortal OC is unowned.
B::operator=(other);
} else {
// Immortal RE; empty eptr; sigil uintptr; small value uintptr
copy_unowned_pointer_sized_state(other);
}
}
constexpr void make_moved_out(bits_t bits) {
// When `rich_exception_ptr` has an exception, the right moved-out state is
// "empty eptr".
if ((B::OWNS_EXCEPTION_PTR_and & bits) ||
B::NOTHROW_OPERATION_CANCELLED_eq == bits ||
B::IS_IMMORTAL_RICH_ERROR_OR_EMPTY_eq == bits) {
set_immortal_storage_and_bits(
nullptr, B::IS_IMMORTAL_RICH_ERROR_OR_EMPTY_eq);
}
// Else: `this` is unchanged, since these pre-move states match post-move
// states: empty eptr; sigil uintptr; small value uintptr.
}
// IMPORTANT: Must not read `this`, it may be after-dtor, or before-ctor
constexpr void move_from_impl(rich_exception_ptr_impl&& other) noexcept {
auto other_bits = other.get_bits();
if (B::OWNS_EXCEPTION_PTR_and & other_bits) {
assign_owned_eptr_and_bits(
std::move(other.get_eptr_ref_guard().ref()), other_bits);
} else if (B::NOTHROW_OPERATION_CANCELLED_eq == other_bits) {
// Copy bit state storage, bypassing `rich_exception_ptr_impl::operator=`.
// DO NOT run the `std::exception_ptr` move ctor; immortal OC is unowned.
B::operator=(other);
} else {
// Immortal RE, empty eptr, sigil uintptr, small value uintptr
copy_unowned_pointer_sized_state(other);
}
other.make_moved_out(other_bits);
}
// Must call via `with_underlying()` -- code that rethrows may target `catch`,
// so the only right behavior is to drop epitaph wrappers.
template <bool PartOfTryImpl> // `true` only when called from `Try.h`.
[[noreturn]] void throw_exception_impl() const {
bits_t bits = B::get_bits();
if (bits_t::OWNS_EXCEPTION_PTR_and & bits) {
const auto& epg = B::get_eptr_ref_guard();
const auto& ep = epg.ref();
if (ep) {
std::rethrow_exception(ep);
}
// Empty eptr, fall through to terminate... Should not be reached --
// `from_exception_ptr_slow` rewrites empty eptrs as immortal `nullptr`s.
B::debug_assert("Bug: empty owned eptr stored inside a REP", false);
} else if (B::IS_IMMORTAL_RICH_ERROR_OR_EMPTY_eq == bits) {
if (auto* immortal_storage = get_immortal_storage()) {
immortal_storage->throw_exception();
compiler_may_unsafely_assume_unreachable(); // required for [[noreturn]]
}
// Empty eptr, fall through to terminate...
} else if (bits_t::NOTHROW_OPERATION_CANCELLED_eq == bits) {
// Don't bother with the stored eptr since re-throwing does not
// preserve object identity.
// NOLINTNEXTLINE(facebook-hte-ThrowNonStdExceptionIssue)
throw StoppedNoThrow{};
} else if (B::SIGIL_eq == bits) {
// All sigil states represent non-error conditions (empty Try, has value).
// Calling throw_exception on these is a bug.
if (PartOfTryImpl) {
B::debug_assert(
"Try has unexpected sigil (not EMPTY_TRY)",
has_sigil<private_rich_exception_ptr_sigil::EMPTY_TRY>());
throw StubUsingUninitializedTry{};
} else {
B::debug_assert("Cannot `throw_exception` in sigil state", false);
throw bad_result_access_error{};
}
} else if (kImplementsSmallValue && B::SMALL_VALUE_eq == bits) {
if constexpr (PartOfTryImpl) {
throw StubTryException{}; // Match `Try::exception()` behavior
} else { // Match `result::error_or_stopped()` behavior
B::debug_assert("Cannot `throw_exception` in value state", false);
throw bad_result_access_error{};
}
}
// Else: the above bit tests are intended to exhaustively cover all allowed
// states, so we only fall through to terminate when:
// - Our bit state is invalid.
// - `this` contains an empty eptr. Per the standard, rethrowing one is
// UB, and our current termination behavior matches `exception_wrapper`.
// Future: maybe relax these to debug-fatal/opt-throw, like `result` does.
B::terminate_on_empty_or_invalid_eptr(bits);
}
// Call ONLY after handling owned-eptr in `to_exception_ptr_...()`
template <bool PartOfTryImpl> // `true` only when called from `Try.h`.
std::exception_ptr to_exception_ptr_non_owned(bits_t bits) const {
if (B::IS_IMMORTAL_RICH_ERROR_OR_EMPTY_eq == bits) {
const auto* s = get_immortal_storage();
return s ? s->to_exception_ptr() : std::exception_ptr{};
}
if constexpr (PartOfTryImpl) {
if (B::SIGIL_eq == bits) {
B::debug_assert(
"Try has unexpected sigil (not EMPTY_TRY)",
has_sigil<private_rich_exception_ptr_sigil::EMPTY_TRY>());
throw StubUsingUninitializedTry{};
} else if (kImplementsSmallValue && B::SMALL_VALUE_eq == bits) {
throw StubTryException{}; // Match `Try::exception()` behavior
} // Else: fall through...
}
// Sigil & small value match `result::error_or_stopped()` failure behavior
B::debug_assert(
"Cannot use `to_exception_ptr_slow` in sigil or small-value state",
false);
return bad_result_access_singleton();
}
template <bool PartOfTryImpl> // `true` only when called from `Try.h`.
std::exception_ptr to_exception_ptr_copy() const {
// Code that examines `exception_ptr` may rethrow or use legacy APIs, so we
// have to drop epitaph wrappers.
return with_underlying([](auto* rep) {
auto bits = rep->get_bits();
if ((B::OWNS_EXCEPTION_PTR_and & bits) ||
// Future: It's quite silly that this copy op, and its eventual dtor,
// both contend on the atomic refcount for the singleton... when a
// leaky singleton doesn't even need refcounts. See the
// `get_outer_exception` doc for a bithacking idea to fix this.
(B::NOTHROW_OPERATION_CANCELLED_eq == bits)) {
return rep->get_eptr_ref_guard().ref();
}
return rep->template to_exception_ptr_non_owned<PartOfTryImpl>(bits);
});
}
template <bool PartOfTryImpl> // `true` only when called from `Try.h`.
std::exception_ptr to_exception_ptr_move() {
// Similar logic to `to_exception_ptr_copy()`, but we are moving out the
// inner exception -- so that needs to be "moved out". Then, any outer
// exceptions need to be destroyed.
std::exception_ptr eptr;
bits_t outer_bits = B::get_bits();
using bits_t = typename B::bits_t; // MSVC thinks `B::BIT_NAME` is private
bool this_is_wrapper = with_underlying([&eptr](auto* rep) {
bits_t bits = rep->get_bits();
// `with_underlying` makes `rep` a pointer-to-`const` iff `this` is not
// an underlying error, or equivalently if `rep` is not an outer error.
constexpr bool rep_is_not_outer =
std::is_const_v<std::remove_pointer_t<decltype(rep)>>;
if (bits_t::OWNS_EXCEPTION_PTR_and & bits) {
if constexpr (rep_is_not_outer) {
// When `rep` is not `this`, it comes from `with_underlying`, which
// (by design!) points to `const`. So, the `else` branch shouldn't
// compile (again, by design), but more importantly, it would violate
// the principle that epitaph wrappers are transparent. The doc
// of `rich_error_base::mutable_underlying_error` speaks to this.
// Concretely:
// rich_exception_ptr rep1 = /* epitaph-wrapped MyErr */;
// auto rep2 = rep1; // Both share the same `epitaph_non_value`!
// This would mutate `epitaph_non_value::next_` for *both*.
// auto eptr2 = std::move(rep2).to_exception_ptr_slow();
// So `rep1` would now be a wrapper around a moved-out (empty) REP.
eptr = rep->get_eptr_ref_guard().ref();
} else { // If `rep` is `this`, then we can safely move it.
eptr = detail::extract_exception_ptr(
std::move(rep->get_eptr_ref_guard().ref()));
}
} else if (bits_t::NOTHROW_OPERATION_CANCELLED_eq == bits) {
// For nothrow OC, the stored eptr is a non-owning copy of a leaky
// singleton. We must copy (not move/extract) to properly increment
// refcounts, matching the behavior of `to_exception_ptr_copy()`.
//
// Future: See `to_exception_ptr_copy` for an optimization idea.
eptr = rep->get_eptr_ref_guard().ref();
} else {
eptr = rep->template to_exception_ptr_non_owned<PartOfTryImpl>(bits);
}
return rep_is_not_outer;
});
if (this_is_wrapper) {
// We just moved out the underlying error, but the wrapper chain still
// needs to be destroyed, or `make_moved_out` would leak it.
destroy_owned_state_and_bits_caller_must_reset();
}
make_moved_out(outer_bits);
return detail::extract_exception_ptr(std::move(eptr));
}
template <typename D1, typename B1, typename D2, typename B2>
static constexpr bool immortal_compares_equal(
// `B1` & `B2` may differ, see `operator==` and `compare_equal`
const rich_exception_ptr_impl<D1, B1>* immortal,
const rich_exception_ptr_impl<D2, B2>* other,
bits_t other_bits) {
if (B::IS_IMMORTAL_RICH_ERROR_OR_EMPTY_eq == other_bits) {
// Why aren't we comparing `get_immortal_storage()->immortal_ptr_`?
// (1) We only expect immortal `rich_exception_ptr`s to be created via
// `immortal_rich_error`, so it is fine to assume that each immortal
// exception has a unique `get_immortal_storage()`.
// (2) `get_immortal_storage()` is null for `rich_exception_ptr` empty.
return immortal->get_immortal_storage() == other->get_immortal_storage();
} else if (B::OWNS_EXCEPTION_PTR_and & other_bits) {
if (auto* immortal_storage = immortal->get_immortal_storage()) {
// An immortal eptr can only compare equal to an owned one if the latter
// is a copy of the immortal's mutable singleton. We don't want to
// eagerly create that singleton here, so instead check whether it has
// already been created.
//
// The right memory ordering here is TBD, but its impact is low, since
// it would only save a few cycles of "acquire" on ARM (acquire loads
// are free on x86). For now, we go with (2), the more conservative of
// two conflicting points of view:
//
// (1) We may test for "is created" with a relaxed load of the
// underlying `eptr_`, and only load the ref to the value when it
// is known to be there.
//
// (2) Must use "acquire" ordering for happens-before synchronization
// with the store-release in the singleton's `ensure_created()`.
// On weaker memory models (e.g. ARM), using "relaxed" here could
// read a stale `nullptr` after `other` was copied from the same
// singleton, causing us to incorrectly return `false`.
if (auto* mutable_eptr =
immortal_storage
->acquire_mutable_singleton_ptr_if_already_created()) {
return *mutable_eptr == other->get_eptr_ref_guard().ref();
}
return false;
} else { // `immortal` is empty
return !other->get_eptr_ref_guard().ref();
}
}
B::debug_assert(
"operator== / immortal exhaustiveness check",
// These states aren't errors
B::SMALL_VALUE_eq == other_bits || B::SIGIL_eq == other_bits ||
// Has eptr, but will never equal an immortal rich eptr, since
// nothrow OC uses its own separate singleton.
B::NOTHROW_OPERATION_CANCELLED_eq == other_bits);
return false;
}
// Must call via `with_underlying()` -- we want to compare the innermost
// exception, ignoring epitaph wrappers.
template <typename D1, typename B1, typename D2, typename B2>
static constexpr bool compare_equal(
const rich_exception_ptr_impl<D1, B1>* lp,
// The doc of `operator==` explains the argument asymmetry.
const rich_exception_ptr_impl<D2, B2>* rp) {
const auto lbits = lp->get_bits(), rbits = rp->get_bits();
if (B::IS_IMMORTAL_RICH_ERROR_OR_EMPTY_eq == lbits) {
return immortal_compares_equal(lp, rp, rbits);
} else if (B::IS_IMMORTAL_RICH_ERROR_OR_EMPTY_eq == rbits) {
// Future: Check if it's worth manually pruning the "immmortal
// other_bits" branch within this call (it was covered above).
return immortal_compares_equal(rp, lp, lbits);
} else if (
((B::OWNS_EXCEPTION_PTR_and & lbits) ||
B::NOTHROW_OPERATION_CANCELLED_eq == lbits) &&
((B::OWNS_EXCEPTION_PTR_and & rbits) ||
B::NOTHROW_OPERATION_CANCELLED_eq == rbits)) {
return lp->get_eptr_ref_guard().ref() == rp->get_eptr_ref_guard().ref();
} else if (
kImplementsSmallValue && B::SMALL_VALUE_eq == lbits &&
B::SMALL_VALUE_eq == rbits) {
B::debug_assert(
"rich_exception_ptr::operator== invalid for 2 small value uintptrs",
false);
// The fallback could trivally compare `get_uintptr()`, to return `true`
// correctly sometimes, but the the right comparison semantics must
// actually be determined by `result<T>` that knows the small-value type.
return false;
}
// Cases now ruled out:
// - Either side is an immortal rich error.
// - Both sides use eptr_ref_guard (either owned or nothrow OC).
// - Both sides hold small value uintptrs.
//
// Those covered all comparisons that can go "across" bit states, namely
// comparing the various kinds of errors. All the remaining heterogeneous
// comparisons are false:
// small value uintptr VS eptr_ref_guard
// sigil VS eptr_ref_guard
// sigil VS small value uintptr
//
// This means that the only possible true comparison is "sigil VS sigil".
//
// NB: Here, we assume that we're part of the `Try` impl if the bits are
// `SIGIL_eq` -- this is done to avoid templating the container on "is it
// in `Try`?" to make it easier to interconvert `Try` and `result`.
if (B::SIGIL_eq == lbits && B::SIGIL_eq == rbits) {
// Both are sigils; compare the actual sigil values
return lp->get_uintptr() == rp->get_uintptr();
}
B::debug_assert("operator== exhaustiveness", lbits != rbits);
return false;
}
protected:
// Implementation for `rich_exception_ptr::from_exception_ptr_slow`.
//
// Future: This sets to "unknown type" to avoid doing RTTI eagerly. In
// theory, our mutable getters could update `bits_` opportunistically.
rich_exception_ptr_impl(force_slow_rtti_t, std::exception_ptr&& e) {
assign_owned_eptr_and_bits(
std::move(e),
static_cast<bits_t>(
B::OWNS_EXCEPTION_PTR_and | B::owned_eptr_UNKNOWN_TYPE_masked_eq));
}
template <typename, typename, auto...>
friend class folly::immortal_rich_error_t;
// Used only by `immortal_rich_error`. IMPORTANT: In any new use, address
// the pointer slicing risk flagged in the `IS_RICH_ERROR_BASE_...` docblock.
// `consteval` guarantees `p` is immortal.
template <typename Ex>
consteval rich_exception_ptr_impl(
const Ex* ex, const detail::immortal_exception_storage* p) {
set_immortal_storage_and_bits(p, B::IS_IMMORTAL_RICH_ERROR_OR_EMPTY_eq);
// Future: This constraint is to allow packing `rich_exception_ptr::bits_`
// into the unused bits of the pointer. Once `constexpr` allows some
// pointer bit twiddling, we can trivially support constexpr
// `OperationCancelled`, as well as non-`rich_error_base` exception types.
static_assert( // Has manual test
std::derived_from<Ex, rich_error_base>,
"In C++20, `rich_exception_ptr` can only support immortal errors via "
"`immortal_rich_error`.");
// Redundant with `rich_error.h` asserts, so no manual test for this.
static_assert(detail::has_offset0_base<Ex, rich_error_base>);
if (ex->underlying_error()) {
// This simplifying assumption is good for perf -- this means that
// `with_underlying()` et al can test for "immortal rich error" and skip
// querying `underlying_error()` which would otherwise require an extra
// non-local load. If this became needed, the load could be made local
// by denormalizing `underlying_error()` into `get_immortal_storage()`.
// NOLINTNEXTLINE(facebook-hte-ThrowNonStdExceptionIssue)
throw "For now, immortal errors don't support `underlying_error()`";
}
}
public:
constexpr ~rich_exception_ptr_impl() {
destroy_owned_state_and_bits_caller_must_reset();
}
/// The default-constructed state is like an empty `std::exception_ptr`
constexpr rich_exception_ptr_impl() {
set_immortal_storage_and_bits(
nullptr, B::IS_IMMORTAL_RICH_ERROR_OR_EMPTY_eq);
}
/// Construct in a sigil state. See `private_rich_exception_ptr_sigil`.
template <private_rich_exception_ptr_sigil S>
explicit rich_exception_ptr_impl(vtag_t<S>) {
B::apply_bits_after_setting_data_with(
[](auto& d) { d.uintptr_ = static_cast<uintptr_t>(S); }, B::SIGIL_eq);
}
/// Check for a specific sigil state. See `private_rich_exception_ptr_sigil`.
template <private_rich_exception_ptr_sigil S>
[[nodiscard]] constexpr bool has_sigil() const noexcept {
return B::SIGIL_eq == B::get_bits() &&
B::get_uintptr() == static_cast<uintptr_t>(S);
}
/// This constructor makes an "owning" `rich_exception_ptr`.
///
/// Future: When needed, in-place construct from a callable, via either of:
/// rich_exception_ptr rep{std::in_place_type<Ex>, ...}
/// auto rep = rich_exception_ptr::make_with([](){ return Ex{...}; });
///
/// On supported platforms, this is FAR more efficient than throw-and-catch.
/// Still, construction allocates, and copy/destroy perform atomic ops.
///
/// For a cheap-to-make and cheap-to-copy "immortal" exception ptr, see
/// `immortal_rich_error.h`.
///
/// IMPORTANT NOTE: We do NOT want users to construct `rich_exception_ptr`
/// from type-erased `std::exception_ptr` or `folly::exception_wrapper`,
/// because that would break RTTI-avoidance optimizations in `result` code.
template <std::derived_from<std::exception> Ex>
explicit rich_exception_ptr_impl(Ex ex) {
using bits_t = typename B::bits_t; // MSVC thinks `B::BIT_NAME` is private
constexpr bits_t bits{[]() -> bits_t {
if constexpr (std::derived_from<Ex, rich_error_base>) {
// Redundant with `rich_error.h` asserts, hence no manual test.
static_assert(detail::has_offset0_base<Ex, rich_error_base>);
return static_cast<bits_t>(
bits_t::OWNS_EXCEPTION_PTR_and |
bits_t::IS_RICH_ERROR_BASE_masked_eq);
} else if constexpr (std::derived_from<Ex, OperationCancelled>) {
// We only want the throwing version here, since nothrow OC uses a
// different ctor, and a non-owned copy of a leaky singleton. This
// should never fire since `StoppedNoThrow` doesn't
// derive from `std::exception`, and `OperationCancelled` will soon
// no longer be directly constructible.
static_assert(
// FIXME: This one will go away:
std::is_same_v<const Ex, const OperationCancelled> ||
std::is_same_v<const Ex, const StoppedMayThrow>);
return static_cast<bits_t>(
bits_t::OWNS_EXCEPTION_PTR_and |
bits_t::IS_OPERATION_CANCELLED_masked_eq);
} else {
return static_cast<bits_t>(
bits_t::OWNS_EXCEPTION_PTR_and |
bits_t::owned_eptr_KNOWN_NON_FAST_PATH_TYPE_masked_eq);
}
}()};
assign_owned_eptr_and_bits(
make_exception_ptr_with(std::in_place, std::move(ex)), bits);
}
/// Copy constructor & assignment
constexpr rich_exception_ptr_impl(const rich_exception_ptr_impl& other) {
copy_from_impl(other);
}
rich_exception_ptr_impl& operator=(const rich_exception_ptr_impl& other) {
if (this != &other) {
destroy_owned_state_and_bits_caller_must_reset();
copy_from_impl(other);
}
return *this;
}
/// Move constructor & assignment
constexpr rich_exception_ptr_impl(rich_exception_ptr_impl&& other) noexcept {
move_from_impl(std::move(other));
}
rich_exception_ptr_impl& operator=(rich_exception_ptr_impl&& other) noexcept {
if (this != &other) {
destroy_owned_state_and_bits_caller_must_reset();
move_from_impl(std::move(other));
}
return *this;
}
// AVOID. Prefer `rich_exception_ptr{YourErr{}}`, since knowing the type
// statically can avoid RTTI for `rich_error.h` types, and can speed up
// `result<T>::has_stopped()`.
//
// Pass by-&& because `std::exception_ptr` isn't always efficiently movable,
// and we have a workaround for this.
static Derived from_exception_ptr_slow(std::exception_ptr&& e) {
if (!e) {
return Derived{}; // "Owned" never stores an empty eptr.
}
return Derived{detail::force_slow_rtti_t{}, std::move(e)};
}
// PRIVATE, not for end users -- the non-stub type will be in `detail`.
// Users will instead use `co_yield co_stopped_nothrow` in coros.
explicit rich_exception_ptr_impl(StoppedNoThrow) {
// Wrapper that constructs the exception_ptr on first use.
// No destructor needed - mutable_eptr_ref_guard is POD-like storage,
// so the exception_ptr is leaked to avoid SDOF.
struct InitializedSingleton : B::mutable_eptr_ref_guard {
InitializedSingleton() {
new (&this->ref()) std::exception_ptr{
make_exception_ptr_with(std::in_place_type<StoppedNoThrow>)};
}
};
// Meyer singleton is thread-safe past C++11.
// Yes, this will make an instance per DSO. That's expected and OK.
// NOLINTNEXTLINE(facebook-hte-InlinedStaticLocalVariableWarning)
static InitializedSingleton singleton;
B::apply_bits_after_setting_data_with(
// Bitwise-copy the singleton without bumping the eptr's refcount
[&](auto& d) { d.eptr_ref_guard_ = singleton; },
B::NOTHROW_OPERATION_CANCELLED_eq);
}
/// Throws the innermost exception, ignoring epitaph wrappers.
///
/// Precondition: Contains a nonempty exception. Terminates on empty
/// `exception_ptr`, on invalid internal state. Small-value is debug-fatal.
[[noreturn]] void throw_exception() const {
with_underlying([](auto* rep) {
rep->template throw_exception_impl</*PartOfTryImpl=*/false>();
});
compiler_may_unsafely_assume_unreachable(); // required for [[noreturn]]
}
// PRIVATE TO `Try`: `throw_exception()` with slight behavior differences.
[[noreturn]] void throw_exception(try_rich_exception_ptr_private_t) const {
with_underlying([](auto* rep) {
rep->template throw_exception_impl</*PartOfTryImpl=*/true>();
});
compiler_may_unsafely_assume_unreachable(); // required for [[noreturn]]
}
/// Returns the `std::exception_ptr` for the innermost exception, DISCARDING
/// epitaph wrappers.
///
/// Overload differences:
/// - `const&` copies the inner eptr (cost: an atomic refcount increment).
/// - `&&` moves the inner eptr, destroys any epitaph wrappers, and
/// leaves `this` in a moved-out, empty eptr state.
///
/// Precondition: Contains an exception, or empty eptr (debug-fatal otherwise)
[[nodiscard]] std::exception_ptr to_exception_ptr_slow() const& {
return to_exception_ptr_copy</*PartOfTryImpl=*/false>();
}
[[nodiscard]] std::exception_ptr to_exception_ptr_slow() && {
return to_exception_ptr_move</*PartOfTryImpl=*/false>();
}
// PRIVATE TO `Try`: `to_exception_ptr_slow()` with edge case differences.
[[nodiscard]] std::exception_ptr to_exception_ptr_slow(
try_rich_exception_ptr_private_t) const& {
return to_exception_ptr_copy</*PartOfTryImpl=*/true>();
}
[[nodiscard]] std::exception_ptr to_exception_ptr_slow(
try_rich_exception_ptr_private_t) && {
return to_exception_ptr_move</*PartOfTryImpl=*/true>();
}
/// Returns `true` when both `lhs` and `rhs`...
/// - ... point at the same underlying exception, per the details below.
/// - ... occur in the `Try` implementation, and both contain empty `Try`
/// (similarly for other non-user-observable sigils)
///
/// When `lhs` and `rhs` are both representable as eptrs, we compare the
/// underlying exception object **pointers** -- ignoring epitaph wrappers.
///
/// Caveat 1: If the same `immortal_rich_error<...>::ptr()` is instantiated
/// in multiple DSOs, then you may end up with multiple copies of the
/// underlying error, and their pointers will compare unequal. To stop this,
/// pick a single link unit to instantiate your `immortal_rich_error`, and
/// expose only the `rich_exception_ptr` via the corresponding header.
///
/// Caveat 2: Since `std::exception_ptr` isn't `constexpr`, an immortal rich
/// errors lazily makes an eptr singleton once needed. Actually, there are
/// two singletons -- immutable & mutable -- for reasons discussed on
/// `get_outer_exception()`. It is worth knowing that `operator==` can
/// compare an "owned eptr" with the mutable singleton of a "immortal rich
/// error". This will happen, for example, if you copy
/// `to_exception_ptr_slow`:
///
/// auto rep1 = immortal_error<YourErr>::get();
/// auto rep2 = rich_exception_ptr::from_exception_ptr_slow(
/// rep.to_exception_ptr_slow());
/// assert(rep1 == rep2);
///
/// The immutable singleton is never exposed as an eptr, so the 2-singleton
/// inconsistency is not observable via this `operator==`.
template <typename D2, typename B2>
friend constexpr inline bool operator==(
const rich_exception_ptr_impl& lhs,
// The template setup is asymmetric, but the implementation should anyhow
// be symmetric in lhs & rhs, so I don't expect issues from that.
//
// Besides symmetry, the key implementation constraint is NEVER to
// use the union members or `bits_`. Always, use the base-independent
// accessors -- this is required to support cross-base comparison.
//
// Note that this doesn't allow cross-value comparisons YET, just like
// `folly::result`, but this can be extended to follow `std::expected`.
const rich_exception_ptr_impl<D2, B2>& rhs) {
return lhs.with_underlying([&rhs](const auto* lp) {
return rhs.with_underlying([lp](const auto* rp) {
// MSVC needs full qualification here
return rich_exception_ptr_impl<Derived, B>::compare_equal(lp, rp);
});
});
}
// Like `folly::get_exception<Ex>()`, but accesses the actual outermost
// exception, even if it's wrapping another one -- instead of walking to the
// underlying error.
//
// CAREFUL: The type of this exception is different from whatever error
// actually occurred, see `epitaph.h` for the most common example.
//
// This is used by `rich_error_base::format_to` to walk the epitaph stack.
template <typename Ex>
constexpr Ex const* get_outer_exception() const noexcept
[[FOLLY_ATTR_CLANG_LIFETIMEBOUND]] {
assert_operation_cancelled_queries<Ex>();
bits_t bits = B::get_bits();
if (B::IS_IMMORTAL_RICH_ERROR_OR_EMPTY_eq == bits) {
if (!get_immortal_storage()) {
return nullptr; // empty eptr
}
// The goal of the branches below is to hide from the end user that
// (until C++26) `rich_error<UserBase>` cannot be `constexpr`, and thus
// `immortal_ptr_` is `immortal_rich_error_storage<UserBase>`.
//
// So, the below tests for `folly_detail_base_of_rich_error` (2nd) and
// `std::exception` (1st) aren't just optimizations, we need them to
// correctly query `Ex` types that derive from `std::exception`.
//
// Internally, both `get_immortal_storage()->as_immutable_*` calls
// instantiate an immortal singleton of `rich_error<UserBase>` with a
// copy of the `constexpr` instance of `UserBase`.
if constexpr (std::is_same_v<const std::exception, const Ex>) {
return get_immortal_storage()->as_immutable_std_exception();
}
const rich_error_base* immortal_ptr =
get_immortal_storage()->immortal_ptr_;
// WATCH OUT: The `rich_error<Err>` part of this "fast path" logic is NOT
// best-effort, but mandatory, see the prior comment.
//
// The "exact type" tests below can never pass if `Ex` isn't related to
// `rich_error_base` (e.g. `std::runtime_error`), and the `static_cast`
// won't even compile.
if constexpr (std::derived_from<Ex, rich_error_base>) {
// Querying for `rich_error<Err>`?
if constexpr (is_detected_v<
detect_folly_detail_base_of_rich_error,
Ex>) {
// Querying for `rich_error<Err>` when the immortal has `Err`.
if (&typeid(Ex) == get_immortal_storage()->rich_error_leaf_type_ ||
// We MUST fall back to the slower comparison of `type_info`
// objects, since in multi-DSO programs, there may exist multiple
// `type_info` pointers for the same type.
typeid(Ex) == *get_immortal_storage()->rich_error_leaf_type_) {
return static_cast<const Ex*>(
get_immortal_storage()->as_immutable_leaf_rich_error());
}
// Since we compared `type_info`s (and not just pointers), there's no
// need to fall back to `dynamic_cast` -- `rich_error<>` is final.
return nullptr;
} else { // Querying for non-leaf (abstract) `Ex`...
// Querying for `Ex` when the immortal stores `Ex`.
//
// Best-effort "fast path" to avoid RTTI when the query type exactly
// matches the stored type. This optimization may fail in multi-DSO
// code (where many copies of the same `type_info` may exist at
// different addresses). Like `exception_ptr_get_object_hint`, this
// gives a nice speedup for the common case.
//
// Only compare pointers, not `type_info` -- falling back to
// `dynamic_cast` will do that part anyway.
//
// Future: Neither this branch, nor the `get_mutable_exception` one
// uses `Ex::folly_get_exception_hint_types`, since the "exact type"
// checks take care of the common case. Implementing hint support is
// possible, but first we need a use-case where it matters.
if (&typeid(Ex) == get_immortal_storage()->user_base_type_) {
return static_cast<const Ex*>(immortal_ptr);
}
}
}
// Fallback. Cheap & RTTI-free only when `Ex == rich_error_base`.
//
// Future: See `docs/future_fast_rtti.md` for how to avoid RTTI entirely.
//
// Future: If we do later support non-rich immortals, keep in mind that
// `dynamic_cast` only works when the pointed-to expression is
// polymorphic -- deriving from `std::exception` or `rich_error_base` is
// fine. We may require one of these bases for immortals.
return dynamic_cast<const Ex*>(immortal_ptr);
} else if (B::OWNS_EXCEPTION_PTR_and & bits) {
return get_exception_from_owned_eptr<const Ex>();
} else if (B::NOTHROW_OPERATION_CANCELLED_eq == bits) {
return get_exception_from_nothrow_oc<const Ex>();
}
// The remaining bit states are not exceptions, so `nullptr` is a good
// match for `folly::get_exception(result<T>)` semantics.
B::debug_assert(
"const get_outer_exception unexpected bits",
B::SIGIL_eq == bits || B::SMALL_VALUE_eq == bits);
return nullptr;
}
/// See docs on the `const` overload of `get_outer_exception<Ex>()`. The
/// mutable one mainly exists to implement `folly::get_mutable_exception<>()`.
///
/// IMPORTANT: For immortal errors, there is no read-after-write consistency
/// in the sense that you might expect. Rather, this returns a pointer into
/// a mutable, immortal singleton of `rich_error<UserBase>` that is
/// completely separate from the one that the `const` overload accesses.
///
/// ## Why don't you have ONE read/write singleton for immortals?
///
/// We could, but it would broadly add cost for a use-case that shouldn't
/// really come up. If your edge-case usage relies on mutable access to an
/// immortal, you're probably aware of this gotcha, and can arrange to
/// call `get_mutable_exception` or the non-`const` overload of
/// `get_outer_exception` where it matters.
///
/// Here's a closer look at the downsides of a 1-singleton implementation:
/// - `get_exception<UserBase>()` & `get_exception<rich_error<UserBase>>()`
/// must access the same instance. To achieve this, immortals would need
/// a mutable pointer to redirect from the constexpr `UserBase` to the
/// immortal singleton, as soon as the first mutable access happens.
/// - This pointer needs to be a relaxed read / relaxed write atomic,
/// which, while not very costly, adds indirection. This also implies a
/// second indirection, for a virtual `immortal_ptr()` instead of the
/// current `immortal_ptr_`. Finally, there'd be an extra branch. The
/// expected extra cost is 3-5ns without memory contention, which is as
/// much as the current `get_rich_error()` latency.
/// - A further plus for the current 2-singletons implementation is that
/// it’s technically possible to avoid the atomic-increment cost of
/// `to_exception_ptr_slow()` through some bithacking. Namely, the
/// mutable singleton `std::exception_ptr` is initially prepared with a
/// Very High Refcount of 2^63, and we reset it if it goes too low or too
/// high. A concrete pattern would involve a relaxed-atomic check of the
/// first 2 bits. If the bits are 00, or 11, then reset back to 2^63.
/// This allows safely handing out copies without bumping the refcount --
/// it would only break if a user simultaneously stored over 2^63 eptrs.
template <typename Ex>
constexpr Ex* get_outer_exception() noexcept
[[FOLLY_ATTR_CLANG_LIFETIMEBOUND]] {
assert_operation_cancelled_queries<Ex>();
// This mirrors the `const` overload, read that first for the comments.
bits_t bits = B::get_bits();
if (B::IS_IMMORTAL_RICH_ERROR_OR_EMPTY_eq == bits) {
if (!get_immortal_storage()) {
return nullptr;
}
B::debug_assert(
"mutable get_outer_exception",
B::get_bits() == B::IS_IMMORTAL_RICH_ERROR_OR_EMPTY_eq);
if constexpr (std::is_same_v<const std::exception, const Ex>) {
return get_immortal_storage()->as_mutable_std_exception();
}
auto* leaf_rich_error =
get_immortal_storage()->as_mutable_leaf_rich_error();
if constexpr (std::derived_from<Ex, rich_error_base>) {
// Try to avoid RTTI when the immortal stores `Err` and the query is
// either `Err` or `rich_error<Err>`.
auto* type_to_match =
is_detected_v<detect_folly_detail_base_of_rich_error, Ex>
// NB: This could "miss" 8x faster by reusing the `const` pattern,
// but... people really shouldn't use mutable errors, anyhow!
? get_immortal_storage()->rich_error_leaf_type_
: get_immortal_storage()->user_base_type_;
// Best-effort optimization, may fail in multi-DSO code: Compare only
// ptrs, not `type_info` -- `dynamic_cast` will do that part anyway.
if (&typeid(Ex) == type_to_match) {
return static_cast<Ex*>(leaf_rich_error);
}
}
return dynamic_cast<Ex*>(leaf_rich_error);
} else if (B::OWNS_EXCEPTION_PTR_and & bits) {
return get_exception_from_owned_eptr<Ex>();
} else if (B::NOTHROW_OPERATION_CANCELLED_eq == bits) {
return get_exception_from_nothrow_oc<Ex>();
}
B::debug_assert(
"mutable get_outer_exception unexpected bits",
B::SIGIL_eq == bits || B::SMALL_VALUE_eq == bits);
return nullptr;
}
private:
/// Returns the `typeid` of the innermost exception object, ignoring
/// epitaph wrappers. If no exception object is stored, returns null.
///
/// UNFINISHED: Could perhaps be made public, currently PRIVATE due to the API
/// design issue with immortals documented inline. That would simplify the
/// `rich_error_base` plumbing.
constexpr const std::type_info* UNFINISHED_exception_type() const noexcept {
using bits_t = typename B::bits_t; // MSVC thinks `B::BIT_NAME` is private
return with_underlying([](auto* rep) -> const std::type_info* {
if (bits_t::OWNS_EXCEPTION_PTR_and & rep->get_bits()) {
return exception_ptr_get_type(rep->get_eptr_ref_guard().ref());
} else if (
bits_t::IS_IMMORTAL_RICH_ERROR_OR_EMPTY_eq == rep->get_bits()) {
// This shouldn't even be hit on our internal formatting code path.
//
// FIXME: This is potentially too confusing for a public API, since:
// - The actual storage type is `immortal_rich_error_storage`, which
// is not user-addressable.
// - The user can get `const UserBase*`, `rich_error<UserBase>*>, or
// `const rich_error<UserBase>*`, but the latter 2 have costs,
// and all 3 are distinct objects.
return rep->get_immortal_storage()
? rep->get_immortal_storage()->user_base_type_
: nullptr;
} else if (bits_t::NOTHROW_OPERATION_CANCELLED_eq == rep->get_bits()) {
return &typeid(StoppedNoThrow);
}
return nullptr; // Non-exceptions: sigil uintptr, small value uintptr
});
}
// Calls `get_outer_exception` on the underlying error in the epitaph stack
template <typename CEx>
static constexpr rich_ptr_to_underlying_error<CEx> get_exception_impl(
auto* rep);
public:
/// Implementation of `folly::get_exception<Ex>(rich_exception_ptr)`
///
/// Transparently handles errors with epitaph wrappers -- the returned
/// pointer-like resolves to the underlying, original exception. But, `fmt` or
/// `ostream::operator<<` will display the full epitaph stack.
///
/// Avoid converting the result to `Ex*`, or you will lose the epitaphs.
///
/// Sample usage:
///
/// if (auto ex = get_exception<Ex>(...)) { // NOT `Ex* ex`!
/// LOG(INFO) << ex; // Will include epitaphs
/// }
///
/// IMPORTANT: For immortal errors, this `const` accessor will access a
/// different instance than `get_mutable_exception` does. This will not
/// matter for 99% of usage, since mutating exceptions is VERY rare, and
/// likely a design smell. In short, this was done is to avoid globally
/// imposing the cost of synchronization for the sake of a tiny minority
/// use-case. The non-`const` overload of `get_outer_exception` says more.
///
/// Future: There's no return state for "did not match `Ex` (aka `nullptr`),
/// but still have epitaphs" -- but it is easy to add if useful.
template <typename Ex>
constexpr rich_ptr_to_underlying_error<const Ex> get_exception(
get_exception_tag_t) const noexcept {
return get_exception_impl<const Ex>(this);
}
// IMPORTANT: For immortal errors, this will access a different instance than
// `get_exception` does, see the details in the non-`const` overload of
// `get_outer_exception`.
template <typename Ex>
constexpr rich_ptr_to_underlying_error<Ex> get_mutable_exception(
get_exception_tag_t) noexcept {
return get_exception_impl<Ex>(this);
}
};
using rich_exception_ptr_base = rich_exception_ptr_impl<
rich_exception_ptr,
conditional_t<
rich_exception_ptr_packed_storage::is_supported,
rich_exception_ptr_packed_storage,
rich_exception_ptr_separate_storage>>;
} // namespace detail
/// `rich_exception_ptr` is an analog of `exception_wrapper` or
/// `std::exception_ptr`, with some extra efficiency optimizations, and
/// integration with `rich_error` / `epitaph`. It was designed to
/// support rich-error features in `result.h`.
///
/// This class typically owns a `std::exception_ptr`, or stores a cheap-to-copy
/// pointer to an immortal exception. Unlike `exception_wrapper`, it enables
/// RTTI-free lookup for `rich_error_base` & `result<T>::has_stopped()`.
///
/// However, private APIs can store some other states in its internal union.
/// For this reason, `rich_exception_ptr` should not be used in most end-user
/// code. Instead, use `error_or_stopped`.
///
/// While generally aligned to `exception_wrapper`, this API is much smaller.
/// Notably, the ONLY way to test `rich_exception_ptr` for the presence of the
/// exception type `Ex` is via `folly::get_exception<Ex>(rich_eptr)`.
class [[nodiscard]]
rich_exception_ptr final : public detail::rich_exception_ptr_base {
using detail::rich_exception_ptr_base::rich_exception_ptr_base;
public:
void format_to(
fmt::appender out, detail::format_to_skip_rich_t opts = {}) const;
};
namespace detail {
template <typename Derived, typename B>
/*static*/ constexpr inline auto
rich_exception_ptr_impl<Derived, B>::with_underlying_impl(auto* me, auto fn)
-> decltype(fn(me)) {
// Why only these bits, and no others?
// - The `consteval rich_exception_ptr_impl` ctor guarantees that immortals
// don't have a `underlying_error()` & explains how to extend if needed.
// - None of the other states, nothrow OC included, are rich errors.
if (B::OWNS_EXCEPTION_PTR_and & me->get_bits()) {
if (const auto* rex = me->template get_outer_exception<rich_error_base>()) {
// The doc of `mutable_underlying_error()` explains why the `const` here
// is crucial. Also, `to_exception_ptr_move` logic above relies on this.
if (const auto* rep = rex->underlying_error()) {
return fn(rep);
}
}
}
return fn(me);
}
// Specializes the `with_underlying()` traversal for `get_exception<>()`, while
// populating `top_rich_error_` to support formatting with epitaphs.
//
// Future: A possible micro-optimization idea to try to save 1-2ns would be to
// deduplicate the PLT call to `exception_ptr_get_object` (one per
// `get_outer_exception` call below).
template <typename Derived, typename B>
template <typename CEx>
constexpr inline auto rich_exception_ptr_impl<Derived, B>::get_exception_impl(
auto* rep) -> rich_ptr_to_underlying_error<CEx> {
assert_operation_cancelled_queries<CEx>();
const rich_error_base* top_rich_error = nullptr;
auto bits = rep->get_bits();
if (B::IS_IMMORTAL_RICH_ERROR_OR_EMPTY_eq == bits) {
// Per the `consteval rich_exception_ptr_impl` ctor, all immortals are
// underlying. So, they get special logic to set `top_rich_error_`.
if constexpr (std::derived_from<CEx, rich_error_base>) {
auto* ex = rep->template get_outer_exception<CEx>();
return {ex, static_cast<const rich_error_base*>(ex)};
} else if constexpr (!std::is_same_v<const CEx, const std::exception>) {
// Immortals are rich, but query is neither std::exception, nor rich.
return {nullptr, nullptr};
} // else: `std::exception` IS accessible on immortals, needs 2 queries:
top_rich_error = rep->template get_outer_exception<rich_error_base>();
// Fall through to query for `CEx` in the final `return`...
} else if (B::OWNS_EXCEPTION_PTR_and & bits) {
// This dynamic eptr may be an epitaph wrapper, so we have to retrieve
// the underlying error. This gives us `top_rich_error_` for free.
if (auto* rex = rep->template get_outer_exception<rich_error_base>()) {
top_rich_error = rex;
if (auto* new_rep = [rex]() {
if constexpr (std::is_const_v<CEx>) {
return rex->underlying_error();
} else {
return rex->mutable_underlying_error(
rich_error_base::underlying_error_private_t{});
}
}()) {
// We found the underlying error, time to query `CEx`!
// NB: Falling through with `rep = new_rep;` emitted slower code.
return {new_rep->template get_outer_exception<CEx>(), top_rich_error};
} else if constexpr (std::is_same_v<const CEx, const rich_error_base>) {
// `rex` is underlying error, and exactly what was queried
return {rex, top_rich_error};
} // Else: `rep` is underlying & `top_rich_error` is set; fall through...
} else if constexpr (std::derived_from<CEx, rich_error_base>) {
// Outer isn't a `rich_error_base`, so it's also the underlying error,
// and thus cannot match any `rich_error_base`-derived `CEx`.
return {nullptr, nullptr};
} // Else: `rep` is underlying & non-rich; `top_rich_error` stays null...
// ... fall through to query `CEx`.
} // Else: We only need to resolve underlying for "owned" bits.
return {rep->template get_outer_exception<CEx>(), top_rich_error};
}
} // namespace detail
std::ostream& operator<<(std::ostream&, const rich_exception_ptr&);
} // namespace folly
template <>
struct fmt::formatter<folly::rich_exception_ptr> {
constexpr fmt::format_parse_context::iterator parse(
fmt::format_parse_context& ctx) {
return ctx.begin();
}
fmt::format_context::iterator format(
const folly::rich_exception_ptr& rep, fmt::format_context& ctx) const {
rep.format_to(ctx.out());
return ctx.out();
}
};
#endif // FOLLY_HAS_RESULT