Live data from Hacker News

"Parse, don't validate" through the years with C++

derekrodriguez.dev

31–40 of 52 posts

Re: "Parse, don't validate" through the years with C++

#31

The C example could have implemented a lot of validation just by checking the return value of sscanf(): if (sscanf(user_input, "%4u-%2u-%2u", &year, &month, &day) != 3) { // return an error } This still does not catch trailing garbage, but you could check for that as well: if (sscanf(user_input, "%4u-%2u-%2u%c", &year, &month, &day, &dummy) != 3) { // return an error } The result would be 4 if there was at least one…

Although it feels intuitively as though a std::scan could make sense, it doesn't, at least not with the sort of API I've seen suggested

Consider a hypothetical Goose type, we can express any Goose usefully as output and, conveniently, some potential inputs could be read as a Goose successfully though most arbitrary strings cannot be understood as a Goose.

Providing std::print for Goose is simple, we've got a variable (or maybe a constant) of type Goose, we just emit the correct sequence of symbols. It's annoying to actually write all the boilerplate in C++ 23 but that's mechanical it's not actually tricky to do just very boring (and so hence maybe C++ 26 makes that easier via reflection)

But how could std::scan for Goose work? We need a Goose variable to potentially store the Goose if we read one, but how can we make a default Goose? No, each Goose is unique and there is no substitute, this can't work.

The std::scan idea seem attractive for simple almost untyped input, strings, integers, that sort of thing, but the whole point of "Parse, don't validate" is that you probably want to parse email addresses and ISBNs and ISO dates, you don't want a string, another string and a third string.

Rust's FromStr trait is more appropriate. Given a type implements FromStr we can parse any string to (maybe) get an instance of that type, but we don't need an "empty" instance first because we're doing the construction when we call the function.

Re: "Parse, don't validate" through the years with C++

#32

Earlier quoted context omitted.

exactly, use std::expected as the return type, avoid exceptions, and make a failable factory constructor to build your type. Make invalid states unrepresentable!!!

Aren't you time-travelling? std::expected is C++23 (so available starting from 2025-2027 xd) https://en.cppreference.com/cpp/utility/expected

It has been available since GCC 12.1 (May 2022), Clang 19.1 (Sep 2024), and Visual Studio 17.13 (2022~): https://godbolt.org/z/on1v6qdf3

These days compiler developers implement accepted standard features pretty fast.

Re: "Parse, don't validate" through the years with C++

#33
post #2

C++ could use some do-notation

Abstracting any part of code structure in C++ is a wasps nest that will attack you back.

Did you mean "abstract you back"?

Being abstracted by code you just wrote is quite a painful experience, yes.

Re: "Parse, don't validate" through the years with C++

#34
The second sentence of your summary is fine, but I don’t like the first sentence:

> Use your language’s type system to parse unstructured inputs.

We don’t use the type system to parse. We use the type system to provide evidence (also called a proof or a witness) that parsing was successful, and we rely on the language’s access control facilities (public/private) and the soundness of its type system to prevent fabrication of false evidence.

Re: "Parse, don't validate" through the years with C++

#35

C is perfectly capable of type-driven design. He's already got the type (struct), and although C is a bit limited, he can: * return pointer-or-null * choose "invalid" sentinel values and then use birthdate_is_valid(...) to check validity. * Add an is_valid bool field (or even an error enum like in the C++23 example) * Add an out field in the constructor function for the error code (similar to how ObjC does things).

The point of parse-don't-validate is that the type checker prevents you from having a value of a particular type that's invalid.

Pointer-or-NULL doesn't work, because all pointers are nullable in C; you can always have a Foo* (NULL) that's doesn't actually point to a valid Foo.

Invalid sentinel values are definitionally values of a particular type that are invalid. Same with an is_valid field.

An out field in the constructor means that whatever you actually return in the case of an error is going to be a well-typed Foo that's invalid.

Re: "Parse, don't validate" through the years with C++

#36

I don't see how this is in any way preferable to having an ordinary default constructor that does the same thing: // There are a few ways to let API callers bring their own // memory, as they would in a no-malloc environment and this // stack-friendly c'tor is a stand-in for that. static Birthdate epoch() { return Birthdate(1900, 1, 1); }

Some readers will expect Birthdate() to be equivalent to Birthdate(0, 0, 0), and naming it Birthdate::epoch() makes it clear that it is not that. I don't think it's worth it, but there is an upside.

Re: "Parse, don't validate" through the years with C++

#37

The C++11 example is the weakest in the article by its own thesis. Public throwing constructor, no year check, no leap-year check, so Birthdate(0, 2, 30) constructs cleanly. The C++17/23 shape (private ctor + static factory) is the actual mechanical insight from King's essay. Make the constructor a function that can fail, so the type itself carries the proof.

Just to note, a throwing constructor is “just as good” as static factory method, provided you want to use exceptions for validation errors. Which you shouldn’t, but from the perspective of testing types as proof, it’s just as good.

Re: "Parse, don't validate" through the years with C++

#38
It seems like the C++98 example is the best by far? Keeps all error information while remaining concise and easy to understand. Not to mention 50 times faster. (Could be improved by adding some simple type aliases like BirthYear that explicitly start from 1900.)

IMO the main takeaway is that malformed input is not an exceptional state when parsing, and should be treated as a first class citizen. Everything else is yak shaving how you want to handle the (status, validObject) tuple coming from the parser.

Re: "Parse, don't validate" through the years with C++

#39
post #35

C is perfectly capable of type-driven design. He's already got the type (struct), and although C is a bit limited, he can: * return pointer-or-null * choose "invalid" sentinel values and then use birthdate_is_valid(...) to check validity. * Add an is_valid bool field (or even an error enum like in the C++23 example) * Add an out field in the constructor function for the error code (similar to how ObjC does things).

The point of parse-don't-validate is that the type checker prevents you from having a value of a particular type that's invalid. Pointer-or-NULL doesn't work, because all pointers are nullable in C; you can always have a Foo* (NULL) that's doesn't actually point to a valid Foo. Invalid sentinel values are definitionally values of a particular type that are invalid. Same with an is_valid field. An out field in the con…

My point is that you do the checking at the call site, and then use a static analysis tool or an AI to enforce checking the result right after calling parse_birthday.

Sure, Optional is more elegant, but the end result is the same: Now none of the other code needs to validate; it's already been verified valid at all points where a parse error could have occurred.

C may not be an easy language, but with the right tooling you can make code safer, and idioms like parse-dont-validate possible.

Re: "Parse, don't validate" through the years with C++

#40
post #32

Earlier quoted context omitted.

Aren't you time-travelling? std::expected is C++23 (so available starting from 2025-2027 xd) https://en.cppreference.com/cpp/utility/expected

It has been available since GCC 12.1 (May 2022), Clang 19.1 (Sep 2024), and Visual Studio 17.13 (2022~): https://godbolt.org/z/on1v6qdf3 These days compiler developers implement accepted standard features pretty fast.

And tl::expected (a largely identical impl) has been available similarly as long!
Post reply on HN