I'm trying to implement a simple type-generic Vector with macros using the newest features of C23. Looking for any advice about macro design and pitfalls because I generally avoid macros at all costs. Wondering how to extend this code to handle more complex data types as well.
Also, I'm not particularly happy with the hacky int vector_abort()
used in the vector_at()
ternary. Any portable advice here is greatly appreciated. Furthermore, if anyone has a way to make opaque structures with macros I would be very interested.
A major problem that I immediately notice is that there isn't an easy way to tell if vector_push()
was successful. However, return values aren't permitted for macros and calling abort()
or exit()
doesn't seem great in this instance.
C23 features used:
nullptr
constant from<stddef.h>
true
andfalse
keywordsfree_sized()
from<stdlib.h>
= {}
empty initializerfunction()
empty function argument declarationtypeof
operatorN3003
Improved Rules for Tag Compatibility
vector.h
#ifndef VECTOR_H
#define VECTOR_H
#include <errno.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int vector_abort(void) {
errno = EINVAL;
fprintf(stderr, "ERROR: %s.\n" "Invalid vector index.\n", strerror(errno));
errno = 0;
abort();
return 0;
}
#define vector(TYPE) struct vector_##TYPE {size_t length; size_t capacity; TYPE* array;}
#define vector_new(TYPE, CAPACITY) {.length = 0, .capacity = CAPACITY, .array = calloc(CAPACITY, sizeof(TYPE))}
#define vector_delete(VECTOR) \
do { \
VECTOR.length = 0; \
VECTOR.capacity = 0; \
free_sized(VECTOR.array, (VECTOR.length * sizeof(*(VECTOR.array)))); \
VECTOR.array = nullptr; \
} while (false)
#define vector_size(VECTOR) VECTOR.length
#define vector_at(VECTOR, INDEX) (VECTOR.length > INDEX) ? VECTOR.array[INDEX] : vector_abort()
#define vector_pop(VECTOR) if (VECTOR.length > 0) VECTOR.length--
#define vector_push(VECTOR, VALUE) \
do { \
if (VECTOR.length == VECTOR.capacity) { \
typeof(VECTOR.array) new_array = realloc(VECTOR.array, (VECTOR.capacity * 2 * sizeof(*(VECTOR.array)))); \
if (!new_array) break; \
VECTOR.capacity *= 2; \
VECTOR.array = new_array; \
} \
VECTOR.array[VECTOR.length] = VALUE; \
VECTOR.length++; \
} while (false)
#endif
main.c
#include "./vector.h"
#include <stdio.h>
int main(void) {
vector(int) v = {}; // or use = vector_new(type, capacity)
vector_push(v, 1);
vector_push(v, 2);
vector_push(v, 3);
for (size_t i = 0; i < vector_size(v); ++i) {
printf("Test Push %d\n", vector_at(v, i));
}
vector_pop(v);
for (size_t i = 0; i < vector_size(v); ++i) {
printf("Test Pop %d\n", vector_at(v, i));
}
vector_delete(v);
for (size_t i = 0; i < vector_size(v); ++i) {
printf("Test Delete %d\n", vector_at(v, i));
}
return EXIT_SUCCESS;
}
Polyfill
Here is a free_sized()
polyfill if your implementation doesn't support it yet.
void free_sized(void* ptr, size_t /*size*/) {
free(ptr);
}
Compiler Flags
-Wall
-Wextra
-Werror
-pedantic-errors
-std=c2x
1 Answer 1
Improve macro safety
You seem to have hit the usual pitfalls of macros - remember these are text substitutions done by the C preprocessor, so we need to ensure that the the rules of precedence don't give an unexpected meaning to the expansion.
For example, consider vector_pop
:
#define vector_pop(VECTOR) if (VECTOR.length > 0) VECTOR.length--
Since .
binds very tightly and if
can catch a following else
, we really need extra parens and a do
/while (0)
:
#define vector_pop(VECTOR) \
do { if ((VECTOR).length > 0) (VECTOR).length--; } while (false)
Similarly, vector_size
should be defined to expand to ((VECTOR).length)
rather than to the much less safe VECTOR.length
.
Improve the "abort" function
Shouldn't vector_abort()
be static
? At present, it seems that we'll get a definition in each translation unit that includes the header, causing link-time errors.
This function has an unreachable return
statement. The assignment of errno = 0
doesn't have any discernable value - unless you have a signal handler that uses it, which seems a poor choice.
I'd write:
static _Noreturn void vector_abort(void)
{
fprintf(stderr, "ERROR: %s.\nInvalid vector index.\n", strerror(EINVAL));
abort();
}
Handle allocation failures correctly
vector_push
provides no feedback when realloc()
fails - the calling program is required to test the vector size before and after (this checking is conspicuously absent from the demo program, suggesting others will forget this too).
vector_new
violates the invariant (that array[capacity-1]
is valid) whenever calloc()
fails.
-
\$\begingroup\$
vector_abort()
cannot be a void statement because it is used in thevector_at()
ternary \$\endgroup\$Biggs– Biggs2023年06月23日 06:41:57 +00:00Commented Jun 23, 2023 at 6:41 -
3\$\begingroup\$ Ah yes - it probably instead needs a comment explaining why the unreachable
return
is present, to help readers like me! \$\endgroup\$Toby Speight– Toby Speight2023年06月23日 06:52:54 +00:00Commented Jun 23, 2023 at 6:52 -
2\$\begingroup\$ @Biggs Note:
_Noreturn
function specifier is deprecated.[[noreturn]]
attribute should be used instead. The macronoreturn
is also deprecated. (Since C23) \$\endgroup\$Madagascar– Madagascar2023年06月26日 07:30:51 +00:00Commented Jun 26, 2023 at 7:30 -
\$\begingroup\$ @Haris As stated before,
[[noreturn]]
would cause undefined behavior if applied to a function with a return statement. The function must return due to its use in a ternary statement. Good catch though about the new syntax \$\endgroup\$Biggs– Biggs2023年06月26日 14:34:48 +00:00Commented Jun 26, 2023 at 14:34 -
\$\begingroup\$ @Biggs, it's perfectly fine for a
[[noreturn]]
function to have a return type, as long as it doesn't actually reach areturn
. See Can I use[[noreturn]]
on non-void returning functions? That's a C++ question, but the same reasoning applies to C. \$\endgroup\$Toby Speight– Toby Speight2023年06月27日 10:48:34 +00:00Commented Jun 27, 2023 at 10:48
vector_delete
appears to use thelength
field after it is set to 0 in a call tofree_sized
. I believe it should be usingcapacity
notlength
, and should be using them before setting them to zero. (Which, if they're both set to zero it doesn't really matter which you use, does it? But still...) \$\endgroup\$