4
\$\begingroup\$

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 and false keywords
  • free_sized() from <stdlib.h>
  • = {} empty initializer
  • function() empty function argument declaration
  • typeof operator
  • N3003 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

\$\endgroup\$
2
  • \$\begingroup\$ Your vector_delete appears to use the length field after it is set to 0 in a call to free_sized. I believe it should be using capacity not length, 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\$
    – aghast
    Dec 25, 2023 at 11:29
  • \$\begingroup\$ @aghast Nice catch. The free_sized() polyfill prevented the compiler from warning me about that bug. \$\endgroup\$
    – Biggs
    Dec 26, 2023 at 11:22

1 Answer 1

5
\$\begingroup\$

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.

\$\endgroup\$
5
  • \$\begingroup\$ vector_abort() cannot be a void statement because it is used in the vector_at() ternary \$\endgroup\$
    – Biggs
    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\$ Jun 23, 2023 at 6:52
  • 2
    \$\begingroup\$ @Biggs Note: _Noreturn function specifier is deprecated. [[noreturn]] attribute should be used instead. The macro noreturn is also deprecated. (Since C23) \$\endgroup\$
    – Harith
    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
    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 a return. See Can I use [[noreturn]] on non-void returning functions? That's a C++ question, but the same reasoning applies to C. \$\endgroup\$ Jun 27, 2023 at 10:48

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.