Devlog #7: String View (Commit: 656a879786)
3 changed files with 624 additions and 4 deletions
The next core module I built was the string view.
The primary goal of this module was to build it fast, avoid heap allocation, ambiguity, and make it “bulletproof”.
My reasoning behind making this module is simple: it will be heavily used in the upcoming CLI argument parser and the media-to-ascii engine.
The struct and constructors
ZinnStringView is dead simple, just a const char* data and a size_t length. Two constructors build views from either explicit parts or null-terminated C strings. Both return ZINN_STRING_VIEW_NULLPTR if you pass them null or empty data.
The literal macro
The coolest part of this module is ZINN_STRING_VIEW_LITERAL(). It wraps a string literal at compile time using _Generic to detect whether you passed an actual literal or a pointer variable.
ZinnStringView sv1 = ZINN_STRING_VIEW_LITERAL("hello"); // works
ZinnStringView sv2 = ZINN_STRING_VIEW_LITERAL(pointer); // compile error (see attachment)
If you pass a variable, _Generic sees char* or const char* and the static_assert fires with a message telling you to use zinn_string_view_from_cstring() instead.
Also, while making the attachment I realized what I wrote is actually not correct for C23, as you cannot have static assertions in expressions. This is only a feature in the upcoming C2Y (C29) standard.
Implemented functions
Generally, I preferred using standard library functions as they are battle-tested, and more often than not use SIMD instructions for much faster execution than whatever I could come up with.
Slicing and trimming
zinn_string_view_subview gives you a sub-slice with bounds clamping. If offset is past the end you get ZINN_STRING_VIEW_NULLPTR. If offset + count overshoots, it clamps down. The three trim functions share one implementation behind an enum that controls whether to trim left, right, or both.
Comparison and matching
I built a 256-byte lookup table that maps A-Z to a-z for case-insensitive comparison. It’s locale-independent, so no tolower() surprises on Turkish systems. find_char delegates to memchr, which your libc likely implements with SIMD (16-32 bytes per cycle). starts_with and ends_with are just memcmp with an offset. split_first finds the delimiter via memchr and chops the view in two.
Number parsing
Parsing from a string view is tedious because standard library functions expect null-terminated strings. The solution is to copy into a small stack buffer, null-terminate it, and call the respective function. The double variant uses strtod_l with a POSIX “C” locale so "3.14" is parsed correctly even if the user’s system uses commas as decimal separators. Both trim whitespace first, check for overflow via errno, and reject partial parses like "123abc".
What’s next?
My favorite activity, unit tests!
Comments 0
No comments yet. Be the first!
Sign in to join the conversation.