Pattern matching
C++ does not have structural pattern matching as a language feature. The closest mechanisms are switch (limited to integer-comparable values, as in C); if constexpr for compile-time discrimination on types; std::visit for runtime discrimination on std::variant; dynamic_cast for runtime type discrimination on inheritance hierarchies; and if-let-style decomposition through structured bindings combined with conventional if checks. A standardised pattern-matching syntax has been proposed for several revisions of the language and is expected to land in a future revision; until then, the patterns documented here are how working C++ approximates the construct.
switch for integer dispatch
switch discriminates an integer-typed expression against integer-constant labels. The treatment is essentially the same as in C, with the C++17 switch-with-initialiser form and the [[fallthrough]] attribute as the principal additions:
switch (auto e = next_event(); e.kind) {
case EventKind::Open: handle_open(e); break;
case EventKind::Close: handle_close(e); break;
case EventKind::Resize:
handle_resize(e);
[[fallthrough]];
case EventKind::Redraw:
invalidate(e);
break;
}
The full mechanics are in Conditionals. Limitations:
- The discriminant must have integer type. Strings, floating-point, and class types cannot be switched on directly.
- Case labels must be integer constants.
- No destructuring; no guards; no exhaustiveness check beyond
-Wswitchfor enumerated types.
if constexpr for compile-time type discrimination
Generic code that needs to behave differently for different template argument types uses if constexpr to select branches based on type traits:
template <typename T>
void process(T value) {
if constexpr (std::is_integral_v<T>) {
binary_write(value);
} else if constexpr (std::is_floating_point_v<T>) {
text_write(std::format("{:.6g}", value));
} else if constexpr (requires { value.serialize(); }) {
value.serialize();
} else {
static_assert(false, "unsupported type");
}
}
The discarded branches are not compiled in the instantiation; the body of each branch may use operations valid only for the branch’s selected type. The mechanism replaces SFINAE-based overload selection in pre-C++17 code and is the conventional way to write code parameterised on type properties.
For the closely-related but runtime-driven case, see std::visit below.
std::variant and std::visit
std::variant<T1, T2, …, TN> (C++17) is a tagged union: an object that holds at most one of the listed types at a time, with a discriminator carried by the type itself. The conventional way to dispatch is std::visit:
#include <variant>
using Shape = std::variant<Circle, Square, Triangle>;
double area(const Shape &s) {
return std::visit([](const auto &shape) -> double {
using T = std::decay_t<decltype(shape)>;
if constexpr (std::is_same_v<T, Circle>) {
return 3.14159 * shape.radius * shape.radius;
} else if constexpr (std::is_same_v<T, Square>) {
return shape.side * shape.side;
} else if constexpr (std::is_same_v<T, Triangle>) {
double p = (shape.a + shape.b + shape.c) / 2;
return std::sqrt(p * (p - shape.a) * (p - shape.b) * (p - shape.c));
}
}, s);
}
The lambda is generic — its parameter is auto — and the body uses if constexpr to select the appropriate computation for each variant type. std::visit instantiates the lambda for each type in the variant and dispatches to the right instantiation at runtime based on the variant’s current discriminator.
The construction is the closest C++ comes to algebraic-data-type matching. The if constexpr cascade is verbose; the overload set idiom shortens it:
template <class... Ts>
struct overloaded : Ts... { using Ts::operator()...; };
template <class... Ts>
overloaded(Ts...) -> overloaded<Ts...>; // CTAD guide
double area(const Shape &s) {
return std::visit(overloaded{
[](const Circle &c) { return 3.14159 * c.radius * c.radius; },
[](const Square &q) { return q.side * q.side; },
[](const Triangle &t) { /* ... */ },
}, s);
}
The overloaded helper combines several lambdas into a single overload set. The pattern admits per-type bodies without the if constexpr cascade; it is the conventional idiom for variant dispatch in modern C++.
std::variant covers the closed-set case: the variant types are fixed at compile time, every reachable type has an entry, and the compiler can verify that the visitor handles every alternative.
dynamic_cast for polymorphic discrimination
When the discrimination is along an inheritance hierarchy rather than a closed variant, dynamic_cast is the conventional mechanism:
class Shape {
public:
virtual ~Shape() = default;
virtual double area() const = 0;
};
class Circle : public Shape { /* ... */ };
class Square : public Shape { /* ... */ };
void describe(const Shape &s) {
if (auto *c = dynamic_cast<const Circle*>(&s)) {
std::cout << "circle radius " << c->radius << '\n';
} else if (auto *q = dynamic_cast<const Square*>(&s)) {
std::cout << "square side " << q->side << '\n';
}
}
dynamic_cast<const Circle*>(&s) returns a pointer to Circle if &s actually points to a Circle (or a derived class of Circle), otherwise nullptr. The reference form (dynamic_cast<const Circle&>(s)) throws std::bad_cast on failure rather than producing a null pointer.
dynamic_cast requires polymorphic types — types with at least one virtual function. The runtime cost is the per-cast type lookup through the runtime type information (RTTI) machinery, which is meaningful in some contexts but typically negligible.
The conventional advice is to prefer virtual functions for the dispatch when the operations are fixed and known, and dynamic_cast when the operations are unknown to the base class:
// Preferred: virtual dispatch
class Shape {
public:
virtual ~Shape() = default;
virtual double area() const = 0;
virtual void draw() const = 0;
};
double total_area(const std::vector<std::unique_ptr<Shape>> &shapes) {
double total = 0;
for (const auto &s : shapes) total += s->area();
return total;
}
// dynamic_cast: when the operation is known only to the derived type
void describe_only_circles(const Shape &s) {
if (auto *c = dynamic_cast<const Circle*>(&s)) {
std::cout << "circle radius " << c->radius << '\n';
}
}
Tagged unions in idiom
When the closed-set semantics is required but std::variant is too costly (both in instantiation cost and runtime overhead), an explicit tagged union remains a viable C++ construction:
struct Shape {
enum Kind { CIRCLE, SQUARE, TRIANGLE } kind;
union {
struct { double radius; } circle;
struct { double side; } square;
struct { double a, b, c; } triangle;
};
};
double area(const Shape &s) {
switch (s.kind) {
case Shape::CIRCLE:
return 3.14159 * s.circle.radius * s.circle.radius;
case Shape::SQUARE:
return s.square.side * s.square.side;
case Shape::TRIANGLE: {
double a = s.triangle.a, b = s.triangle.b, c = s.triangle.c;
double p = (a + b + c) / 2;
return std::sqrt(p * (p - a) * (p - b) * (p - c));
}
}
return 0;
}
The construction is C-style. The trade-off is the loss of type-checking — the compiler does not enforce that the discriminator and the active union member are consistent. The standard’s std::variant is a wrapper around essentially the same mechanics with the added discipline of the type system; for new code, std::variant is preferred unless measurement justifies the manual approach.
Structured bindings and “if-let” patterns
Structured bindings (C++17) destructure pairs, tuples, arrays, and small classes into named variables:
std::map<std::string, int> ages = {{"alice", 30}, {"bob", 28}};
for (const auto &[name, age] : ages) {
std::cout << name << ": " << age << '\n';
}
Combined with if-with-initialiser, the construction approximates the if-let form of pattern-matching languages:
if (auto [it, inserted] = map.try_emplace(key, value); inserted) {
std::cout << "new entry\n";
} else {
std::cout << "key already present; existing value: " << it->second << '\n';
}
if (auto opt = parse_int(input); opt) {
int v = *opt;
/* use v */
} else {
/* parse failed */
}
The construction is the C++ idiom for “match the result, name its parts, conditionally proceed”.
Equivalents to common matching patterns
For reference, the conventional C++ shape of constructions that pattern-matching languages express directly:
Destructuring
match expr {
Add(left, right) => left + right
Mul(left, right) => left * right
}
In C++:
double evaluate(const Expr &e) {
return std::visit(overloaded{
[](const Add &a) { return evaluate(a.left) + evaluate(a.right); },
[](const Mul &m) { return evaluate(m.left) * evaluate(m.right); },
}, e);
}
Guards
match n {
0 => "zero"
n if n < 0 => "negative"
_ => "positive"
}
In C++:
const char *classify(int n) {
if (n == 0) return "zero";
if (n < 0) return "negative";
return "positive";
}
Or-patterns
match c {
'a' | 'e' | 'i' | 'o' | 'u' => true
_ => false
}
In C++:
bool is_vowel(int c) {
switch (c) {
case 'a': case 'e': case 'i': case 'o': case 'u':
return true;
default:
return false;
}
}
A note on the proposed pattern-matching feature
A pattern-matching extension has been proposed for a future C++ revision (the most recent proposals are P1371 and P2688). The mechanism would admit a unified inspect (or match) construct that destructures values, binds names, applies guards, and selects the corresponding body. The proposal has gone through several revisions; uptake into a numbered revision of the language is expected but not yet scheduled. Until then, the constructions above are the conventional approximations.