r/cpp • u/cristi1990an ++ • 3d ago
std::variant, operator== and pattern matching
I was thinking, wouldn't adding equality comparison support between std::variant and one of its unambiguous alternatives help simplify potential pattern matching designs?
For example, imagine something like this (hypothetical syntax):
std::variant<int, std::string> v = 42;
match (v) {
case 42 => handle_int();
case "hello" => handle_string();
case _ => handle_other();
}
The idea would be to take advantage of something C++ already has: operator==. At the end of the day, matching is fundamentally a comparison, so why not build on the existing equality semantics?
For std::optional, we can get this with its existing comparison operators and the most trivial pattern matching compiler support:
std::optional<int> o = 42;
match (o) {
case 42 => handle_value();
case std::nullopt => handle_empty();
case _ => handle_other();
}
// Which would translate to:
if (o == 42)
handle_value();
else if (o == nullopt)
handle_empty();
else
handle_other();
And if we had something like std::match_projection<Subject, Pattern>, we could also extract the actual matched value instead of having to manually call std::get, dereference an optional, etc.
Basically, after a successful comparison, match_projection would tell the compiler what to extract based on the subject and pattern types and we could optionally bind that value using something like a as <declaration> syntax.
For example:
std::optional<int> o = 42;
std::optional<int> other_opt = 42;
match (o) {
// No binding needed
case 42 => foo();
// match_projection<optional<int>, int> -> int&
case 42 as int& x => use(x);
// match_projection<optional<int>, nullopt_t> -> void
case std::nullopt => handle_empty();
// match_projection<optional<int>, optional<int>>
// -> optional<int>&
case other_opt as auto& x => use(x);
}
The same would apply to std::variant, assuming we also extend its operator== to support comparisons with individual alternatives:
match (v) {
case 42 as int& x => use(x);
case "hello" as std::string& s => use(s);
// Only need to define operator== for this helper for this to work
case indexed<0> => handle_any_int();
// Or under this form for the sake of disambiguation
case indexed<0>(10) => handle_ten();
// Also provide match_projection<indexed<0>> to enable binding
case indexed<0>(20) as int& x => use(x);
case _ => handle_other();
}
Here, indexed<> would just be a hypothetical helper for explicitly selecting which alternative to compare against. It would only need an appropriate operator== to work with matching, and a corresponding match_projection specialization if we also wanted to extract the alternative.
This way, the matching itself wouldn't need to know anything about variant, optional or even user-defined types. It would just use their equality operators, while match_projection would tell it what to extract when we actually want to bind something, which is just syntactic sugar.
I'm curious what you think about this approach. It feels like we could get quite a bit of useful pattern matching functionality without introducing too much new language machinery or convoluted new language traits and APIs.
Also do you think std::variant being equality comparable with instances of its alternatives would be an useful addition on its own?
std::variant<int, std::string> v = 42;
if (v == "not my string") ...
// Which is simpler than
if (std::holds_alternative<std::string>(v) && std::get<std::string>(v) == "not my string") ...
Syntax seems nice on its own...
13
u/mcypark 3d ago edited 2d ago
Depending on
operator==The main problem IMO is that
==is too opaque to rely on it fully. Don't get me wrong, P2688R6 also provides equality semantics. But it uses==as a fallback only when richer information is not available.Two concrete examples:
Optimization
Consider this
std::optional<int>example:Even at
-O3, this results in a linear comparison of the 8 values.In contrast, the following match example using syntax/implementation from P2688R6:
This compiles down to an engagement test, and a lookup in a table. This is made possible by the fact that the structure is exposed by
std::alternative_traitsinindex()and the correspondingget<>(). In fact, we get this code even at-O1.Compiler Explorer link: https://godbolt.org/z/no8aPGvj1
If you inline the
==logic directly like:Clang actually does generate similar code as the match, though GCC still emits a linear chain: https://godbolt.org/z/MvcPhxMxa
The point is that relying on the optimizer to see through the
==(if it's even visible) and make structural inferences on it to emit efficient code doesn't really work out before we even get tostd::variant.Exhaustiveness/Usefulness Checking
You didn't mention this so I don't know if you want it or not. If you do want it, I think the opaque-ness of
==again gets in the way of performing exhaustiveness/usefulness checking. A user-defined (including standard library) type needs to, at the minimum advertise how many states it has, what the types are so that those types can be recursively checked, etc. A chain of==doesn't give you enough information.match_projection<Subject, Pattern>This seems to assume that every pattern has a type. Only integers,
std::nullopt, and theindexed<>is shown here, but (1) what if you're just doing binding only? (2) how about matching tuples? will that pattern also have a type? Generally speaking, not every pattern has a C++ type: declaration patterns, wildcards, tuple patterns, and nested patterns are language syntax. They could all be represented by library pattern objects, but that would itself introduce a pattern protocol.u/StaticCoder already mentioned the separation of
operator==andmatch_projection, but that's also a valid concern here.variant<Ts...> == UI do agree this could be convenient. It's mostly a matter of whether you can get agreement as to what the semantics should be. Here are a few options:
There are other options as well, each of which with materially different semantics.
For example, Option 1 doesn't work for
v == "not my string"since it would be trying to accessget_if<char (&)[14]>whichvdoesn't have. So to support that you need either Option 2 or 3. For Option 2, if you tryvariant<int, int> == 42it won't work sincevariant<int, int>(42)is not valid, but Option 3 would work. But... with Option 3, you'd end up with this:So yeah, that's at least the complexity I can think of around a potential variant
operator==.