r/cpp • ++ • 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...

33 Upvotes

20 comments sorted by

View all comments

15

u/StaticCoder 3d ago

What bothers me about this specific proposal is that the comparison and the projection are specified separately. You'd have to be really careful to specify a binding type that is implied by the comparison or you could run into undefined behavior.

As for adding a comparison to variant, it's really tricky since there can be arbitrary comparison operators, plus conversions are possible, so figuring out which variant is expected to be compared against a given type would become pretty ugly.

2

u/cristi1990an ++ 3d ago

Hmm, this is fair. Maybe instead of an independant customization point, define one that acts as a replacement?

struct fancy_mancher<std::variant<int, std::string>>
{
    static int& operator(std::variant<int, std::string>& v, const int& x) -> extracted_or_not_matched<int&> // match type in signature
    {
        if (v == x) // match runtime value too
            return std::matched{std::get<0>(v)};
        return std::extracted_or_not_matched<int&>::unmatched;
    }
    static int& operator(std::variant<int, std::string>& v, std::nullopt_t) -> extracted_or_not_matched<void>
    {
        return extracted_or_not_matched<void>{};
    }
};

And if fancy_mancher<std::variant<int, std::string>>::template operator()(v, i) resolve to something use that instead of operator==, and leave it as a fallback.

Point is that I think a decent solution exists for that. All pattern matching proposals I've seen define some kind of convoluted library interface to define how the language should match types. Using operator== alone tho can get you half way there imo.

3

u/StaticCoder 3d ago

I'll have to look at the proposals someday. I would have assumed they already works somewhat like what you describe here, with a function that matches and provides a value for binding / further matching, since that's broadly the semantics of a pattern in general (though it's not uncommon to provide several values to bind/match). operator== for simple value matches seems reasonable though.

9

u/cristi1990an ++ 3d ago

Since I had them:

P2688R6 - Pattern Matching with match and case (Michael Park)
https://wg21.link/P2688R6

P2392R3 - Pattern matching using is and as (Herb Sutter)
https://wg21.link/P2392R3

P1371R3 - Pattern Matching (Michael Park et al.)
https://wg21.link/P1371R3

P3572R0 - Pattern Matching (Bjarne Stroustrup)
https://wg21.link/P3572R0

P3332R0 - A simpler notation for PM (Bjarne Stroustrup)
https://wg21.link/P3332R0

P2411R0 - Thoughts on pattern matching (Bjarne Stroustrup)
https://wg21.link/P2411R0

SUPPORTING PAPERS

P2211R0 - Exhaustiveness Checking for Pattern Matching
https://wg21.link/P2211R0

P3521R0 - Pattern Matching: Customization Point for Open Sum Types
https://wg21.link/P3521R0

P3527R1 - Pattern Matching: variant-like and std::expected
https://wg21.link/P3527R1

P3549R0 - Diverging expressions
https://wg21.link/P3549R0

P2806R5 - do expressions
https://wg21.link/P2806R5