Files
scylladb/utils/result_try.hh
Dawid Mędrek a151944fa6 treewide: Replace __builtin_expect with (un)likely
C++20 introduced two new attributes--likely and unlikely--that
function as a built-in replacement for __builtin_expect implemented
in various compilers. Since it makes code easier to read and it's
an integral part of the language, there's no reason to not use it
instead.

Closes scylladb/scylladb#24786
2025-07-03 13:34:04 +03:00

623 lines
24 KiB
C++

/*
* Copyright 2022-present ScyllaDB
*/
/*
* SPDX-License-Identifier: LicenseRef-ScyllaDB-Source-Available-1.0
*/
#pragma once
#include <concepts>
#include <type_traits>
#include "utils/result.hh"
namespace utils {
namespace internal {
struct noop_converter {
template<typename T>
using wrapped_type = T;
template<typename T>
static
wrapped_type<T>
wrap(T&& t) {
return std::move(t);
}
template<typename F, typename... Args>
static
wrapped_type<std::invoke_result_t<F, Args...>>
invoke(const F& f, Args&&... args) {
return std::invoke(f, std::forward<Args>(args)...);
}
};
struct futurizing_converter {
template<typename T>
using wrapped_type = seastar::futurize_t<T>;
template<typename T>
static
wrapped_type<T>
wrap(T&& t) {
if constexpr (seastar::is_future<T>::value) {
return t;
} else {
return seastar::make_ready_future<T>(std::forward<T>(t));
}
}
template<typename F, typename... Args>
static
wrapped_type<std::invoke_result_t<F, Args...>>
invoke(const F& f, Args&&... args) {
return seastar::futurize_invoke(f, std::forward<Args>(args)...);
}
};
template<typename From, typename Converter, typename To>
concept ConvertsWithTo = std::convertible_to<typename Converter::template wrapped_type<From>, typename Converter::template wrapped_type<To>>;
// We require forall<ExceptionContainerResult R> ExceptionHandle<H, R>.
// However, C++ does not support quantification like that in the constraints.
// Here, we use the dummy_result to SCYLLA_ASSERT that the handles defined below
// at least work with dummy_result.
template<typename T = void>
using dummy_result = result_with_exception<T, std::exception>;
// TODO: Should we make a virtual base class for this?
// This would allow using non-generic lambdas in result_catch and result_catch_dots.
template<typename Handle>
concept ExceptionHandle =
ExceptionContainerResult<dummy_result<>> &&
requires (Handle handle, seastar::promise<dummy_result<int>>& p) {
{ handle.into_result() } -> std::same_as<dummy_result<>>;
{ handle.into_future() } -> std::same_as<seastar::future<dummy_result<>>>;
{ handle.forward_to_promise(p) } -> std::same_as<void>;
};
template<ExceptionContainerResult R>
struct failed_result_handle {
private:
R&& _failed_result;
public:
failed_result_handle(R&& r) : _failed_result(std::move(r)) {}
// Returns the failed result. Can be used only once as this moves out
// the failed result. This should be called once during the handler's
// lifetime, preferably as `return handler.into_result()`.
R into_result() {
return std::move(_failed_result);
}
seastar::future<R> into_future() {
return seastar::make_ready_future<R>(std::move(_failed_result));
}
template<ExceptionContainerResult S>
requires std::same_as<typename R::error_type, typename S::error_type>
void forward_to_promise(seastar::promise<S>& p) {
p.set_value(std::move(_failed_result).as_failure());
}
const typename R::error_type& as_inner() const & {
return _failed_result.assume_error();
}
typename R::error_type&& as_inner() && {
return std::move(_failed_result).assume_error();
}
typename R::error_type clone_inner() {
return _failed_result.assume_error().clone();
}
};
static_assert(ExceptionHandle<failed_result_handle<dummy_result<>>>);
template<ExceptionContainerResult R>
struct exception_ptr_handle {
private:
std::exception_ptr _eptr;
public:
exception_ptr_handle(std::exception_ptr&& eptr) : _eptr(std::move(eptr)) {}
// Throws the exception.
[[noreturn]] R into_result() {
std::rethrow_exception(std::move(_eptr));
}
seastar::future<R> into_future() {
return seastar::make_exception_future<R>(std::move(_eptr));
}
template<ExceptionContainerResult S>
requires std::same_as<typename R::error_type, typename S::error_type>
void forward_to_promise(seastar::promise<S>& p) {
p.set_exception(std::move(_eptr));
}
const std::exception_ptr& as_inner() const & {
return _eptr;
}
std::exception_ptr&& as_inner() && {
return std::move(_eptr);
}
std::exception_ptr clone_inner() {
return _eptr;
}
};
// Check that it is a valid handle with some arbitrary ExceptionContainerResult
static_assert(ExceptionHandle<exception_ptr_handle<dummy_result<>>>);
template<typename Ex, typename Cb>
struct result_catcher {
private:
Cb _cb;
public:
using exception_type = Ex;
using callback_type = Cb;
public:
result_catcher(Cb&& cb) : _cb(std::move(cb)) {}
template<ExceptionContainerResult R, typename Converter, typename Continuation>
typename Converter::template wrapped_type<R>
handle_exception_from_result(const auto& ex, R& original_result, Continuation&& cont) {
if constexpr (std::is_base_of_v<Ex, std::remove_cvref_t<decltype(ex)>>) {
if constexpr (std::is_invocable_v<Cb, const Ex&, failed_result_handle<R>>) {
// Invoke with exception reference and handle
return Converter::template invoke<Cb, const Ex&, failed_result_handle<R>>(_cb, ex, failed_result_handle<R>(std::move(original_result)));
} else {
// Simplified interface - invoke with reference to exception only
static_assert(std::is_invocable_v<Cb, const Ex&>,
"The handler function does not have a suitable call operator");
return Converter::template invoke<Cb, const Ex&>(_cb, ex);
}
} else {
// Let another handler try it
return std::invoke(std::forward<Continuation>(cont));
}
}
template<ExceptionContainerResult R, typename Converter, typename Continuation>
auto wrap_in_catch(Continuation&& cont) {
return [this, cont = std::forward<Continuation>(cont)] (bool& already_caught) mutable -> typename Converter::template wrapped_type<R> {
try {
return std::invoke(std::forward<Continuation>(cont), already_caught);
} catch (const Ex& ex) {
if (already_caught) {
throw;
}
already_caught = true;
if constexpr (std::is_invocable_v<Cb, const Ex&, exception_ptr_handle<R>>) {
// Invoke with exception reference and handle
return Converter::template invoke<const Cb, const Ex&, exception_ptr_handle<R>>(_cb, ex, exception_ptr_handle<R>(std::current_exception()));
} else {
// Simplified interface - invoke with reference to exception only
static_assert(std::is_invocable_v<Cb, const Ex&>,
"The handler function does not have a suitable call operator");
return Converter::template invoke<const Cb, const Ex&>(_cb, ex);
}
}
};
}
};
template<typename T>
struct is_result_catcher : std::false_type {};
template<typename Ex, typename Cb>
struct is_result_catcher<result_catcher<Ex, Cb>> : std::true_type {};
template<typename Handler, typename Converter, typename Result>
concept ResultCatcherCallback =
requires (typename Handler::callback_type callback, typename Handler::exception_type& ex,
failed_result_handle<Result>&& frh, exception_ptr_handle<Result>&& eph) {
{ callback(ex, frh) } -> ConvertsWithTo<Converter, Result>;
{ callback(ex, eph) } -> ConvertsWithTo<Converter, Result>;
} ||
requires (typename Handler::callback_type callback, typename Handler::exception_type& ex) {
{ callback(ex) } -> ConvertsWithTo<Converter, Result>;
};
template<typename Handler, typename Converter, typename Result>
concept ResultCatcher =
is_result_catcher<Handler>::value &&
ResultCatcherCallback<Handler, Converter, Result>;
template<typename Cb>
struct result_catcher_dots {
private:
Cb _cb;
public:
using callback_type = Cb;
public:
result_catcher_dots(Cb&& cb) : _cb(std::move(cb)) {}
template<ExceptionContainerResult R, typename Converter, typename Continuation>
typename Converter::template wrapped_type<R>
handle_exception_from_result(const auto& ex, R& original_result, Continuation&& cont) {
if constexpr (std::is_invocable_v<Cb, failed_result_handle<R>>) {
// Invoke with handle
return Converter::template invoke<Cb, failed_result_handle<R>>(_cb, failed_result_handle<R>(std::move(original_result)));
} else {
// Simplified interface - invoke without arguments
static_assert(std::is_invocable_v<Cb>,
"The handler function does not have a suitable call operator");
return Converter::template invoke<Cb>(_cb);
}
// Don't propagate to the next handler. The catch (...) is supposed
// to match on all errors.
}
template<ExceptionContainerResult R, typename Converter, typename Continuation>
auto wrap_in_catch(Continuation&& cont) {
return [this, cont = std::forward<Continuation>(cont)] (bool& already_caught) mutable -> typename Converter::template wrapped_type<R> {
try {
return std::invoke(std::forward<Continuation>(cont), already_caught);
} catch (...) {
if (already_caught) {
throw;
}
already_caught = true;
if constexpr (std::is_invocable_v<Cb, exception_ptr_handle<R>>) {
// Invoke with handle
return Converter::template invoke<Cb, exception_ptr_handle<R>>(_cb, exception_ptr_handle<R>(std::current_exception()));
} else {
// Simplified interface - invoke without arguments
static_assert(std::is_invocable_v<Cb>,
"The handler function does not have a suitable call operator");
return Converter::template invoke<Cb>(_cb);
}
}
};
}
};
template<typename T>
struct is_result_catcher_dots : std::false_type {};
template<typename Cb>
struct is_result_catcher_dots<result_catcher_dots<Cb>> : std::true_type {};
template<typename Handler, typename Converter, typename Result>
concept DotsResultCatcherCallback =
requires (typename Handler::callback_type callback,
failed_result_handle<Result>&& frh, exception_ptr_handle<Result>&& eph) {
{ callback(frh) } -> ConvertsWithTo<Converter, Result>;
{ callback(eph) } -> ConvertsWithTo<Converter, Result>;
} ||
requires (typename Handler::callback_type callback) {
{ callback() } -> ConvertsWithTo<Converter, Result>;
};
template<typename Handler, typename Converter, typename Result>
concept DotsResultCatcher =
is_result_catcher_dots<Handler>::value &&
DotsResultCatcherCallback<Handler, Converter, Result>;
// Constructs an `invoke_in_try_catch` function which allows to call a callback
// and handle C++ exceptions using a set of handlers given during construction.
//
// The `Converter` is used to appropriately invoke the exception handlers.
template<ExceptionContainerResult R, typename Converter, typename... CatchHandlers>
struct try_catch_chain_impl {};
template<ExceptionContainerResult R, typename Converter, typename FirstCatchHandler, typename... CatchHandlers>
struct try_catch_chain_impl<R, Converter, FirstCatchHandler, CatchHandlers...> {
template<typename Continuation>
static
typename Converter::template wrapped_type<R>
invoke_in_try_catch(Continuation&& cont, FirstCatchHandler& first_handler, CatchHandlers&... catch_handlers) {
return try_catch_chain_impl<R, Converter, CatchHandlers...>::template invoke_in_try_catch<>(
first_handler.template wrap_in_catch<R, Converter, Continuation>(std::forward<Continuation>(cont)),
catch_handlers...);
}
};
template<ExceptionContainerResult R, typename Converter>
struct try_catch_chain_impl<R, Converter> {
template<typename Continuation>
static
typename Converter::template wrapped_type<R>
invoke_in_try_catch(Continuation&& cont) {
// Not using an invoker as we always want it to throw
bool already_caught = false;
return std::invoke(std::forward<Continuation>(cont), already_caught);
}
};
// Given a set of handlers, constructs a visitor which can be used to inspect
// a failed result.
//
// The `Converter` is used to appropriately invoke the exception handlers
// and convert the result to the return type if no handlers match on it.
template<ExceptionContainerResult R, typename Converter, typename... ResultHandlers>
struct combined_handler_impl {};
template<ExceptionContainerResult R, typename Converter, typename FirstResultHandler, typename... ResultHandlers>
struct combined_handler_impl<R, Converter, FirstResultHandler, ResultHandlers...>
: protected combined_handler_impl<R, Converter, ResultHandlers...> {
private:
using base = combined_handler_impl<R, Converter, ResultHandlers...>;
FirstResultHandler& _first_handler;
public:
combined_handler_impl(FirstResultHandler& first_handler, ResultHandlers&... result_handlers)
: base(result_handlers...)
, _first_handler(first_handler)
{ }
typename Converter::template wrapped_type<R>
handle(const auto& ex, R& original_result) {
return _first_handler.template handle_exception_from_result<R, Converter>(
ex, original_result,
[this, &ex, &original_result] () mutable {
return base::handle(ex, original_result);
});
}
auto with_original_result(R& res) {
return [this, &res] (const auto& ex) {
return this->handle(ex, res);
};
}
};
template<ExceptionContainerResult R, typename Converter>
struct combined_handler_impl<R, Converter> {
public:
typename Converter::template wrapped_type<R>
handle(const auto& ex, R& original_result) {
// No more handlers to try out, just return
return Converter::template wrap<R>(std::move(original_result));
}
auto with_original_result(R& res) {
return [this, &res] (const auto& ex) {
return handle(ex, res);
};
}
};
template<typename Result, typename... Handlers>
[[gnu::always_inline]]
inline
seastar::future<Result>
result_handle_available_future(seastar::future<Result>&& f, Handlers&... handlers) {
using combined_handler_type = internal::combined_handler_impl<Result, internal::futurizing_converter, Handlers...>;
using try_catch_chain_type = internal::try_catch_chain_impl<Result, internal::futurizing_converter, Handlers...>;
if (!f.failed()) {
Result&& res = f.get();
if (res) {
return seastar::make_ready_future<Result>(std::move(res));
}
// Handle the exception in result
auto combined_handler = combined_handler_type(handlers...);
return res.assume_error().accept(combined_handler.with_original_result(res));
} else {
// The future has an exception
// We need to create a try..catch chain from the handlers
// and rethrow the exception inside it
//
// futurize_invoke will protect us in case the exception is not handled
// by any of the provided handlers.
return seastar::futurize_invoke([&] {
return try_catch_chain_type::invoke_in_try_catch(
[&f] (bool&) mutable -> seastar::future<Result> { std::rethrow_exception(std::move(f).get_exception()); },
handlers...);
});
}
}
}
template<typename Handler, typename Converter, typename Result>
concept ResultCatcherMaybeDots =
internal::ResultCatcher<Handler, Converter, Result> ||
internal::DotsResultCatcher<Handler, Converter, Result>;
template<typename Fun>
concept ResultTryBody = requires (Fun fun) {
{ fun() } -> ExceptionContainerResult<>;
} && std::is_nothrow_move_constructible_v<std::invoke_result_t<Fun>>;
template<typename Fun>
concept ResultFuturizeTryBody = requires (Fun&& fun) {
{ seastar::futurize_invoke(std::forward<Fun>(fun)) } -> ExceptionContainerResultFuture<>;
} && std::is_nothrow_move_constructible_v<typename seastar::futurize_t<std::invoke_result_t<Fun>>::value_type>;
/// \brief Allows to handle C++ exceptions and failed results in a unified way.
///
/// When you modify a code path to return a result<> and you encounter
/// a try..catch block, you can use it to migrate the block so that it handles
/// both C++ exceptions and exceptions stored in the result<>.
///
/// \section Example
///
/// Let's say that you have the following try..catch chain:
///
/// try {
/// return a_function_that_may_throw();
/// } catch (const my_exception& ex) {
/// return 123;
/// } catch (...) {
/// throw;
/// }
///
/// You can add support for results in the following way:
///
/// return utils::result_try([&] {
/// return a_function_that_may_throw_or_return_a_failed_result();
/// }, utils::result_catch<my_exception>([&] (const Ex&) -> result<int> {
/// return 123;
/// }), utils::result_catch_dots([&] (auto&& handle) -> result<int> {
/// return handle.into_result();
/// });
///
/// Each `result_catch` handler declares the exception type it handles
/// and accepts a const reference to it as the first argument.
/// The `result_catch_dots` handler is equivalent to `(...)` so it does not
/// declare exception type or accept a reference to it.
///
/// In addition, each `result_catch` and `result_catch_dots` can optionally
/// accept a reference to an exception handle. The exception handle can be used
/// to return/rethrow the exception being currently handled. Depending on
/// whether the handler is invoked for a C++ exception or a failed result<>,
/// it will have a different type and its `into_result()` method will either
/// rethrow the exception or return the failed result, respectively.
///
/// \section Limitations
///
/// The main limitation of result_try is that is is not as flexible with control
/// flow as a try..catch block is. If you have some logic after the try..catch
/// block and some code paths in it block return and some don't, you need to
/// mark it somehow in the return value - for example, you can change result<T>
/// to result<std::optional<T>> and continue if it is std::nullopt, otherwise
/// return.
///
/// The return type of the first argument must be nothrow move constructible.
/// Because this function uses many recursive calls to simulate a multi-catch
/// clause try..catch block and NRVO is not in general guaranteed, there is
/// a risk that some moves are not elided and one of them can throw
/// in the middle of the stack. This exception can be thrown after we already
/// exited the try..catch blocks for some handlers, so not all of them will
/// participate in handling this exception. Because of the inability to properly
/// handle exceptions from failed moves, hence the nothrow-move-constructibility
/// requirement.
///
/// This function does not work with futures and will trigger a static_assert
/// if you use it with a future-returning function. See result_futurize_try
/// for a version which additionally works with exceptional futures.
template<ResultTryBody Fun, ResultCatcherMaybeDots<internal::noop_converter, std::invoke_result_t<Fun>>... Handlers>
requires (!seastar::is_future<std::invoke_result_t<Fun>>::value)
inline
std::invoke_result_t<Fun>
result_try(Fun&& fun, Handlers&&... handlers) {
using result_type = std::invoke_result_t<Fun>;
using combined_handler_type = internal::combined_handler_impl<result_type, internal::noop_converter, Handlers...>;
using try_catch_chain_type = internal::try_catch_chain_impl<result_type, internal::noop_converter, Handlers...>;
// Invoke `fun` and catch C++ exceptions if any occur
result_type res = try_catch_chain_type::template invoke_in_try_catch<>([&fun] (bool&) { return fun(); }, handlers...);
if (res) {
return res;
}
// No C++ exceptions but the result is a failure - inspect using a visitor
auto combined_handler = combined_handler_type(handlers...);
return res.assume_error().accept(combined_handler.with_original_result(res));
}
/// \brief A version of `result_try` which works with futures. It handles
/// C++ exceptions, failed results and exceptional futures returned from `fun`.
///
/// Migration from a try..catch block or f.handle_exception(...) is similar
/// as in the case of `result_try`, with a small number of differences
/// described below.
///
/// The `fun` function and all exception handlers are futurize_invoked,
/// therefore all C++ exceptions are converted to exceptional futures.
///
/// In order to perform `throw`/`make_exception_future<>(std::current_exception())`,
/// you should prefer `handle.into_future()` instead of `handle.into_result()`.
/// Unlike the latter, the former avoids rethrowing the exception and just
/// returns an exceptional future from a captured exception pointer.
///
/// Important note about preemption: if the `try` body returns a failure
/// without preemption, then the `catch` handler will be called without
/// preemption, too.
template<ResultFuturizeTryBody Fun,
ResultCatcherMaybeDots<internal::futurizing_converter,
typename seastar::futurize_t<std::invoke_result_t<Fun>>::value_type>... Handlers>
seastar::futurize_t<std::invoke_result_t<Fun>>
result_futurize_try(Fun&& fun, Handlers&&... handlers) {
using fun_return_type = std::invoke_result_t<Fun>;
using future_type = seastar::futurize_t<fun_return_type>;
using result_type = typename future_type::value_type;
using try_catch_chain_type = internal::try_catch_chain_impl<result_type, internal::futurizing_converter, Handlers...>;
// Try to invoke the function and handle C++ exceptions if there are any
bool no_exceptions = false;
future_type f = seastar::futurize_invoke([&] () {
return try_catch_chain_type::template invoke_in_try_catch<>([&no_exceptions, &fun] (bool&) {
// If `fun` didn't throw, we can set the `no_exceptions` flag
// so that we know that there was a throw and we shouldn't
// run the exception handling logic again.
fun_return_type ret = fun();
no_exceptions = true;
return seastar::futurize<fun_return_type>::convert(std::move(ret));
}, handlers...);
});
if (!no_exceptions) [[unlikely]] {
// There were C++ exceptions, and we handled them already
return f;
}
// Although then_wrapped should call the lambda without preemption,
// this is not guaranteed in debug mode, and I feel that the compiler
// struggles to generate efficient code for this case in release mode.
// We manually check if the future is available so that we can ensure
// the guarantee stated in the comment.
if (f.available()) {
return internal::result_handle_available_future(std::move(f), handlers...);
}
return f.then_wrapped([...handlers = std::move(handlers)] (future_type&& f) mutable -> future_type {
return internal::result_handle_available_future(std::move(f), handlers...);
});
}
/// \brief Represents a `catch (const Ex& ex) {}` part in a `result_try` chain.
///
/// The callback must be a generic lambda, and be callable with one
/// of the following sets of arguments:
///
/// (const Ex&)
/// (const Ex&, Handle&&)
///
/// where `Handle` is satisfies the ExceptionHandle<R> concept
/// and `R` is the result return type.
///
/// See the description of `result_try` for more info.
template<typename Ex, typename Cb>
auto result_catch(Cb&& cb) {
return internal::result_catcher<Ex, Cb>(std::move(cb));
}
/// \brief Represents a `catch (...) {}` part in a `result_try` chain.
///
/// The callback must be a generic lambda, and be callable with one
/// of the following sets of arguments:
///
/// ()
/// (Handle&&)
///
/// where `Handle` is satisfies the ExceptionHandle<R> concept
/// and `R` is the result return type.
///
/// See the description of `result_try` for more info.
template<typename Cb>
auto result_catch_dots(Cb&& cb) {
return internal::result_catcher_dots<Cb>(std::move(cb));
}
}