title | document | date | audience | author | toc | toc-depth | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Pattern Matching: *variant-like* and `std::expected` |
P3527R0 |
today |
|
|
true |
4 |
[@P2688R3] introduces pattern matching for C++, including handling of
std::variant
s. For example:
auto v = std::variant<int32_t, int64_t, float, double>{/* ... */};
v match {
int32_t: let i32 =>
std::print("got int32: {}", i32);
int64_t: let i64 =>
std::print("got int64: {}", i64);
float: let f =>
std::print("got float: {}", f);
double: let d =>
std::print("got double: {}", d);
};
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:
- Introduce the variant-like exposition-only concept.
- Add
std::expected
to the variant-like concept. - Modify
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.
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 [variant.visit]{.sref} 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 [variant.get]{.sref}:
template<size_t I, class... Types>
constexpr variant_alternative_t<I, variant<T, E>>&
@*GET*@(expected<T, E>& v);
template<size_t I, class... Types>
constexpr variant_alternative_t<I, variant<T, E>>&&
@*GET*@(expected<T, E>&& v);
template<size_t I, class... Types>
constexpr const variant_alternative_t<I, variant<T, E>>&
@*GET*@(const expected<T, E>& v);
template<size_t I, class... Types>
constexpr const variant_alternative_t<I, variant<T, E>>&&
@*GET*@(const expected<T, E>&& v);
Add a new section [variant.like] after [variant.syn]{.sref}:
+ template<class T>
+ concept @*variant-like*@ = @*see below*@; // exposition only
Then, define variant-like as outlined in Option 1:
::: add
[1]{.pnum} 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:
::: add
[1]{.pnum} 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 [expected.object.general]{.sref}
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 [expected.object.obs]{.sref}
+ constexpr size_t index() const noexcept;
::: add
[26]{.pnum} Returns: has_value() ? 0 : 1
.
:::
Add a new member function index
to [expected.void.general]{.sref}
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 [expected.void.obs]{.sref}
+ constexpr size_t index() const noexcept;
::: add
[15]{.pnum} Returns: has_value() ? 0 : 1
.
:::
Add a new section [expected.asvariant] after [expected.void.eq]{.sref}:
+ 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*@ ;
+ };
::: add
[1]{.pnum} Mandates: I < 2
[2]{.pnum} Type: The type T
if I
is 0
, otherwise the type 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 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);
::: add
[3]{.pnum} Mandates: I < 2
.
[4]{.pnum} Throws: bad_variant_access
if e.index() != I
.
- If
e.index()
is notI
, throw an exception of typebad_variant_access
. - Otherwise, return
*e
ifhas_value()
istrue
, ande.error()
otherwise. :::
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]{.pnum} Mandates: I < sizeof...(Types)
.
[4]{.pnum} Preconditions: v.index()
is I
.
[5]{.pnum} Returns: A reference to the object stored in the variant
.
::: add
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]{.pnum} Mandates: I < 2
.
[4]{.pnum} Preconditions: v.index()
is I
.
[5]{.pnum} Returns: A reference to the object stored in the expected
.
:::
In [variant.visit]{.sref} Change the name of the exposition-only as-variant to as-variant-like, and add overloads for expected
.
[1]{.pnum} Let [as-variant]{.rm}[as-variant-like]{.add} 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-variant]{.rm}[as-variant-like]{.add}(std::forward<Variants
i>(vars
i)))
.
[2]{.pnum} Constraints: V
i is a valid type for all 0
≤ i < n.
[3]{.pnum} Let V
denote the pack of types V
i.
[4]{.pnum} 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]{.pnum} Mandates: For each valid pack m, e(m) is a valid expression. All such expressions are of the same type and value category.
[6]{.pnum} Returns: e(m), where m is the pack for which mi is [as-variant]{.rm}[as-variant-like]{.add}(vars
i).index()
for all 0
≤ i < n. The return type is decltype(
e(m))
for the first form.
[7]{.pnum} Throws: bad_variant_access
if (
[as-variant]{.rm}[as-variant-like]{.add}(vars).valueless_by_exception() || ...)
is true
.