Skip to content

Patternia v0.9.5 Release Note

Release Date: October 2026 Version: 0.9.5


Overview

Patternia v0.9.5 is a naming-and-dispatch release. has<...> is now tuple-like, so a C++17 structured binding names the members of a structural pattern without a macro, and PTN_BIND is deprecated in its favor. Handler classification is rewritten so generic lambdas, pointers to members and reference-returning handlers work, and the bare spellings is<T> and alt<I> reach the static variant dispatch fast path that only is<T>() and alt<I>() used to get.

There are no breaking API changes. One behavior changed for code that passed a pointer to member as a handler; see Behavior Changes.


New Features

Structured-binding member names

has<...> implements the tuple-like protocol (std::tuple_size, std::tuple_element and an ADL get). A structured binding declares one member-anchored placeholder per non-ignored member:

struct Point { int x; int y; };

constexpr auto pt = has<&Point::x, &Point::y>;
auto [x, y] = pt;

match(p) | on(
  $(pt)[x * x + y * y == 25] >> "on circle",
  _                          >> "elsewhere"
);

Names follow the members listed in has<...>, and _ign slots do not get a name:

constexpr auto pkt = has<&Pkt::type, &Pkt::len, ptn::_ign, &Pkt::crc>;
auto [type, len, crc] = pkt;   // three names, one per member

The names are placeholders, not values extracted from a subject; $(pt) still decides which values reach the handler.

Scope rules:

  • Function-local auto [x, y] = pt; works with on(...), but not with PTN_ON(...), whose captureless lambda cannot see automatic bindings.
  • For PTN_ON, declare the pattern and its names at namespace scope.
  • In C++20, static constexpr auto pt = ...; plus static auto [x, y] = pt; also works inside a function.
  • In C++17, keep PTN_BIND for function-local names used inside PTN_ON.

Misuse is reported once, not as a cascade: a placeholder that does not belong to the pattern, an out-of-range index into has<...>, or member names on a non-structural pattern each produce a single diagnostic. Guard diagnostics no longer mention PTN_BIND.

Handler forms

Handler classification no longer instantiates the handler to find out what it is. This fixes three cases that did not work before:

  • A generic lambda that reads members of its argument now compiles inside on(...):
match(v) | on(
  $(is<Circle>()) >> [](const auto &c) { return c.r; },
  $(is<Square>()) >> [](const auto &s) { return s.side; },
  _               >> 0
);
  • Handlers that return a reference (a lambda returning const T &, or a pointer to a data member such as $ >> &Point::y) are accepted.
  • Pointers to members are handlers (see below).

Behavior Changes

Pointers to members are handlers

$(is<Circle>()) >> &Circle::area now calls area on the matched value. Before, the pointer itself was the result of the case. Code that relied on a case returning a member pointer has to wrap it in a lambda:

// v0.9.4: the case produced the member pointer
// v0.9.5: the case produces the call result
$(is<Circle>()) >> &Circle::area

// to keep the old meaning
$(is<Circle>()) >> [] { return &Circle::area; }

Deprecations

PTN_BIND

PTN_BIND is deprecated in favor of structured-binding member names. It keeps working, and it remains the way to name placeholders inside a function that uses PTN_ON in C++17. The removal version is not decided.

// v0.9.4
PTN_BIND(Point, x, y);
$(has<&Point::x, &Point::y>)[x * x + y * y == 25]

// v0.9.5
constexpr auto pt = has<&Point::x, &Point::y>;
auto [x, y] = pt;
$(pt)[x * x + y * y == 25]

Performance

Bare is<T> and alt<I> are now stored as the canonical pattern types that the dispatch optimizer recognizes. On an 8-alternative std::variant, is<T> >> ... and alt<I> >> ... cases dispatch about 40% faster than in v0.9.4 and match the is<T>() / alt<I>() forms. See the v0.9.5 performance note.


Bug Fixes

  • -Wextra -Werror users no longer fail on #include <ptn/patternia.hpp>: negation_pattern::bind named an argument it never used.
  • Two unsequenced double-forwards in guard and tuple evaluation, and an unconstrained forwarding constructor, found by the new clang-tidy gate, are fixed.

Internal Improvements

  • CI: clang-tidy gate on headers, tests and samples; a libFuzzer oracle target (fuzz/ptn_fuzz_match) with a smoke run on every PR and a weekly deep run; pinned developer tools and CI dependency versions; Linux matrix runners pinned to ubuntu-24.04.
  • Bench: the PR benchmark job compares base and PR on one runner and posts a sticky comment with the noise floor applied; generated charts moved off main to the bench-charts branch.
  • Tests: 256 tests (up from 204 in v0.9.4), including compile-fail cases that pin the diagnostics for invalid member names and out-of-range has<...> indices.
  • Docs: tutorials and the API reference use structured-binding names.

Migration Guide

Nothing needs to change to upgrade. To move off the deprecated macro:

// before
PTN_BIND(Point, x, y);
auto r = match(p) | on($(has<&Point::x, &Point::y>)[x < y] >> 1, _ >> 0);

// after: namespace scope, also valid inside PTN_ON
constexpr auto pt = has<&Point::x, &Point::y>;
auto [x, y] = pt;
auto r = match(p) | on($(pt)[x < y] >> 1, _ >> 0);

Inside a function that uses PTN_ON in C++17, keep PTN_BIND.