std::expected
Document #: | P3527R0 |
Date: | 2024-12-17 |
Project: | Programming Language C++ |
Audience: |
Evolution Library Evolution |
Reply-to: |
Michael Park <mcypark@gmail.com> Zach Laine <whatwasthataddress@gmail.com> |
[P2688R3] introduces pattern matching for
C++, including handling of
std::variant
s.
For example:
auto v = std::variant<int32_t, int64_t, float, double>{/* ... */};
match {
v int32_t: let i32 =>
::print("got int32: {}", i32);
stdint64_t: let i64 =>
::print("got int64: {}", i64);
stdfloat: let f =>
::print("got float: {}", f);
stddouble: let d =>
::print("got double: {}", d);
std};
However, there is an unsupported type in the standard that is
variant-like but is not covered by [P2688R3] without explicit library
support. That type is std::expected
.
This proposal does not interact with the pattern matching language
feature, nor does it interact with user defined types; it only affects
whether std::expected
interoperates gracefully with the language feature.
This paper adds library facilities that allow std::expected
to
conform to the notion of variant-like used in the pattern matching core
wording. In order to accomplish this, we propose to do three things:
std::expected
to
the variant-like concept.std::visit
to handle variant-like types (optional).This has the side effect that users will now be able to use
std::visit
in a generic context to visit alternatives of both
std::variant
(as is the status quo) and std::expected
(via
the changes in this paper).
The language semantics for pattern matching ([P2688R3]) include the notion of a “variant protocol”. This is analogous to the tuple protocol used for structured bindings. The tuple protocol is used in defining the behavior of pattern matching as well.
This section does not describe a library change, and is not part of this proposal. It is here to describe the relevant language part of the pattern matching design, since that language design informs the library design in this paper.
There are two options for exactly how to define the variant-like concept.
This is what the tuple-like concept does. The standard says,
“A type T
models and satisfies the
exposition-only concept tuple-like if remove_cvref_t<T>
is a specialization of array
,
complex
,
pair
,
tuple
, or ranges::subrange
.”
So, Option 1 would be to say nearly the same thing: “A type
T
models and satisfies the
exposition-only concept variant-like if remove_cvref_t<T>
is a specialization of expected
or
variant
.”
This has the benefit of being consistent with how tuple-like is defined.
It has the disadvantage of being a maintenance burden. Future library
additions of variant
-like templates
must remember to update the wording for variant-like.
Another way to define variant-like would be to provide
wording that applies to
std::variant
and std::expected
,
which is designed to be general enough to encompass similar types.
This has the benefit of being more maintainable.
This definition also has the disadvantage of novelty, and specifically of not having more than two templates that model it. This means that it might not work just to add new templates without adjusting variant-like anyway, for reasons we can not anticipate now.
std::visit
Modifying
std::visit
to handle variant-like types is optional. It seems useful to be
able to use
std::visit
for std::expected
s,
since they are gaining variant-like status. However, modifying
std::visit
will not be directly used by the pattern matching language rules. In
fact, once pattern matching is available, the use cases for
std::visit
will be far rarer. This addition is something that LEWG should poll
separately from the rest of the paper.
The idea of the modification is to change the names of all the
exposition-only as-variant overload set described in
22.6.7
[variant.visit] to
as-variant-like, change
std::visit
to use as-variant-like, then add these four new overloads:
template<class T, class E>
constexpr auto&& as-variant-like(expected<T, E>& var) { return var; }
template<class T, class E>
constexpr auto&& as-variant-like(const expected<T, E>& var) { return var; }
template<class T, class E>
constexpr auto&& as-variant-like(expected<T, E>&& var) { return std::move(var); }
template<class T, class E>
constexpr auto&& as-variant-like(const expected<T, E>&& var) { return std::move(var); }
We would also need to add overloads of the GET exposition-only function defined in 22.6.5 [variant.get]:
template<size_t I, class... Types>
constexpr variant_alternative_t<I, variant<T, E>>&
(expected<T, E>& v);
GETtemplate<size_t I, class... Types>
constexpr variant_alternative_t<I, variant<T, E>>&&
(expected<T, E>&& v);
GETtemplate<size_t I, class... Types>
constexpr const variant_alternative_t<I, variant<T, E>>&
(const expected<T, E>& v);
GETtemplate<size_t I, class... Types>
constexpr const variant_alternative_t<I, variant<T, E>>&&
(const expected<T, E>&& v); GET
Add a new section [variant.like] after 22.6.2 [variant.syn]:
+ template<class T>
+ concept variant-like = see below; // exposition only
Then, define variant-like as outlined in Option 1:
1
A type T
models and satisfies
the exposition-only concept variant-like if
remove_cvref_t<T>
is a
specialization of expected
or
variant
.
… or define variant-like as outlined in Option 2:
1
A type T
models and satisfies
the exposition-only concept variant-like if
remove_cvref_t<T>
is a
specialization of expected
or
variant
.
Add a new member function index
to 22.8.6.1
[expected.object.general]
namespace std {
template<class T, class E>
class expected {
public:
...
// [expected.object.obs], observers
...
template<class U> constexpr T value_or(U&&) const &;
template<class U> constexpr T value_or(U&&) &&;
template<class G = E> constexpr E error_or(G&&) const &;
template<class G = E> constexpr E error_or(G&&) &&;+ constexpr size_t index() const noexcept;
...
}; }
Add a new member function index
to 22.8.6.6
[expected.object.obs]
+ constexpr size_t index() const noexcept;
26
Returns:
has_value() ? 0 : 1
.
Add a new member function index
to 22.8.7.1
[expected.void.general]
template<class T, class E> requires is_void_v<T>
class expected {
public:
...
// [expected.void.obs], observers
...
template<class G = E> constexpr E error_or(G&&) const &;
template<class G = E> constexpr E error_or(G&&) &&;+ constexpr size_t index() const noexcept;
... };
Add a new member function index
to 22.8.7.6
[expected.void.obs]
+ constexpr size_t index() const noexcept;
15
Returns:
has_value() ? 0 : 1
.
Add a new section [expected.asvariant] after 22.8.7.8 [expected.void.eq]:
+ template<class T, class E>
+ struct variant_size<expected<T, E>> : integral_constant<size_t, 2> { };
+
+ template<size_t I, class T, class E>
+ struct variant_alternative<I, expected<T, E>> {
+ using type = see below ;
+ };
+ template<size_t I, class T, class E>
+ constexpr variant_alternative_t<I, expected<T, E>>& get(expected<T, E>& e);
+ template<size_t I, class T, class E>
+ constexpr variant_alternative_t<I, expected<T, E>>&& get(expected<T, E>&& e);
+ template<size_t I, class T, class E>
+ constexpr const variant_alternative_t<I, expected<T, E>>& get(const expected<T, E>& e);
+ template<size_t I, class T, class E>
+ constexpr const variant_alternative_t<I, expected<T, E>>&& get(const expected<T, E>&& e);
3
Mandates: I < 2
.
4
Throws:
bad_variant_access
if
e.index() != I
.
e.index()
is not
I
, throw an exception of type
bad_variant_access
.*e
if
has_value()
is
true
, and
e.error()
otherwise.expected
overloads of the
GET exposition-only function.
template<size_t I, class... Types>
constexpr variant_alternative_t<I, variant<Types...>>&
GET(variant<Types...>& v); // exposition only
template<size_t I, class... Types>
constexpr variant_alternative_t<I, variant<Types...>>&&
GET(variant<Types...>&& v); // exposition only
template<size_t I, class... Types>
constexpr const variant_alternative_t<I, variant<Types...>>&
GET(const variant<Types...>& v); // exposition only
template<size_t I, class... Types>
constexpr const variant_alternative_t<I, variant<Types...>>&& GET(const variant<Types...>&& v); // exposition only
3
Mandates: I < sizeof...(Types)
.
4
Preconditions: v.index()
is I
.
5
Returns: A reference to the object stored in the
variant
.
template<size_t I, class T, class E>
constexpr variant_alternative_t<I, expected<T, E>>&
GET(expected<T, E>& v); // exposition only
template<size_t I, class T, class E>
constexpr variant_alternative_t<I, expected<T, E>>&&
GET(expected<T, E>&& v); // exposition only
template<size_t I, class T, class E>
constexpr const variant_alternative_t<I, expected<T, E>>&
GET(const expected<T, E>& v); // exposition only
template<size_t I, class T, class E>
constexpr const variant_alternative_t<I, expected<T, E>>&& GET(const expected<T, E>&& v); // exposition only
3
Mandates: I < 2
.
4
Preconditions:
v.index()
is
I
.
5
Returns: A reference to the object stored in the
expected
.
expected
.1
Let as-variantas-variant-like denote
the following exposition-only function templates:
- template<class... Ts>
- constexpr auto&& as-variant(variant<Ts...>& var) { return var; }
- template<class... Ts>
- constexpr auto&& as-variant(const variant<Ts...>& var) { return var; }
- template<class... Ts>
- constexpr auto&& as-variant(variant<Ts...>&& var) { return std::move(var); }
- template<class... Ts>
- constexpr auto&& as-variant(const variant<Ts...>&& var) { return std::move(var); }
+ template<class... Ts>
+ constexpr auto&& as-variant-like(variant<Ts...>& var) { return var; }
+ template<class... Ts>
+ constexpr auto&& as-variant-like(const variant<Ts...>& var) { return var; }
+ template<class... Ts>
+ constexpr auto&& as-variant-like(variant<Ts...>&& var) { return std::move(var); }
+ template<class... Ts>
+ constexpr auto&& as-variant-like(const variant<Ts...>&& var) { return std::move(var); }
+ template<class T, class E>
+ constexpr auto&& as-variant-like(expected<T, E>& var) { return var; }
+ template<class T, class E>
+ constexpr auto&& as-variant-like(const expected<T, E>& var) { return var; }
+ template<class T, class E>
+ constexpr auto&& as-variant-like(expected<T, E>&& var) { return std::move(var); }
+ template<class T, class E>
+ constexpr auto&& as-variant-like(const expected<T, E>&& var) { return std::move(var); }
Let n be sizeof...(Variants)
.
For each 0
≤
i < n, let
V
i denote the
type decltype(
as-variantas-variant-like(std::forward<Variants
i>(vars
i)))
.
2
Constraints: V
i
is a valid type for all
0
≤
i < n.
3
Let V
denote the pack of types
V
i.
4
Let m be a pack of n values of type
size_t
. Such
a pack is valid if 0 ≤ mi < variant_size_v<remove_reference_t<V
i>>
for all 0 ≤ i < n. For each valid pack m,
let e(m) denote the expression:
INVOKE(std::forward<Visitor>(vis), GET<m>(std::forward<V>(vars))...) // see [func.require]
for the first form and
INVOKE<R>(std::forward<Visitor>(vis), GET<m>(std::forward<V>(vars))...) // see [func.require]
for the second form.
5 Mandates: For each valid pack m, e(m) is a valid expression. All such expressions are of the same type and value category.
6
Returns: e(m), where m is the pack
for which mi is as-variantas-variant-like(vars
i).index()
for all 0
≤
i < n. The return type is decltype(
e(m))
for the first form.
7
Throws: bad_variant_access
if (
as-variantas-variant-like(vars).valueless_by_exception() || ...)
is true
.