Implementing a sort of generic, sort of type-safe array in C
kirbyfan64.github.io
kirbyfan64.github.io
printf("%zu %d\n", list_len(l), *l[0]); // Prints 1 1.
This prints raptors unless your list is of size_t. #define list_lenref(l) ((size_t*)l)[-1]
The only way to write this kind of size-prefixed plain array in legal C will involve separate getter/setters to explicitly copy the size in and out. It's not as simple as getting a reference because you cannot recast an arbitrary array as a pointer to size_t and read/write to it. A compiler might be happy to optimize these accesses out altogether. size_t list_len(void* l) {
size_t tmp;
memcpy(&tmp, ((char*)l) - sizeof(size_t), sizeof(size_t));
return tmp;
}
void list_set_len(size_t length) {
memcpy(((char*)l) - sizeof(size_t), &s, sizeof(size_t));
}
Or if you prefer crazy macros #define list_len(l) (*(size_t*)memcpy(&(size_t){0},((char*)l)-sizeof(size_t),sizeof(size_t)))
#define list_set_len(l,s) memcpy(((char*)l)-sizeof(size_t),&(size_t){s},sizeof(size_t))
Note the ((char* )l). Pointer arithmetic on void* as used elsewhere in the code is a GNU extension. This is easily fixed by instead casting to char* or u/intptr_t.Otherwise this style of list is just fine, in fact it's not necessarily limited to pointer types.
2. my_list_type.my_array is not properly aligned
3. list_append() doesn't handle realloc() failures
4. Doesn't properly () macro argument and lots of macros also reuse their arguments. That sure is going to be fun to debug, e.g. list_len(l++);
In short, if this works for the author - great, but the re-usability of this code is next to zero. The beauty of C is that it's very explicit and verbose. Once you start concealing things behind abstract APIs, you start trading verbosity for implicit invariants leading to obscure bugs and intense head scratching. Not a worthy trade off in hell of a lot of cases.
list_append needn't take a second argument that is assigned across. If you've got an array of structs, you can then initialise the struct members in place.
#define CONCAT(A,B) A##B
#define ALEN_NAME(N) CONCAT(N,_length)
#define ACAP_NAME(N) CONCAT(N,_capacity)
#define ARRAY(T,N) \
T *N; \
size_t ALEN_NAME(N); \
size_t ACAP_NAME(N)
#define AINIT(N) do {N=NULL;ALEN(N)=0;ACAP(N)=0;}while(0)
#define ADESTROY(N) do{free(N);AINIT(N);}while(0)
#define ALEN(N) (ALEN_NAME(N))
#define ACAP(N) (ACAP_NAME(N))
#define AADD(N,V) \
do { \
if(ALEN(N)>=ACAP(N)) {N=Grow(&ACAP(N),(N),ALEN(N),sizeof *N,1);} \
N[ALEN(N)++]=V; \
} while(0)
(etc., etc.)I was a bit unsure about it at first, but having used it for a few months I've ended up quite happy with it:
- Easy to examine in a debugger;
- Code is fairly straightforward (taking into account the usual macro token pasting nonsense);
- Less risk of falling foul of whatever UB nonsense the gcc/clang people have decided to pull this week;
- Simple interop with foreign APIs (of course, reading is very easy; and my implementation explicitly allows the capacity to be smaller than the size in order to allow creation of one)
Not very convenient to pass these things to a function, though. I also don't like that the ARRAY macro generates 3 declarations. These don't really seem to be problems in practice however.
This might not be to all tastes, but there you go.
(Here's another option, that's more like the link here: https://github.com/nothings/stb/blob/master/stretchy_buffer.... - note note about strict aliasing, which I imagine could apply to the list macros here as well. Obligatory links: http://blog.metaobject.com/2014/04/cc-osmartass.html, http://robertoconcerto.blogspot.co.uk/2010/10/strict-aliasin...)
List(int*) mylist = NULL; // NULL represents the empty list.
int a=1, b=2;
list_append(l, &a);
What's lower case L?