I don't think there's any objectively good solution to this perennial annoyance, so this is just some random thing that tries to tick the specific boxes I want ticked: simple to look at in the debugger; few/no casts during use; easy interop with existing C arrays; OK performance in an unoptimised build.
A growable array needs 3 values: base, length and capacity. This system just treats any 3 variables of the right names and types as defining an array. There's a macro that automates the definition of these variables.
#define ARRAY(T,S) T *S; size_t S##_length; size_t S##_capacity
You can use this to create 3 locals, 3 globals, or 3 struct members. Like this:
ARRAY(int, xs);
You'll also need to initialise and these destroy array "objects".
#define ARRAY_INIT(S) \
do { \
S = NULL; \
(S##_length) = 0; \
(S##_capacity) = 0; \
} while(0)
#define ARRAY_DESTROY(S) \
do { \
Array_Free(S); \
ARRAY_INIT(S); \
} while(0)
You'll probably want to add an item to an array too.
#define ARRAY_ADD(S,X) \
do { \
if((S##_length) >= (S##_capacity)) { \
S = Array_Grow(S, \
sizeof *S, \
&(S##_length), \
&(S##_capacity)); \
} \
(S)[(S##_length)++] = (X); \
} while(0)
So you might use them like this:
ARRAY(int, xs);
ARRAY_INIT(xs);
for(int i = 0; i < 100; ++i)
ARRAY_ADD(xs, i);
ARRAY_DESTROY(xs);
Array_Free
is very simple, and Array_Grow
is barely more
complicated (however I wrote it off the cuff, so of course it could
still be wrong). Both of these mainly exist just to keep stdlib.h out
of the array header.
void Array_Free(void *p) {
free(p);
}
/* Pick any value you like */
#define MIN_CAPACITY ((size_t)1)
void *Array_Grow(void *base, size_t stride, size_t *length, size_t *capacity) {
*capacity += *capacity / 2;
*capacity = MAX(*capacity, MAX(MIN_CAPACITY, *length + 1));
return realloc(base, *capacity * stride);
}
(Note that if the capacity is smaller than the length, Array_Grow
will assume the capacity is the length. This is so you can use
malloc'd buffers from other sources, that have a known size but
unknown capacity. Set the capacity to 0 and Array_Grow
will do
something sensible.)
Array accesses and iteration and the like are just done in the traditional way:
for(size_t i = 0; i < xs_length; ++i) {
printf("%d\n", xs[i]);
}
And so on, and so on - there's plenty more you can do, much of it obvious. But the above is the long and the short of it.
If a function is just going to consume an array, typically it only needs the base and length, so those can just be passed in manually.
If the function is going to modify the array, it'll need all 3 variables. More macros for this:
#define ARRAY_PARAMS(T,S) T *S, size_t S##_length, size_t S##_capacity
#define ARRAY_ARG(S) S, S##_length, S##_capacity
Then you might have a function that takes a pointer to an "array":
void FunctionThatTakesAnArray(ARRAY_PARAMS(int, *p));
And you call it like this:
ARRAY(int, myarray);
FunctionThatTakesAnArray(ARRAY_ARG(&myarray));
And operate on it like this:
void FunctionThatTakesAnArray(ARRAY_PARAMS(int, *p)) {
ARRAY_ADD(*p, 100);
}