Document #: | P3521R0 |
Date: | 2024-12-17 |
Project: | Programming Language C++ |
Audience: |
Evolution Library Evolution |
Reply-to: |
Michael Park <mcypark@gmail.com> Zach Laine <whatwasthataddress@gmail.com> |
This is an accompanying paper for [P2688R3] with respect to customization points in the library. At the Wrocław meeting in November 2024, the following poll was taken in EWG:
Poll: [P2688R3] — Pattern Matching:
match
Expression, we encourage more work on the language-only paper towards C++26 in the next meeting (note: voting against this poll does not exclude getting pattern matching in C++29)
SF
|
F
|
N
|
A
|
SA
|
---|---|---|---|---|
17 | 16 | 6 | 1 | 9 |
The “language-only paper” still relies on library facilities, the same way that structured bindings is a “language-only” feature that relies on library facilities as a hook for customization.
A Reflection-based Tuple-like and Variant-like Customization Points has been explored, but the design decision for [P2688R3] targeting C++26 is to use the existing facilities.
This makes it such that even a “language-only paper” is plenty useful without any library changes. However, there is one customization point that is currently missing: the ability for user-defined types to opt-in as an open sum type.
This paper proposes to add that customization point.
First, definitions. A “sum type”
SUM
is a type that represents one of
several types in the set S
=
{T0
,
T1
, …
TN
}. Which
Ti
SUM
represents is a runtime
property.
The set of types S
is part of the
definition of SUM
, but the exact
elements of S
might not be known at
the time SUM
is defined. For
instance: for std::variant<int, double>
,
S
= {int, double}
;
however, for
std::any
,
S
= {x | x is a copyable C++ type}
.
Though we know that
std::any
can
hold any copyable type, we don’t know all the copyable types that might
exist in a program.
If all the elements S
are known
at the time that SUM
is defined, we
say that SUM
is a “closed” sum type
(e.g. std::variant<int, double>
).
If the members of the set are not known at the time
SUM
is defined, we say that
SUM
is an “open” sum type
(e.g. std::any
).
[P2688R3] proposes an alternative pattern which has syntax like the following:
int f(const std::variant<int, double>& v) {
return v match {
int: let i => i;
double: let d => int(d);
};
}
This uses the “variant-like” protocol which uses the existing set of
variant helpers: variant_size
,
variant_alternative
,
get
, and
index
.
However, for open sum types, there is no such existing facility.
std::any
and
std::exception_ptr
are examples of such types.
C++23
|
P2688 with This Paper
|
---|---|
|
|
[P2927R2] mentions a desired pattern
matching use case for std::exception_ptr
.
The example is written with [P1371R3], but the following is what it
would look like with [P2688R3]:
C++23
|
P2688 with This Paper
|
---|---|
|
|
This paper proposes try_cast
as
the customization point that pattern matching invokes to test and
extract the state of open sum types.
The Alternative Pattern as proposed in [P2688R3] considers the following 3 cases in order:
This is modeled similarly to structured bindings protocol, which considers the following 3 cases:
The cast protocol is the only one that needs library support for
std
entities to benefit from it.
The proposal is to add try_cast
overloads that handle casts of
std::any
using std::any_cast
, and
casts of std::exception_ptr
using std::exception_ptr_cast
from P2927R2.
This works because the language feature itself uses
try_cast
if available. Casts of
user-defined types using calls to
try_cast
found via ADL, will also
work, such as:
namespace ns {
struct Widget { /* ... */ };
template <typename T>
const T* try_cast(const Widget& w) noexcept {
return // ...
};
}
Given an example like:
match {
subject : subpattern => // ...;
type=> // ...;
_ };
Conceptually: try_cast<type>(subject)
is invoked, which returns an optional result that can be tested and
dereferenced to match the subpattern.
More precisely: Let E
be try_cast<type>(subject)
.
The call to try_cast
is done using
ADL-only lookup, just as is done with calls to
get
for structured bindings (see
9.6
[dcl.struct.bind]).
The match is well-formed if and only if:
E
is well-formed;E
is contextually convertible to
bool
;
andE
is dereferencable.A well-formed match succeeds if and only if
E
contextually converts to
true
and
*E
matches
subpattern.
<any>
synopsis
template<class T>
T any_cast(const any& operand);
template<class T>
T any_cast(any& operand);
template<class T>
T any_cast(any&& operand);
template<class T>
const T* any_cast(const any* operand) noexcept;
template<class T>
T* any_cast(any* operand) noexcept;
+ template<class T, same_as<any> Any>
+ const T* try_cast(const Any& operand) noexcept;
+ template<class T, same_as<any> Any>
+ T* try_cast(Any& operand) noexcept;
+ template<class T, same_as<any> Any>
+ T* try_cast(Any&& operand) noexcept;
+ template<class T, same_as<any> Any>
+ const T* try_cast(const Any& operand) noexcept;
+ template<class T, same_as<any> Any>
+ T* try_cast(Any& operand) noexcept;
+ template<class T, same_as<any> Any>
+ T* try_cast(Any&& operand) noexcept;
Mandates:
is_void_v<T>
isfalse
.Returns:
any_cast<T>(&operand)
<exception>
synopsis
exception_ptr current_exception() noexcept;
[[noreturn]] void rethrow_exception(exception_ptr p);
template <class E>
const E* exception_ptr_cast(const exception_ptr& p) noexcept;
template <class T> [[noreturn]] void throw_with_nested(T&& t);
+ template<class E>
+ const E* try_cast(const exception_ptr& p) noexcept;
+ template<class E>
+ const E* try_cast(const exception_ptr& p) noexcept;
Mandates:
E
is a cv-unqualified complete object type.E
is not an array type.E
is not a pointer or pointer-to-member type.Returns:
exception_ptr_cast<E>(p)
any_cast
Adding new overloads of any_cast
was considered as a way to opt-in as an any-like type. Effectively
designating variant-like as the closed sum type opt-in, and any-like as
the open sum type opt-in.
The reason this approach is not taken is purely technical. The
current overloads of any_cast
include versions that takes any
by
reference. Since any
is implicitly
constructible from anything, testing the validity of
any_cast
is virtually always
true.
It was considered to take the parameters by pointer instead to avoid
the implicit construction of any
from anything problem:
namespace std {
template <typename T>
const T* try_cast(const any* a) noexcept { return any_cast<T>(a); }
template <typename T>
* try_cast(any* a) noexcept { return any_cast<T>(a); }
T
template <typename T>
const T* try_cast(const exception_ptr* p) noexcept {
if (!p) return nullptr;
return exception_ptr_cast<T>(*p); // P2927R2
}
}
This solves the problem of having to specify same_as<any>
constraint, but it makes the rest of the overloads less obvious and
familiar.
std::hash
-like
ApproachThis approach is to make try_cast
be a class template that users specialize, rather than an overload
set.
namespace std {
template <typename>
struct try_cast; // undefined
template <>
struct try_cast<std::any> {
template <typename T>
static const T* to(const std::any& a) noexcept {
return std::any_cast<T>(&a);
}
template <typename T>
static T* to(std::any& a) noexcept {
return std::any_cast<T>(&a);
}
};
template <>
struct try_cast<std::exception_ptr> {
template <typename T>
static const T* to(const std::exception_ptr& p) noexcept {
return std::exception_ptr_cast<T>(p); // P2927R2
}
};
}
This allows the parameters to become references again. There are two downsides of this approach:
std
namespace in order to provide
the specialization.std::try_cast<From>::to<To>(from)
,
compared to the proposed API which would be simply try_cast<To>(from)
.try_cast
CPOThis approach was seriously considered by the authors, but discarded.
We wrote wording for it and everything. While it may be useful for users
in general, it is a terrible fit for use by a language feature. The CPO
would have to live in a particular header. That header would then need
to be included wherever a
type:
pattern was applied to an open sum type.
In the general case, the user would have no obvious way of noticing
that this requirement was not met. They would just get a diagnostic
telling them to
#include
the
right header. Users already have to deal with this occasionally when the
compiler informs them that they forgot to include <initializer_list>
.
Note that the tuple protocol uses direct calls to the overloads that it requires for producing structured bindings, and this decision is in line with that. In any case, the decision not to use a CPO is already made; it’s part of the language feature.