Pattern matching is a commonly used idiom in other contemporary programming languages, such as Rust or Haskell, and is a way to inspect and deconstruct value(s) based on their type(s). Unlike destructuring in C++, which takes a single compound type and extracts the fields as separate variables, pattern matching is a way to deal with conditional logic based upon the type(s) involved. In this article we’ll be taking a look into the future (none of the code featured in this article compiles with trunk Clang or GCC at the time of writing) and consider what implications an adoption of this feature into the Standard might have for Modern C++.
Pattern matching will allow for more natural coding styles by specifying the desired types or values and how to handle them; at present we need to use chains of if–else statements or switch to achieve this in program logic. As we know, switch has the limitation of only being able to make a decision based upon an integral value (up to 64-bits wide), while the proposed new keyword match will be far more flexible and need much less boilerplate code.
The proposal for C++ is based around the following pattern matching concepts:
- Patterns are expressions which describe the structure or type of the value being matched. In the case of
match(x),xwould be compared against various patterns, and in the case of a match, an action would be taken. - Guards are optional additional constraints upon a match, possibly checking it falls within a valid range of values for the given type.
- Deconstruction (sometimes known as destructuring) is where element fields (such as those in a
std::pairorstd::tuple) are extracted into individual variables for use within the action. - Dynamic type-based matching allows for polymorphic types such as
std::variantto be catered for, as an alternative to usingstd::visit.
The general layout of a match statement has similarities to switch, reusing the keywords case, break and default. Due to the fact C++ is a statically typed language, the type of the match pattern must be known at compilation time, so it is in many ways a compile-time feature.
auto value = some_function(); // may use if constexpr to choose return type
match (value) {
case int i:
std::cout << "Got an integer: " << i << "\n";
break;
case std::string s:
std::cout << "Got a string: " << s << "\n";
break;
case std::tuple<int, std::string> t:
std::cout << "Got a tuple with int and string\n";
break;
default:
std::cout << "Unmatched type\n";
break;
}
In the above code, we check for three different types, int, std::string and std::tuple<int,std::string>, outputting the value in case of a match (or a diagnostic message in case of failure to match). In addition to the form case <type> <variable_name>: it is possible to use combinations of several alternatives:
1. Match a type and exact value: case <value>:
match (value) {
case 42: // value is an int and equal to 42
std::cout << "You have found the answer!\n";
break;
}
2. Match a type and predicate: case <type> <variable_name> if <boolean>:
match (value) {
case std::string s if s.starts_with("<!DOCTYPE"):
std::cout << "Found a webpage.\n";
break;
}
3. Match a composite type and destructure elements: case <type>(field_type field_name, ...):
std::tuple<float, std::string> t = std::make_tuple(1.5f, "Overtime rate");
match (t) {
case std::tuple<float, std::string>(float x, std::string s):
std::cout << "Number: " << x << ", String: " << s << '\n';
}
It should be emphasised that the syntax and implementation details are not yet finalized, so it will likely take some time before we can use pattern matching in production compilers. If adopted into the Standard, it will provide the advantages of cleaner, more expressive and safer code, with the type-safety and compile-time checking that Modern C++ provides.