HiveBrain v1.2.0
Get Started
← Back to all entries
patterncModerate

Template vector struct in C11

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
structtemplatevectorc11

Problem

This one review will be a little bit tricky: I was trying to implement a template Vector struct in C11. Of course, templates don't really exist in C11, so I used a few macros to get the desired interface. Here is the implementation of the core features, trying to mimick those of the c++ std::vector. Some are still missing though. First, the macro define_Vector:

```
#define define_Vector(T) \
\
struct _vector_##T; \
\
typedef struct \
{ \
void (delete)(struct _vector_##T); \
T (at)(const struct _vector_##T, size_t); \
T (front)(const struct _vector_##T); \
T (back)(const struct _vector_##T); \
T (data)(const struct _vector_##T*); \
T (begin)(struct _vector_##T*); \
const T (cbegin)(const struct _vector_##T*); \
T (end)(struct _vector_##T*); \
const T (cend)(const struct _vector_##T*); \
bool (is_empty)(const struct _vector_##T); \
size_t (size)(const struct _vector_##T); \
size_t (*max_size)(void); \
void (reserve)(struct _vector_##T, size_t); \
size_t (capacity)(const struct _vector_##T); \
void (shrink_to_fit)(struct _vector_##T); \
void (*clear)(struct

Solution

There is a rather large amount of code here and a lot of macro trickery, so I shall refrain from attempting to comment on it all.

There are, however, a few things that immediately jumped out at me.

Don't use new as your allocating macro. That's extremely confusing, and it's going to make compiling it as C++ rather difficult. (Not that having C be compilable as C++ is exactly a noble goal -- it's just not worth throwing away over one word).

Your macros should be namespaced. size? at? You don't think anyone else might try to use those names?

Rather than define_Vector have vector_define and then vector_at, vector_size, etc. Not only does this make it immediately clearer that things like size aren't some kind of magic, it should hopefully help avoid potential collisions if someone were to use this with other code.

realloc can fail. You need to consider that in vector_reserve, vector_shrink_to_fit and so on.

Are you sure your accessors (vector_at, vector_front, etc) should return a T and not a T*? Imagine if you're storing a large struct. You would probably rather be able to mutate the struct inside of the vector rather than having to copy it, modify it, and then put it back. (Yes, you can get direct access like this with your 'iterators', but eh...).

This feels a bit incomplete if you're trying to mimic C++. Where are constructors? Destructors? Copiers? What if I have a vector of 10 structs that have FILE* pointers? It would be nice to be able to assign a destructor to those so that calls like vector_clear would not leave a dangling file handle.

I actually might consider going with T here like you've done, but it's fairly customary in C to use an automatic form of sizeof rather than explicit. Like:

res->_data = malloc(40*sizeof(*(res->data)));


Accepting constant pointers rather than values in a few places would allow you to avoid potential copies. Unfortunately that makes the calling semantics a bit grosser, but welcome to C :).

40 is a rather large default capacity. (Also, you have to handle malloc/realloc failures for a library like this.)

It would be nice to be able to avoid a malloc until something is actually stored in the vector. It could avoid a rather costly allocation in certain cases where a vector is created and then not pushed into.

I might try to pull the default capacity into a constant or something, but either way, it shouldn't be repeated:

res->_capacity = 40;                                           \
res->_size = 0;                                                \
res->_data = malloc(40*sizeof(T));


Could be:

res->_capacity = 40;                                           \
res->_size = 0;                                                \
res->_data = malloc(res->_capacity*sizeof(T));


This is just to avoid a mismatch if at some point 40 is changed on one line and not the other.

Unsolicited, blatant opinion incoming :)

Though this is elegant and clever, overall I'm not sure how I feel about it.

As you almost certainly know, data structures in C typically revolve around void* with a complete lack of type safety. Your vector give type safety at a relatively minor cost (binary size), so that's rather nice and a very real benefit.

I can't help but feel though that at the end of the day, you've just recreated templates in a language not meant to support them. In almost all situations where this would be used, I think the proper option would be to just use C++. Then again, I'm very biased towards C++, and my sentiment is almost always "if you can use C++, then you should use C++."

In situations where you're stuck using C, this could be very valuable if type safety is very important. I do believe though that it would require a bit more polishing in terms of the items mentioned above, and it would need to be tested very thoroughly in both functional and performance senses.

Code Snippets

res->_data = malloc(40*sizeof(*(res->data)));
res->_capacity = 40;                                           \
res->_size = 0;                                                \
res->_data = malloc(40*sizeof(T));
res->_capacity = 40;                                           \
res->_size = 0;                                                \
res->_data = malloc(res->_capacity*sizeof(T));

Context

StackExchange Code Review Q#43809, answer score: 13

Revisions (0)

No revisions yet.