ctre · part 07

One function that fills any struct from any pattern

· english· audience: working-cpp· AI-generated, reviewed by Filip Sajdak

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