Generic dynamic array in 60 lines of C
gist.github.com
gist.github.com
The handling of endptr in DYN_ARR_RESIZE seems to be incorrect. If I have an array with 2 elements and capacity of 3 and I DYN_ARR_RESIZE it to 5, I now have an array with 5 elements, 3 of which are garbage values.
Nevermind, I see how it's supposed to work now.
To your point, `RESIZE` is definitely incorrect. It should be checking the length before calling `realloc`.
Why do you say this is wrong? That's exactly what I would expect.
Exposing uninitialized memory as valid values tends to be a really bad idea.
this somewhat mirrors what happens with a std::vector when you call resize() except of course c doesn't have default ctors. the point of resize is to make exactly "size" elements of the dynamic array addressable.
As fpoling points out, capacity is a 32-bit unsigned. That can overflow. There is no safety in append: if capacity is zero no new space will be made, if capacity overflows the realloc will not have enough space -- in either case, you end up writing past the end.
Allocating a [capacity] zero array and appending to it is extremely common. That you'll write past the end in that case shows that the author has barely even used this.
The code is unacceptably bad and unsafe. I don't usually do this, but I'm going to flag this post. I encourage everybody to do the same.
Apologies to the author. Golf is fun. This is uncool.
edit: changed size to [capacity]
In that scenario (where you're allocating a multi-gigabytes array), you typically know the size ahead of time, instead of growing it dynamically, as the latter doesn't really perform too well.
a.capacity <<= 1u;
"all c code is unsafe" is not an excuse to permit bloody obvious, undocumented memory overruns.I write a lot of c. Avoiding the unsafe bits, avoiding UB, is the skill required to write good c. "C code is unsafe" is a Rustacean marketing slogan. Don't believe it, but definitely don't practice it.
I'm also not certain one should use "end pointers." Conventionally, it seems more advisable to use `size_t capacity`, `size_t length`, and `void *data`.
Great use of Cunningham's Law, though! I appreciate C posts on Hacker News.
struct parser {
struct {
unsigned char *buffer;
size_t capacity;
} buffer;
struct {
size_t read;
size_t write;
} position;
};
The lexer makes progress by consuming bytes from the buffer, incremeting the read position. The buffer is reallocated whenever such a read would overrun the write position: I/O functions are called to fill up the buffer and push the write position further ahead.Had they been pointers within the [buffer, capacity[ range, they would have become invalid whenever buffer is reallocated for expansion since its location in memory would change. So I chose to implement the read and write heads as offsets to the buffer's base address and calculating those pointers only when they're needed.
I don't think those are impossible scenarios, but the cost of one additional pointer in terms of size leaves you with a lot more functionality. Saving one pointer in size and not having capacity makes GLib arrays nearly useless, which I find confusing.
You could simply pass a pointer and a size around instead. Which is what most people actually do when they don't need resizable data layouts.
If you're working with C, I think you have to just accept that void pointers happen. Working around losing compile-time type data requires you to create runtime structures, which I don't find acceptable.
Compare:
#define FOO() { ... }
#define BAR() do { ... } while (0)
In the former case: if (x)
FOO();
else
...;
// becomes
if (x) {
...
}
;
else
...;
In the latter case: if (x)
BAR();
else
...;
// becomes
if (x)
do {
...
} while (0);
else
...;I just discovered that gcc also supports non-standard "statement expressions" of the form ({...}) which would serve the same purpose at the cost of portability.
For example, a C++ Vector provides a generic container and the code can still call C functions with the underlying array.
This is in the "Doctor it hurts if I do X" bucket.
https://stackoverflow.com/questions/2923272/how-to-convert-v...
If you're exposing structs that are passed back and forth for state, it's a bit more cumbersome.
(I know these are far an in-between for ffmpeg, too many leaky abstractions.)
If heavy macro usage is found, it's definitely time to reconsider the approach.
Macros can be a nightmare, but when they're used properly they're not.
Yeah, they are.
Source: Decades of C programming
Granted, Lisp already has arrays, so not much reason to reinvent the wheel.
(make-array '(2 6) :initial-element 0 :element-type '(unsigned-byte 32))
;; => #2A((0 0 0 0 0 0) (0 0 0 0 0 0))stuff like this is great when you are trying to find the performance ceiling of some workload in c/cpp. literally nothing to hide.
Here's one I've written a few years ago: https://github.com/kiryk/mlisp/blob/master/vector.c
You may find it dirty, but it can be wrapped using macros, and used both on LHS and RHS like in "string(v, i) = ch"
(the macros BTW) https://github.com/kiryk/mlisp/blob/71d028738d2f9607fa7ce8c1...
(and a usecase) https://github.com/kiryk/mlisp/blob/71d028738d2f9607fa7ce8c1...
There's also a well-known one here, in klib: https://github.com/attractivechaos/klib/blob/master/kvec.h
1. not using "do {} while (0)". This may lead to compile errors.
2. using uint32_t for capacity. On 64-bit machine, this doesn't save memory.
3. DYN_ARR_RESIZE() may have a quadratic time complexity if some is calling DYN_ARR_RESIZE(a, 10); DYN_ARR_RESIZE(a, 11); DYN_ARR_RESIZE(a, 12) etc in a loop. kv_resize() in kvec.h wouldn't have this problem.
4. Segfault if capacity is 0.
Part of the reason why C developers "feel" productive, but can't produce anything of meaningful complexity.
Anyway, I think this HN posting has this "crazy" flavor of using macros to simulate generics, and that's the specific kind of implementation that I meant, which klib also does.
Not only did this shave 8 bytes off of each instance but in many cases it saved more because they became 16 bytes which reduced alignment padding in other objects that use SIMD types that require 16-byte alignment.
This saved 40 MB of memory for us in https://warframe.fandom.com/wiki/Orb_Vallis which, given the poverty our mobile minspec, was a terrific savings.
Your needs may vary, but it's 2023 we're still cramming 4GB of poop into a 2GB bag.
[0] https://rkeene.org/viewer/tmp/hat.c.htm [1] https://rkeene.org/viewer/tmp/hat.png.htm
[1] https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3003.pdf
There are long term ABI benefits to these data structures just being opaque pointers outside of the implementing libraries
I've seen too many C developers re-write C++ containers in C because they are afraid of C++, its madness.
The result is that you can easily link C code to almost any language, including C++, with almost any linker. But for C++, you usually have to use the linker that comes with the C++ compiler that compiled your library. You can write code in C++ that is as compatible as C, but you have to go out of your way to achieve that, extern "C" is only the beginning.
As a result, when the overhead of using C instead of a more complete language is not too great, I prefer to write my libraries in ANSI-C, for maximum compatibility.
Not to say I think this particular implementation is my favorite, but why anybody should expect a free online code snippet should be bulletproof is beyond me, it is a damn gist, a demo.
https://doc.rust-lang.org/src/alloc/vec/mod.rs.html#400
https://docs.rs/containers/latest/src/containers/collections...
Section 6.10 (Page 145): Preprocessing Directives
Hey, would you look at that! The preprocessor is a mandatory part of the language!
a.capacity <<= 1u;
decltype(a.data) tmp = (decltype(a.data)) realloc(a.data, sizeof(a.data[0]) * a.capacity);
assert(tmp != NULL);
That's not ideal. Imagine you are at 32GB capacity, the next realloc will ask for 64GB which is pretty excessive.realloc() generally allocates a new block of memory of the new size, copies the entire content over, and then frees the old block. Only sometimes you get to be lucky and have enough spare room after the existing data in the virtual address map to not need that copy.
By the point this copying becomes relevant, a continuous dynamic array like this becomes fundamentally the wrong data structure.
Good ones might resize by the golden ratio, and enlarge by blocksize if larger
No checking malloc or realloc though. The anonymous struct is dubious too, though maybe being unable to pass these things to functions is a feature.
however, you can still do that, if you really want.
just `typedef DYN_ARR_OF(widget) widget_array;` and now you have a name-able type, and can even have dynamic-arrays-of-dynamic-arrays (`DYN_ARR_OF(widget_array)`).