One function that fills any struct from any pattern
Every time you write a parser by hand you write the same shape twice. Once as a pattern with capture groups, and once as the struct those captures fill:
struct date { std::string_view year, month, day; };
if (auto m = ctre::match<R"((\d{4})/(\d{1,2})/(\d{1,2}))">(s)) {
return date{m.get<1>().to_view(), m.get<2>().to_view(), m.get<3>().to_view()};
}
The pattern says there are three fields. The struct says there are three fields. The assignment restates it a third time, in order, by hand.
C++26 reflection can read the struct’s fields, so the third copy is unnecessary.
The helper
template <typename T, ctll::fixed_string Pattern>
constexpr std::optional<T> parse_into(std::string_view s) {
auto m = ctre::match<Pattern>(s);
if (!m) { return std::nullopt; }
constexpr auto ctx = std::meta::access_context::unchecked();
constexpr auto members =
std::define_static_array(std::meta::nonstatic_data_members_of(^^T, ctx));
T out{};
[&]<std::size_t... I>(std::index_sequence<I...>) {
((out.[:members[I]:] = m.template get<I + 1>().to_view()), ...);
}(std::make_index_sequence<members.size()>{});
return out;
}
Capture I + 1 goes to member I, since capture 0 is the whole match. That is the entire implementation.
Two different shapes, same function, nothing written per type:
struct date { std::string_view year, month, day; };
struct host_port { std::string_view host, port; };
parse_into<date, R"((\d{4})/(\d{1,2})/(\d{1,2}))">("2026/8/22");
parse_into<host_port, R"(([a-z.]+):(\d+))">("example.com:8080");
giving
date : 2026-8-22
host_port : host=example.com port=8080
rejected : nonsense did not match, nullopt returned
Why positional rather than by name
The obvious design is to match capture names to member names, so (?<year>\d{4}) finds the year field regardless of order. That is the version I tried first, and it does not compose.
CTRE addresses a named capture as m.get<"year">(), where the name is a template argument. Reflection hands you an identifier as a value, from std::meta::identifier_of. Getting from one to the other means turning a reflected value back into a template parameter, and the straightforward spellings do not survive the trip.
Positional assignment sidesteps it completely, and the constraint it imposes is mild: the captures have to appear in the same order as the members. For a pattern and a struct written together, they already do.
What it is worth
The helper is about fifteen lines and replaces a category of code rather than a specific function. Anywhere you parse a well-shaped string into a small record, which is most configuration handling, most log processing, and a good deal of protocol work, the pattern is now the only place the shape is written down.
It also fails safely. A non-matching input returns nullopt rather than a partially filled struct, and a mismatch between capture count and member count is a compile error rather than a silent truncation.
Availability
This runs on released GCC 16.1 with -std=c++26 -freflection, using the standard <meta> header, and on the clang-p2996 fork with -freflection-latest -stdlib=libc++. Reflection is available further than its reputation suggests. One caution learned the hard way: do not gate on __cpp_impl_reflection, because the fork implements reflection without defining it, and a feature test that lies is worse than no feature test.
Sources: P2996 “Reflection for C++26” · CTRE · source in the examples repo