You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
include+lib+common: Update v.h with new v_arr implementation
The code is overall nicer to look at, however, some sacrifices had to be
made. I didn't foresee how mmap() & friends play with this new header +
array scheme (not well), so all that stuff makes ugly expensive copies
now as a stopgap. Everything seems to work, tests pass, and array stuff
is now much less verbose and overbuilt, so that's nice. I'll copy the
commit message from the (for now) private v.h repo commit message here
verbatim:
v.h: Reimplement v_arr
Technically this is v3 or even higher, but I think I'm getting close to
my ideal for dynamic, reallocating arrays.
Usage for the previous approach looked like this:
// generate scaffolding for custom type, foo_t. This generated a rather
// large blob of code that had a type-safe, generic interface via
// dynamic dispatch.
v_arr_def(foo_t)
void dostuff(void) {
v_arr(foo_t) my_arr = v_arr_new(foo_t);
v_arr_add(my_arr, (foo_t){ ...});
v_arr_free(my_arr);
}
Benefits of this previous approach were that the generated code was
type safe, and I liked having an explicit "array of T" type to make it
more obvious there's more going on here than just a plain pointer.
The drawbacks, however, were numerous:
- Passing a v_arr(foo_t) by value was really awkward and bugprone.
- It required the use of that v_arr_def(T) macro in a strategic place,
once for each type one would want to put in an array. I was moderately
okay with this, I'd usually put the codegen after structs and other
types.
- v_arr_def(T) was a bit awkward in that it required the type to not
have spaces, so structs had to be typedef'd in order to use it. I
generally try to avoid superfluous typedefs, and I like to call
structs what they are, so this was unfortunate.
- Accessing elements was a bit ugly, having to use the arbitrarily named
'items' element, e.g. 'my_arr.items[i] = ...' never really felt very
good.
- It violated the 'zero is initialization' principle that I quite like,
meaning that one had to always initialize each array with
v_arr_new(T), otherwise the macro wrappers around array ops would
detect a NULL ops table and trigger an abort. I *really* don't like
that, and it is exactly this misfeature that prompted this rewrite.
This new implementation takes cues from Sean Barrett's delightful
stb_ds.h[1], mainly it reminded me to take another look at the concept
of a header + plain C array. I had played around with flexible struct
array members before, but this approach is even nicer. I adapted the
rest from the old implementation.
This new approach is very nice. I'm sure there are bugs and subtleties
to work out, but no more dynamic dispatch, heaps of macro codegen and
awkward semantics. v_arr now appears as a plain C array. Check this out:
foo_t *my_arr = { 0 }; // Or NULL, your choice
v_arr_add(my_arr, (foo_t){ ...}); // Nice!
// Array size & cap are now macros:
printf("len: %zu, cap: %zu\n", v_arr_len(my_arr), v_arr_cap(my_arr));
// Accessing elements feels good again:
my_arr[0].member = ...;
v_arr_free(my_arr); // After this, my_arr == NULL
I'm really quite happy with this. Thanks to Feoramund on Twitter[2] for
reminding me to take a look at stb_ds.h.
TODO: Custom allocator support, more operations & tests.
[1]: http://nothings.org/stb_ds
[2]: https://x.com/Feoramund/status/1995117603522576692
0 commit comments