ctre · part 03

Validating a string before the program exists

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

A regex that runs at compile time sounds like a parlour trick until you notice what it replaces: the class of bug where a hardcoded string is subtly wrong and nobody finds out until the code path runs.

CTRE matches are usable in constant expressions, which makes this a build error:

static_assert(is_semver("1.2"));   // fails: two components, not three

Proving it really is compile time

The demo below is written so it cannot cheat. Every check is a static_assert, and the validator is consteval rather than constexpr, which means it cannot be called at run time at all. If the matcher were not running during translation, the file would not compile.

static constexpr auto kSemver = ctll::fixed_string{R"((\d+)\.(\d+)\.(\d+))"};

consteval bool is_semver(std::string_view s) {
    return static_cast<bool>(ctre::match<kSemver>(s));
}

static_assert(is_semver("1.2.3"));
static_assert(!is_semver("1.2"));
static_assert(!is_semver("1.2.3-rc1"));
static_assert(!is_semver("banana"));

Parsing, not just checking

The captures are ordinary string_views into the literal, so they work in a constant expression like anything else. That is enough to turn a string into a structure at compile time:

consteval version parse_semver(std::string_view s) {
    auto m = ctre::match<kSemver>(s);
    if (!m) { return version{-1, -1, -1}; }
    return version{to_int(m.get<1>().to_view()),
                   to_int(m.get<2>().to_view()),
                   to_int(m.get<3>().to_view())};
}

constexpr auto v = parse_semver("2.11.4");
static_assert(v.major == 2);
static_assert(v.minor == 11);
static_assert(v.patch == 4);

v is a constant. Nothing about the parse survives into the binary; what remains is three integers the compiler already knew.

consteval or constexpr

One detail that catches people, and caught me while writing this. A constexpr function may be called at run time, so it cannot forward its parameter to a consteval one:

constexpr bool require_semver(std::string_view s) { return is_semver(s); }
// error: call to consteval function 'is_semver(s)' is not a constant expression

The compiler is right, and the fix is to say what you meant. If the check only ever happens during compilation, the function is consteval:

consteval bool require_semver(std::string_view s) { return is_semver(s); }

Choosing between the two is choosing whether run-time callers are allowed. For a validator over string literals, they are not, and saying so is the more honest signature.

Where this earns its place

Version strings are the small example. The shape generalises to anything spelled as a literal and structured by convention: a route table, a set of environment-variable names, a configuration key, an identifier format your team agreed on. Each of those is usually checked by a comment, a code review, or a test that runs long after the mistake was typed.

A static_assert over a pattern moves that check to the place where the mistake happens, and costs nothing at run time because there is no run time involved. The bill arrives at compile time instead, which the last episode of this series measures rather than hand-waves.


Sources: CTRE · demo source in the examples repo