| Document #: | P3014R0 |
| Date: | 2023-10-14 |
| Project: | Programming Language C++ |
| Audience: |
LEWG |
| Reply-to: |
Jonathan Müller (think-cell) <foonathan@jonathanmueller.dev> |
We propose a way to customize the exception thrown by
std::expected::value(). That
way, we can make std::expected
more usable in interfaces that want to support both error codes and
exceptions.
[P0260R7] proposes concurrent queues to
the C++ standard library. Their operations can fail, so the current
design proposes the
std::filesystem approach of
having two overloads: one that throws, and one that fills a
std::error_code parameter. [P2921R0] explores different designs, in
particular an approach that uses
std::expected:
Status Quo
|
std::expected
|
|---|---|
|
|
Status Quo
|
std::expected
|
|---|---|
|
|
Status Quo
|
std::expected
|
|---|---|
|
|
We propose a way to customize the exception thrown by
std::expected::value() using a
new std::expected_traits
mechanism. When applied to
conqueue_errc, it can result in
the following interface.
Status Quo
|
std::expected, our
proposal
|
|---|---|
|
|
The std::expected paper [P0323R12] has a discussion on this in
section 3.16, but it was not proposed and has not been seriously
discussed in the committee at the time.
The new std::expected_traits
has a default specialization that throws std::bad_expected_access<E>:
template <typename E>
struct expected_traits
{
[[noreturn]] static void throw_error(E e)
{
throw std::bad_expected_access<E>(std::move(e));
}
};Code in std::expected that
currently throws
std::bad_expected_access
unconditionally (e.g. .value()),
instead calls std::expected_traits::throw_error(error()).
As no std::expected_traits
specialization exists currently, this is not a breaking change.
User code is allowed to customize
std::expected_traits for their
own error types, where
throw_error() can do whatever is
appropriate, as long as it does not return. Crucially, it does not
necessarily need to throw, but
could also std::abort() the
program instead.
Alternatively, we could introduce a marker type, let’s call it std::unexpected_exception<E, Exception>
and bikeshed later. A std::expected<T, std::unexpected_exception<E, Exception>>
behaves just like a
std::expected<T, E>, but
instead of throwing std::bad_expected_access(error()) it
throws Exception(error()).
That way, the exception associated with an error type can be customized on a per-instance basis instead of globally per error type.
std::expected_traits
take <T, E> and not
<E>?This could enable a customization based on specific value-error combinations only.
std::expected?The concept of “exception associated to an error type” seems more
general than std::expected. We
could add it as something more general, like
std::default_error_exception or
std::error_traits.
std::error_code to throw
std::system_error?It would make a lot of sense, but is unfortunately a breaking change.
It would work with the alternative design by using std::expected<T, std::expected_error<std::error_code, std::system_error>>.
.error()?Currently it is UB if there is no stored error. It could instead call something on the traits, so a user can customize it to return an error value that means “ok”.
std::expected::check() as
well?Calling .value() on a
std::expected<void, E> as
in the example is a bit awkward. Maybe we should have
.check() which does nothing if
the expected has a value, but calls std::expected_traits<E>::throw_error
if it has an error.
Thanks to JF Bastien and Jonathan Wakely for providing feedback on an initial draft of this paper.