I have implemented a simple dynamic vector library in C. It is a header-only library.
Full library listing -
void_vector.h:
#ifndef VOID_VECTOR_H
#define VOID_VECTOR_H
typedef enum {
VV_SUCCESS,
VV_MALLOC_FAILURE
} vv_err;
typedef struct vv {
size_t length;
size_t size;
void *data[];
} void_vector;
void_vector* new_void_vector(size_t size);
vv_err vv_push(void_vector **vv, void *el);
void *vv_pop(void_vector *vv);
const void* vv_read_index(void_vector *vv, size_t index);
void delete_void_vector(void_vector *vv, void (del_el)(void*));
#ifdef VOID_VECTOR_IMPL
#undef VOID_VECTOR_IMPL
#include <stdlib.h>
#define defualt_size 16ul
void_vector*
new_void_vector(size_t size)
{
if (!size) size = defualt_size;
void_vector* vv = malloc(sizeof(void_vector) + sizeof(void*) * size);
if (vv) {
vv->size = size;
vv->length = 0;
}
return vv;
}
vv_err
vv_push(void_vector **vv, void *el)
{
if ((*vv)->length >= (*vv)->size) {
void_vector *new_vv = realloc((*vv), sizeof(void_vector)
+ sizeof(void*) * (*vv)->size * 2);
if (!new_vv) return VV_MALLOC_FAILURE;
(*vv) = new_vv;
(*vv)->size *= 2;
}
(*vv)->data[(*vv)->length] = el;
(*vv)->length++;
return VV_SUCCESS;
}
void*
vv_pop(void_vector *vv)
{
if(vv->length == 0) return NULL;
vv->length--;
return vv->data[vv->length];
}
const void*
vv_read_index(void_vector *vv, size_t index)
{
if (index > vv->length) return NULL;
return (vv->data[index]);
}
void
delete_void_vector(void_vector *vv, void (del_el)(void*))
{
if (!del_el) del_el = &free;
for (int i = vv->length; i; i--) {
del_el(vv->data[i-1]);
}
free(vv);
}
#endif /* VOID_VECTOR_IMPL */
#endif /* VOID_VECTOR_H */
I am using this as test bench - void_vector_tb.c
#include <stdio.h>
#define VOID_VECTOR_IMPL
#include "void_vector.h"
int
main (int argc, char **argv)
{
void_vector* vv = new_void_vector(4);
char *strings[10];
for (int i = 0; i < 10; i++) {
strings[i] = malloc(32);
snprintf(strings[i], 32, "This is String: %d", i);
}
for (int i = 0; i < 10; i++) {
vv_push(&vv, strings[i]);
}
for (int i = 0; i < vv->length; i++) {
printf("%d:\t%s\n", i+1, vv_read_index(vv, i));
}
char *s;
while(s = (char*) vv_pop(vv)) {
printf("%s\n", s);
}
for (int i = 0; i < 10; i++) {
vv_push(&vv, strings[i]);
}
delete_void_vector(vv, NULL);
}
I am not using a make file. Compilation is performed by gcc -ggdb void_vector_tb.c -o void_vector_tb
I would greatly appreciate your time and any comments but I'm particularly interested in the following points:
- Am I doing anything dangerous in this?
- Is the code clear, what can I do to make it easier to follow?
- Is there an obvious way to greatly improve the efficiency?
- General style comments
- I'm not overly concerned about the contents of
void_vector_tb.c
.
I ran this through valgrind and got the following output:
==7009== HEAP SUMMARY:
==7009== in use at exit: 0 bytes in 0 blocks
==7009== total heap usage: 14 allocs, 14 frees, 1,616 bytes allocated
==7009==
==7009== All heap blocks were freed -- no leaks are possible
==7009==
==7009== For counts of detected and suppressed errors, rerun with: -v
==7009== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
I am trying to minimize use of external libraries (although I don't think it would be possible to avoid stdlib.h).
-
\$\begingroup\$ "Am I doing anything dangerous in this?" Yeah you are using void pointers :) They tend to kill type safety and there are many better ways of generic programming. \$\endgroup\$Lundin– Lundin2019年03月08日 12:27:31 +00:00Commented Mar 8, 2019 at 12:27
2 Answers 2
Overall, fairly good.
Am I doing anything dangerous in this?
Lack of comments
I'd expect at least some commentary about the roll of the functions and how to use them near the function declaration section.
Assume a user will read the header and not delve into implementation.
Unexpected type change
The stored ellement is a void *
, changing the return type to const
is not good.
// const void* vv_read_index(void_vector *vv, size_t index)
void* vv_read_index(void_vector *vv, size_t index)
Overuse of NULL
vv_pop(), vv_read_index()
return NULL
as an error indicator and can return NULL
as a valid return. If NULL
is not to be allowed (which I think is a bad idea) as a storable void *
, vv_push(&vv, NULL)
should error.
I'd re-work calls to return an error code on vv_pop()
, like vv_err vv_pop(void_vector **vv, void **el)
.
It seems odd to have 2 forms of error signaling: error codes and NULL
.
"default size"
I see no real value in a default size of 16. Suggest setting aside size == 0
as something special and allow an initial vector size of 0. Add code to handle doubling array size when size is 0.
Create a VV_DEFAULT_SIZE 16u
if desired for the user to call with new_void_vector(VV_DEFAULT_SIZE)
.
Overflow
I'd detect overflow in size computations.
Tolerate freeing NULL
free(NULL)
is well defined. I'd expect the same with delete_void_vector(NULL, foo)
. Currently code dies on this.
Is the code clear, what can I do to make it easier to follow?
Naming convention
Recommend to use the same one prefix with all external types, names and functions.
// void_vector.h
vv.h
// VOID_VECTOR_H
VV_H
// void_vector* new_void_vector(size_t size);
void_vector* vv_new(size_t size);
Or use void_vector
instead of vv
throughout, not both.
Allocate to the size of the object, not type
Allocating to the size of the referenced object is 1) less error prone, 2) easier to review 3) easier to maintain than allocating to the type.
// void_vector* vv = malloc(sizeof(void_vector) + sizeof(void*) * size);
void_vector* vv = malloc(sizeof *vv + sizeof *vv->data * size);
Avoid unnecessary suffixes
An L
, l
, LL
, ll
is needed a lot less than one thinks. In the following case, L
certainty not needed. The u
is useful though. I am curios as to why OP thought is was needed - what issue was of concern?
/// #define defualt_size 16ul
#define defualt_size 16u
Use const
const
would help convey that the function is not altering *vv
// vv_read_index(void_vector *vv ...
vv_read_index(const void_vector *vv ...
Is there an obvious way to greatly improve the efficiency?
Allocations only increase as vv_pop()
does not reduce things. This can make for an inefficiency of memory usage.
IMO, a more advanced scheme could decrease by 1/2 when size needed falls below 25% or 33%.
General style comments
Spell check
// #define defualt_size 16ul
#define default_size 16ul
!
vs. >
Minor: With arithmetic concerns, >
is more readily understood that !
negation.
// if (!size)
if (size > 0)
I'm not overly concerned about the contents of void_vector_tb.c.
Avoid naked magic numbers
Replace 10
with #define VV_STR_N 10
, 32
with #define VV_STR_SZ 3
2,
Cast not needed
// while(s = (char*) vv_pop(vv)) {
while(s = vv_pop(vv)) {
-
1\$\begingroup\$ First off thank you for the detailed and extended review. I will make your recommended changes this evening. I have a single follow up question. Currently the function
const void* vv_read_index(vv, i)
returns aconst void*
and you are recommending it should returnvoid*
.Originally I did this as I considered the returned value to "belong" to the the vector and did not want the user changing it. Is there a better way to achieve this without confusing types or do I just trust the user not to corrupt the data store? \$\endgroup\$eternal newbie– eternal newbie2019年03月04日 08:00:16 +00:00Commented Mar 4, 2019 at 8:00 -
\$\begingroup\$ @eternalnewbie
vv
stores a pointer. Returningconst void *
orvoid *
does not allow the use to change the pointer stored invv
. Returningconst void *
does not allow the use to change what the pointer points to. That is notvv
concern sovv_read_index()
should return what it was given, avoid *
. It sounds like to are thinking about returning the address of the pointer:const void** vv_index_address(void_vector *vv, size_t index) { if (index > vv->length) return NULL; return (&vv->data[index]); }
\$\endgroup\$chux– chux2019年03月04日 13:32:17 +00:00Commented Mar 4, 2019 at 13:32 -
\$\begingroup\$ I was trying to avoid a situation where the user would call
p = vv_read_index(vv, i);
thenfree(p);
finally followed bydelete_vv(vv);
. This would cause double a free() which would be UB. I tried this out and it works and it works fine. \$\endgroup\$eternal newbie– eternal newbie2019年03月04日 18:59:07 +00:00Commented Mar 4, 2019 at 18:59 -
\$\begingroup\$ I think my best option is to take your advice and return void*. But also note in the usage comments and readme about the potential UB. \$\endgroup\$eternal newbie– eternal newbie2019年03月04日 20:58:04 +00:00Commented Mar 4, 2019 at 20:58
-
\$\begingroup\$ I now see why you coded a return of
const
. Yet the memory management of thevoid*
pointers is not really the concern ofvv
. \$\endgroup\$chux– chux2019年03月05日 02:45:02 +00:00Commented Mar 5, 2019 at 2:45
I have two things to add to @chux's comments.
The vv_read_index
function can read past the end of the vector if index
is equal to vv->length
. If vv->length == vv->size
, this will read past the end of allocated memory.
While the cast in while(s = (void *) vv_pop(vv))
is not needed, the use of an assignment within a conditional test can be misread as a typo. (Some compilers will issue a warning when you do this.) To clarify the intent, you can use
while((s = vv_pop(vv)) != NULL)