std::variant
Document #: | P2162R0 |
Date: | 2020-05-05 |
Project: | Programming Language C++ |
Audience: |
LEWG |
Reply-to: |
Barry Revzin <barry.revzin@gmail.com> |
[LWG3052] describes an under-specification to std::visit
:
the Requires element imposes no explicit requirements on the types in
Variant
s. Notably, theVariant
s are not required to bevariant
s. This lack of constraints appears to be simply an oversight.
The original proposal [P0088R3] makes no mention of other kinds of of variants besides std::variant
, and this does not appear to have been discussed in LEWG.
The proposed resolution in the library issue is to make std::visit
only work if all of the Variant
s are, in fact, std::variant
s:
Remarks: This function shall not participate in overload resolution unless
remove_cvref_t<Variantsi>
is a specialization ofvariant
for all0 <= i < n
.
This paper suggests a different direction. Instead of restricting to just std::variant
(and certainly not wanting to go all out and design a “variant-like” interface), this paper proposes to allow an additional category of useful types to be std::visit()
-ed: those that publicly and unambiguously inherit from a specialization of std::variant
.
variant
There are two primary motivators for inheriting from std::variant
.
One is to simply extend functionality. If we’re using variant
to represent a state machine, we may want additional operations that are relevant to our state that variant
doesn’t itself provide:
struct State : variant<Disconnected, Connecting, Connected>
{
using variant::variant;
bool is_connected() const {
return std::holds_alternative<Connected>(*this);
}
friend std::ostream& operator<<(std::ostream&, State const&) {
// ...
}
};
Another may be to create a recursive variant, as in the example from [P1371R2]:
struct Expr;
struct Neg {
std::shared_ptr<Expr> expr;
};
struct Add {
std::shared_ptr<Expr> lhs, rhs;
};
struct Mul {
std::shared_ptr<Expr> lhs, rhs;
};
struct Expr : std::variant<int, Neg, Add, Mul> {
using variant::variant;
};
namespace std {
template <> struct variant_size<Expr> : variant_size<Expr::variant> {};
template <std::size_t I> struct variant_alternative<I, Expr> : variant_alternative<I, Expr::variant> {};
}
That paper even has an example of passing an Expr
to std::visit()
directly, a use-case that this paper is seeking to properly specify. It would be pretty nice if that just worked.
Note also that the example includes an explicit specialization of variant_size
and variant_alternative
that just forward along to Expr
’s base class. These specializations are pure boilerplate - they basically have to look the way they do, so they don’t really offer much in the way of adding value to the program.
The proposed resolution of LWG3052 is to, basically, add this constraint onto std::visit
:
template <typename Visitor, typename... Variants>
requires (is_specialization_of_v<remove_cvref_t<Variants>, variant> && ...)
constexpr decltype(auto) visit(Visitor&&, Variants&&) {
// as today
}
This paper proposes instead that visit
conditionally upcasts all of its incoming variants to std::variant
specializations:
template <typename Visitor, typename... Variants>
requires (is_specialization_of_v<remove_cvref_t<Variants>, variant> && ...)
constexpr decltype(auto) visit(Visitor&&, Variants&&) {
// as today
}
template <typename... Ts>
constexpr auto variant_cast(std::variant<Ts...>& v) -> std::variant<Ts...>& {
return v;
}
template <typename... Ts>
constexpr auto variant_cast(std::variant<Ts...> const& v) -> std::variant<Ts...> const& {
return v;
}
template <typename... Ts>
constexpr auto variant_cast(std::variant<Ts...>&& v) -> std::variant<Ts...>&& {
return std::move(v);
}
template <typename... Ts>
constexpr auto variant_cast(std::variant<Ts...> const&& v) -> std::variant<Ts...> const&& {
return std::move(v);
}
template <typename Visitor, typename... Variants>
constexpr decltype(auto) visit(Visitor&& vis, Variants&&... vars) {
return visit(std::forward<Visitor>(vis),
variant_cast(std::forward<Variants>(vars))...);
}
This means the body of std::visit
for implementations can remain unchanged - it, as today, can just assume that all the variants are indeed std::variant
s. Such an implementation would allow visitation of the State
and Expr
examples provided earlier.
Now, this means we can std::visit
a variant that we can’t even invoke std::get
or std::get_if
on, such as with this delightful type courtesy of Tim Song:
struct MyEvilVariantBase {
int index;
char valueless_by_exception;
};
struct MyEvilVariant : std::variant<int, long>, std::tuple<int>, MyEvilVariantBase { };
But… who cares. Don’t write types like that.
The libc++ implementation has supported this design since day one [libcpp].
The Microsoft STL implementation also already supports this design [stlstl] since the first Visual Studio 2019 release in April 2019.
Change 20.7.7 [variant.visit]:
-2 Let
as-variant
denote the exposition-only function templateLet
n
besizeof...(Variants)
. For each0 <= i < n
, letVi
denote the the typeremove_cvref_t<decltype(as-variant(varsi))>
.-1 Constraints:
Vi
is a valid type for all0 <= i < n
.0 Let
VRi
denote the typeVi
with the addition ofVarianti
’s cv and reference qualifiers. LetVR
denote the pack of typesVRi
.1
LetLetn
besizeof...(Variants)
.m
be a pack of n values of typesize_t
. Such a pack is called valid if0 <= mi < variant_size_v<
for allremove_reference_t<Variantsi>Vi>0 <= i < n
. For each valid packm
, lete(m)
denote the expression:for the first form and
for the second form.
2 Mandates: For each valid pack
m
,e(m)
is a valid expression. All such expressions are of the same type and value category.3 Returns:
e(m)
, wherem
is the pack for whichmi
isvarsi.index()
as-variant(varsi).index()
for all0 <= i < n
. The return type isdecltype(e(m))
for the first form.4 Throws:
bad_variant_access
ifanyifvariant
invars
isvalueless_by_exception()
(as-variant(vars).valueless_by_exception() || ...)
istrue
.5 Complexity: For
n <= 1
, the invocation of the callable object is implemented in constant time, i.e., forn=1
, it does not depend on the number of alternative types ofVariants0
V0
. Forn>1
, the invocation of the callable object has no complexity requirements.
This paper proposes to bump the value __cpp_lib_variant
. The macro already exists, so this is, in a sense, free. And users can use the value of this macro to avoid having to specialize variant_size
and variant_alternative
for their inherited variants.
Thanks to Casey Carter, Ville Voutilainen, and the unfortunately non-alliterative Tim Song for design discussion and help with the wording.
[libcpp] Michael Park. 2017. libc++ variant.
https://github.com/llvm/llvm-project/blob/24b4965ce65b14ead595dcc68add22ba37533207/libcxx/include/variant#L455
[LWG3052] Casey Carter. visit is underconstrained.
https://wg21.link/lwg3052
[P0088R3] Axel Naumann. 2016. Variant: a type-safe union for C++17 (v8).
https://wg21.link/p0088r3
[P1371R2] Sergei Murzin, Michael Park, David Sankel, Dan Sarginson. 2020. Pattern Matching.
https://wg21.link/p1371r2
[stlstl] Microsoft. 2019. stlstl variant.
https://github.com/microsoft/STL/blob/65d98ffabab3a95d79255f741daa1230692e8066/stl/inc/variant#L1638-L1657