I am interested in opinions of experienced C programmers about a certain approach to creating a "generic" dynamic array in C. The idea is to use macros to generate function declarations and definitions, and to avoid using void pointers (of course, this is nothing unheard of). For simplicity, I will only generate three basic functions - one to create such an array, one to destroy it, and one to append an element to the array.
It seems a practical approach, however, before starting to use this pattern all over, I would like to check if it makes sense and if it suffers from some major drawbacks (which I can not identify). So, let's get started.
First, let us define a few utility macros, whose purpose it primarily to avoid repeating boilerplate "safety" code (eg checking if returned pointers are NULL, etc):
// utils.h
#include <stdio.h>
#include <stdlib.h>
/*
* Exit with an error message.
*
* @param msg String literal, message to display.
*/
#define EXIT_ERROR(msg) \
do { \
printf("Error in %s at line %d: %s - terminating program.\n", __FILE__, __LINE__, msg); \
exit(EXIT_FAILURE); \
} while(0)
/*
* Macro which also checks if the pointer returned by malloc is NULL.
*
* @param ptr Name of pointer to which malloc is assigned.
* @param size Multiple which determines size of malloc'd array
* @param type Type of array element
*/
#define MALLOC_SAFE(ptr, size, type) \
malloc((size) * sizeof(type)); \
if (ptr == NULL) \
EXIT_ERROR("Memory allocation failure")
/*
* Macro with additional safety actions to accompany realloc. If
* requested size if 0, frees the pointer. If the call to realloc
* returns NULL, terminates the program. Else, assigns the original
* pointer to the one returned by realloc.
*
* @param ptr Name of pointer to be realloc'd.
* @param size Multiple which determines size of realloc'd array
* @param type Type of array element
*/
#define REALLOC_SAFE(ptr, size, type) \
do { \
if (size == 0) \
FREE_SAFE(ptr); \
else { \
type* p = realloc(ptr, (size) * sizeof(type)); \
if (p == NULL) \
EXIT_ERROR("Memory reallocation failure"); \
ptr = p; \
} \
} while(0)
/*
* Macro which assigns the pointer to null after freeing. Useful
* to avoid dangling pointers.
*
* @param ptr Name of pointer to free.
*/
#define FREE_SAFE(ptr) \
do { \
free(ptr); \
ptr = NULL; \
} while(0)
Next, let us define the header with macros used to generate declarations and definitions, respectively. Also, note that a benefit of this is to make the data type opaque.
// dynarr.h
#ifndef DYNARR_H
#define DYNARR_H
#include <stdlib.h>
#include "utils.h"
/*
* Macro which generates declarations of dynamic array functions,
* along with its opaque type declaration.
*
* @param DYNARRAY_TYPE_NAME Name of the dynamic array type.
* @param DYNARRAY_ITEM_TYPE Type of the item which the dynamic array
* shall contain.
*/
#define GEN_DYNARR_DECL(DYNARRAY_TYPE_NAME, DYNARRAY_ITEM_TYPE) \
typedef struct DYNARRAY_TYPE_NAME DYNARRAY_TYPE_NAME; \
DYNARRAY_TYPE_NAME* DYNARRAY_TYPE_NAME##_create (void); \
void DYNARRAY_TYPE_NAME##_destroy (DYNARRAY_TYPE_NAME** d); \
void DYNARRAY_TYPE_NAME##_append (DYNARRAY_TYPE_NAME* d, DYNARRAY_ITEM_TYPE item); \
/*
* Macro which generates definitions of dynamic array functions,
* along with its type definition.
*
* @param DYNARRAY_TYPE_NAME Name of the dynamic array type.
* @param DYNARRAY_ITEM_TYPE Type of the item which the dynamic array
* shall contain.
*/
#define GEN_DYNARR_DEF(DYNARRAY_TYPE_NAME, DYNARRAY_ITEM_TYPE) \
struct DYNARRAY_TYPE_NAME { \
size_t capacity; \
size_t size; \
DYNARRAY_ITEM_TYPE* arr; \
}; \
\
DYNARRAY_TYPE_NAME* DYNARRAY_TYPE_NAME##_create (void) { \
DYNARRAY_TYPE_NAME* d = MALLOC_SAFE(d, 1, DYNARRAY_TYPE_NAME); \
d->capacity = 0; \
d->size = 0; \
d->arr = NULL; \
return d; \
} \
\
void DYNARRAY_TYPE_NAME##_destroy (DYNARRAY_TYPE_NAME** d) { \
if ((*d) == NULL) \
return; \
FREE_SAFE((*d)->arr); \
FREE_SAFE((*d)); \
} \
\
void DYNARRAY_TYPE_NAME##_append (DYNARRAY_TYPE_NAME* d, DYNARRAY_ITEM_TYPE item) { \
if (d->capacity == 0) {\
d->capacity = 8; \
REALLOC_SAFE(d->arr, d->capacity, DYNARRAY_ITEM_TYPE); \
} \
else if (d->size == d->capacity) { \
size_t new_cap = 2 * d->capacity; \
REALLOC_SAFE(d->arr, new_cap, DYNARRAY_ITEM_TYPE); \
d->capacity = new_cap; \
} \
d->arr[d->size] = item; \
d->size++; \
}
#endif
Finally, let us actually use this. In the following header, we define two types of arrays:
// arrays.h
#ifndef ARRAYS_H
#define ARRAYS_H
#include "dynarr.h"
GEN_DYNARR_DECL(doublearr, double);
GEN_DYNARR_DECL(intarr, int);
#endif
Next, let us accompany the header with the following file:
// arrays.c
#include "arrays.h"
GEN_DYNARR_DEF(doublearr, double);
GEN_DYNARR_DEF(intarr, int);
Now, we can use it in a "dummy" main program:
// main.c
#include "arrays.h"
int main(void) {
doublearr* a1 = doublearr_create();
doublearr_append(a1, 1.1);
doublearr_append(a1, 2.2);
doublearr_destroy(&a1);
intarr* a2 = intarr_create();
intarr_append(a2, 1);
intarr_append(a2, 2);
intarr_destroy(&a2);
return 0;
}
Of course, such an array is not yet useful, however, my main focus here is the concept.