Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

This sort of approach is a pain to use, because you keep having to cast when you're in the debugger, and there's zero type safety. And I'm afraid I don't have much positive to say about something like "((Vector2i * )arr3->data)[0].x = 333".

You can do better than this!

What is an array? It's 3 variables: base, length and capacity. So why not decide that an array is just that. 3 variables of the right size and type.

    #define ARRAY(T,S) T S;size_t S##_length;size_t S##_capacity
Then you can make one 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)

    
Add 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 header.

    void Array_Free(void *p) {
        free(p);
    }

    void *Array_Grow(void *base,size_t stride,size_t *length,size_t *capacity) {
        *capacity+=*capacity/2;
        *capacity=MAX(*capacity,MAX(MIN_CAPACITY,*length));
        return realloc(base,*capacity*stride);
    }
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]);
    }
Even performs nicely with -O0.

For a full implementation you'll probably also need a way of generating a static array. (I mainly found myself needing this for test code, which uses globals for convenience; most arrays I create normally are locals, or parts of structs.)

You'll also need a parameters list for use in a function declaration or definition, and a macro that expands to all 3 variables.

    #define ARRAY_PARAMS(T,S) T *S,size_t S##_length,size_t S##_capacity
    #define ARRAY_ARG(S) S,S##_length,S##_capacity
Like then you might have a function that takes a pointer to an "array":

    void FunctionThatTakesAnArray(ARRAY_PARAMS(T,*p));
And you call it like this:

    ARRAY(T,myarray);
    FunctionThatTakesAnArray(ARRAY_ARG(&myarray));
(I found this cropped up often enough that I needed the macro, but it was less common than I thought.)

There's more you can do, but the above is the long and the short of it.

This might all look terrible - or perhaps it sort of looks OK, but you're just not sure that it would actually work - but I've used this in a prototype project and thought it worked out well. (I've been using C for 20+ years, so hopefully even if I've got no taste, I've at least got a rough feel for what works out OK and what's going to end up a disaster.)



This gets messy in a couple of ways; for example, what if you want to pass two of them to a function? Then the names of the parameters generated by the ARRAY_ARG macro will clash and you'll have to add a counter to it, etc. (Also I'm not sure that you can concatenate `* p` with `_length` in `S##_length` where `S` is `* p`, and the same thing for the other, but I understand what you meant.) You'll also have potentially very confusing errors for the users of your library when they happen to create a variable whose name collides with one that the macro generates. And those are just the cursory observations.


ARRAY_PARAM (as I assume you mean?) has no problem with two arrays. You just give them different names. Suppose you do this:

    void CopyIntArray(ARRAY_PARAMS(int,*dest),ARRAY_PARAMS(int,src))
Now you end up with this:

    void CopyIntArray(int **dest,size_t *dest_length,size_t *dest_capacity,
                      int *src,size_t src_length,size_t src_capacity);
And you can call it like this:

    ARRAY(int,xs);
    ARRAY(int,ys);
    CopyIntArray(ARRAY_ARG(&xs),ARRAY_ARG(ys))
Your point about token pasting with * p is a very good one, and I don't think that had occurred to me... but neither clang, gcc or VC++ seems to mind (and I used a number of different versions of each). I need to go and look up what the C standard has to say about this now.

I do note that I didn't use ARRAY_PARAM or ARRAY_ARG all that much in my code, though - but I don't remember whether this is because I found some problem with them in practice, or whether it just ended up that way.

(I'm on OS X right now and I just tried my code with clang. Probably-relevant compile flags were "-std=c1x -Wall -Wuninitialized -Winit-self -pedantic -Werror=implicit-function-declaration -Wsign-conversion -Wunused-result -Werror=incompatible-pointer-types -Werror=int-conversion -Werror=return-type -Wno-overlength-strings -Wunused-parameter".)


Ah good catch, I misread it.


This is what the macros expand to.

From:

    $ cat test.c
    #define ARRAY(T,S) T S;size_t S##_length;size_t S##_capacity
    #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)
    #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)


    void Array_Free(void *p) {
    	free(p);
    }

    void *Array_Grow(void *base,size_t stride,size_t *length,size_t *capacity) {
    	*capacity+=*capacity/2;
    	*capacity=MAX(*capacity,MAX(MIN_CAPACITY,*length));
    	return realloc(base,*capacity*stride);
    }

    #define ARRAY_PARAMS(T,S) T *S,size_t S##_length,size_t S##_capacity
    #define ARRAY_ARG(S) S,S##_length,S##_capacity


    void test(ARRAY_ARG(p), ARRAY_ARG(a))
    {
    }

    int main() {
    	ARRAY(int, myarray);
    	ARRAY(int, ourarray);
    	test(ARRAY_ARG(myarray), ARRAY_ARG(ourarray));
    }
To:

    $ gcc -E test.c
    # 1 "test.c"
    # 1 "<built-in>"
    # 1 "<command-line>"
    # 31 "<command-line>"
    # 1 /usr/include/stdc-predef.h" 1 3 4
    # 32 "<command-line>" 2
    # 1 "test.c"
    # 22 "test.c"
    void Array_Free(void *p) {
        free(p);
    }

    void *Array_Grow(void *base,size_t stride,size_t *length,size_t *capacity) {
        *capacity+=*capacity/2;
        *capacity=MAX(*capacity,MAX(MIN_CAPACITY,*length));
        return realloc(base,*capacity*stride);
    }

    void test(p,p_length,p_capacity, a,a_length,a_capacity)
    {
    }

    int main() {
        int myarray;size_t myarray_length;size_t myarray_capacity;
        int ourarray;size_t ourarray_length;size_t ourarray_capacity;
        test(myarray,myarray_length,myarray_capacity, ourarray,ourarray_length,ourarray_capacity);
    }

Looks like fairly reasonable code.

Edit: I would like to see it take a type in the Params and a __typeof__ while passing the args to make sure you know what is going where.


Thanks for trying it out - my post was assembled by copying bits out of the (rather gnarlier) code that I actually used, so I'm glad it mostly survived the process ;)

Interesting point about the type checking; my thinking was that the compiler could check they matched, and that this would suffice - and sure enough it worked absolutely fine in practice. But now that I'm made to think about it again, I think it's probably still not quite good enough to be perfect, because you could do this:

    void f(ARRAY_PARAMS(const char *,*xs)) {
        ARRAY_ADD(*xs,"fred");
    }

    ...
    ARRAY(char *,xs);
    ARRAY_INIT(xs);
    f(&xs);
And now you've got a char * that points to a const string. Erm... that's not good!

EDIT: maybe I see what you're getting at with ARRAY_ARG now. The intention is that you use ARRAY_PARAMS to generate the text for the function declaration or definition (that's why it has the type in it), and ARRAY_ARG to generate the text for the code where you pass one to a function so declared (that's why it's just 3 names - they're intended to be expressions, not names for function parameters). That means test should (?) be like this:

    void test(ARRAY_PARAMS(int, p), ARRAY_PARAMS(int, a))
    {
    }

Hopefully that makes sense.

Maybe they'd have been better off with the common C terminology of formal and actual parameters. Then you'd have ARRAY_FORMAL_PARAMS for ARRAY_PARAMS, and ARRAY_ACTUAL_PARAMS for ARRAY_ARG.


I think the main drawback for every C implementation, including this one, is that you still have to write a new version of every algorithm for every type that you want to use your array struct with, unless you put all your functions in macros too. And even then, they'll only work with pointers, not with other data structures like the C++ algorithms will, unless you start doing dynamic dispatch which is what we want to avoid.

I think C++ solves this in a neater way (not saying it's good, just better) with templates, the iterator idea, and the algorithms library because you only write things once and the code is only generated for each type (not each use of the function like it would with macros).


Instead of putting it all in macros you could write the algorithms once in a "template include file" array_template_impl.h:

    #define CONCAT2(a, b) a##b
    #define CONCAT(a, b) CONCAT2(a, b)
    #define ARRAY CONCAT(array_,ARRAY_ELEMENT_TYPE)
    #define ARRAY_FUNC(name) CONCAT(CONCAT(array_,ARRAY_ELEMENT_TYPE), CONCAT(_,name))

    typedef struct {
        size_t size;
        size_t capacity;
        ARRAY_ELEMENT_TYPE *data;
    } ARRAY;

    ARRAY *ARRAY_FUNC(create)(size_t size) {
        ARRAY *p = malloc(sizeof(ARRAY));
        p->size = size;
        p->capacity = size;
        if(size) {
            p->data = malloc(size * sizeof(ARRAY_ELEMENT_TYPE));
        } else {
            p->data = NULL;
        }
        return p;
    }

    //...

    #undef ARRAY
    #undef ARRAY_FUNC
    #undef ARRAY_ELEMENT_TYPE
Then you can use them with the #include code generation trick like this:

    #include <stdlib.h>

    #define ARRAY_ELEMENT_TYPE int
    #include "array_template_impl.h"

    #define ARRAY_ELEMENT_TYPE float
    #include "array_template_impl.h"

    int main() {
        array_int *ai = array_int_create(0);
        array_float *af = array_float_create(0);

        for(int i = 0; i < 10; ++i) {
            array_int_push_back(ai, i);
            array_float_push_back(af, i * 0.3f);
        }

        array_int_free(ai);
        array_float_free(af);
        return 0;
    }
(In practice you might want even more indirection files: Put #define and #include in array_int.c. and array_float.c, and create also array_int.h, array_float.h and array_template_decl.h with just the usual declarations.)


This is a cool technique, but I tried compiling the first block of code you posted (with ARRAY_ELEMENT_TYPE being defined to be int* ) and got "error: expected ‘=’, ‘,’, ‘;’, ‘asm’ or ‘__attribute__’ before ‘*’ token" from gcc 6.2 using the flags that to3m specified in one of the parent posts posts. Is there a special trick I can use to get that to work?


You'd need a second #define, something like:

    #define ARRAY_ELEMENT_NAME int_ptr
    #define ARRAY_ELEMENT_TYPE int*
    #include "array_template_impl.h"
And then use ARRAY_ELEMENT_NAME only in the definitions of ARRAY and ARRAY_FUNC, and use ARRAY_ELEMENT_TYPE instead everywhere else.


Actually now that I think about it, it might be better to just make a typedef and then you can go back to using the same name everywhere. I think that would solve the problem.

This technique seems to take care of the bulk of the use of templates in C++, i.e. simple data structure or function definitions. But it's not a full replacement, because I don't think you can use this to do things like pass an integer to one of these templates, then have that template use another template and pass a calculation based on that number to the other template like you could do in C++. Something like this:

    template<int X>
    struct foo_t { /* ... */ };
     
    template<int Y> 
    struct bar_t {
       foo_t<Y / 2> foo;
    };
so that bar_t<50> and bar_t<24> are different types which contain a foo_t<25> and foo_t<12> respectively.

But it's cool nevertheless.


Ah yes, using a typedef would work too, nice.

It for sure not quite the same as C++ templates, but if you can tolerate crazy things you can do a lot with the preprocessor. See http://www.boost.org/libs/preprocessor/ (supports both C++ and C).

If you have foo_template.h:

    #define FOO_T CONCAT(CONCAT(foo_,X),_t)
    struct FOO_T { /* ... */ };
and bar_template.h:

    #define X Y/2
    #include "foo_template.h"

    #define BAT_T CONCAT(CONCAT(bar_,Y),_t)
    struct BAR_T {
       FOO_T foo;
    };
It might almost work, but you'd need to pull out some more tricks I'm sure. Maybe BOOST_PP_DIV(Y,2) would help. In practice I'd prefer something sane. :)


I think it's cleaner to allocate the header information before the actual pointer (or, conversely, to return the pointer sizeof(array_header) into the allocation). Then it's trivial to pass around.

  struct vec_header_t {
      size_t length;
      size_t capacity;
  };

  static inline struct vec_header_t *vec_to_header(void *vec)
  {
      return ((struct vec_header_t *)vec) - 1;
  }

  #define _vec_length(vec) (vec_to_header(vec)->length)

  static inline void vec_free(void *vec)
  {
      if (vec)
          free(vec_to_header(vec));
  }

  #define vec_foreach(vec, iter) \
      for ((iter) = (vec); (iter) < ((vec) + vec_length(vec)); ++(iter))
It's slightly more cognitive overhead when, for example, debugging, but the vast improvement in usability (no special macros for normal/static declaration, trivial passing to functions, etc) is worth it IMHO.


Compared to the code in the linked post, I guess I'm a lot more strongly in favour of type safety, which this solution has too. But as a bear of very little brain, I really must insist on no extra cognitive overhead while debugging!

Sadly there's no really good way of doing this in C, so whatever you do there's a tradeoff somewhere. You just have to decide what type of crap you want to put up with, and code accordingly ;)

(If you're working on your own, I do say this is a fair argument for at least giving C a go when you might otherwise have gone with C++. If you can come up with some stuff that doesn't annoy you in any way you care about, and you don't mind the fact that C lacks a few of C++'s creature comforts, you'll reap the benefit of hilariously better build times.)


> #define ARRAY(T,S) T S;size_t S##_length;size_t S##_capacity

I don't understand why you don't wrap this in a struct, something like (not tested):

    #define MAKE_ARRAY_T(T) typedef struct array_##T { T data; size_t length; size_t capacity; } array_##T;
(This could be generalized for types that don't paste cleanly with ##, requiring the user to specify an extra type name.)

This would buy you several advantages:

- shallow copies of arrays using =

- easier parameter passing

- easier declarations due to real type names: array_int my_integer_array;


Shallow copies aren't important to me, and after years of using C++ templates and C# generics I've come to quite like having the type where I can see it!

As for why I didn't use this approach in general, a couple of reasons:

- I often have arrays of pointers. Now you need a second parameter, to give the struct its name...

- You need to decide in advance all the possible array types you're going to have, and keep the list up to date. I wanted this to feel a lot more like using std::vector

- You can't redeclare a struct, even with the same declaration. So if you have MAKE_ARRAY_T(int) in one header, because your object has an array of int, and MAKE_ARRAY_T(int) in another, because ditto, you can't include both headers from the same file. So again you need to be able to give the type a name

- Structs can't be mixed and matched as flexibly as variables. So say you've got MAKE_ARRAY_T(int,Module1Ints) in the header for module 1 and MAKE_ARRAY_T(int,Module2Ints) in the header for module 2 - both are arrays of ints, and you avoid the naming problems I described above. But now you can't use one in place of the other, even when this would make sense

It also doesn't help with passing your arrays into your generic array functions, since you don't have a single type that you can pass around. So you're restricted to passing in the 3 individiual pieces, or inlining snippest of code via macro, like mine does.

Something I did try in the past was using anonymous structs:

    #define ARRAY(T) struct { T *p; size_t len, cap; }
However, anonymous structs are always different, even if they're structurally equivalent, so... no go. (I also dimly remember Visual Studio not even being able to show you the struct in the debugger!) But I guess trying that out was probably a stepping stone on the way to my thinking of the code I've put here.


> It also doesn't help with passing your arrays into your generic array functions, since you don't have a single type that you can pass around. So you're restricted to passing in the 3 individiual pieces, or inlining snippest of code via macro, like mine does.

When I did something like this, the type definition macro also defined strongly typed implementations of all the array functions for the given type T. Then a call is a real function call and is only inlined if the compiler chooses to do it.

The other points you mention were not problems in the particular application I was working on, but yes, these are interesting trade-offs.


This way you either have to declare every type you want to use this with at file scope (which is fine and some people do this, but it's a little irksome), or not be able to pass them as parameters to functions. Also it doesn't solve the problem of function genericity.




Consider applying for YC's Fall 2026 batch! Applications are open till July 27.

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: