1

I'm looking into vectors in C, i.e. dynamically resizable arrays. I've implemented one that handles a char array.

But if I want the same functionality for a different type of array -- int, long etc- I'd have to do the whole thing over, as well as use different names for them not to collude given that they'd be in the same namespace.

My question is: is there any way to handle either an int array or a char array depending on a certain argument passed to the intializer function?

So say I have a struct Vector with a member that's a char pointer. As it stands, when I run the init function, it'll allocate memory to this char pointer for use as a char array.

Can I make it so that, depending on a certain argument passed to init, it will allocate memory for an int array instead of a char array, for example?

I've been thinking about this and it would involve using void pointers and casting in a bunch of places. Is there any other way/what's the best way to go about this?

Daniel
  • 249
  • 2
  • 11
  • Are you asking for a way to improve something you've written that already works? If so, post it on [Code Review](https://codereview.stackexchange.com/). But if you do, I suggest you post the code, not its description. Take a minute and read [how to ask a good question](https://stackoverflow.com/help/how-to-ask). – ryyker Aug 17 '20 at 13:02
  • 1
    Can use a `union` of pointers. – kaylum Aug 17 '20 at 13:05
  • @kaylum that sounds like something that might work. Thanks – Daniel Aug 17 '20 at 13:09

2 Answers2

3

It cannot be done in a very neat way, but it is possible. Here is an example:

enum vec_type { CHAR, INT, DOUBLE };

struct vector {
    enum vec_type vec_type;
    void *data;
};

struct vector *vec_realloc(struct vector *vec, enum vec_type vec_type, size_t n)
{
    size_t elsize;
    switch(vec_type) {
        case CHAR:   elsize = sizeof(char);   break;
        case INT:    elsize = sizeof(int);    break;
        case DOUBLE: elsize = sizeof(double); break;
    }
    struct vector *ret = realloc(vec, elsize * n);
    return ret;
}

And yes, I know that sizeof(char) always evaluates to 1, but in this case I think it looks much better.

klutt
  • 30,332
  • 17
  • 55
  • 95
2

You could implement it with type-generic macros:

Simple example:

int* vec_alloc_int (size_t n)
{
  return malloc(sizeof(int[n]));
}

char* vec_alloc_char (size_t n)
{
  return malloc(sizeof(char[n]));
}

#define vec_alloc(type, n) _Generic( (type[]){0}[0],      \
                                    int:  vec_alloc_int,  \
                                    char: vec_alloc_char)(n)

Usage:

int*  i = vec_alloc(int, 5);
char* c = vec_alloc(char, 5);

Explanation:

The (type[]){0}[0] trick is creating a compound literal array of 1 item, then de-referencing item 1 of that array. This converts the macro parameter from a type to an object, that can be passed on to _Generic. _Generic then picks the suitable function to use.


Pros:

  • Type safer than void pointers.
  • Types are resolved at compile-time, not run-time.

Cons:

  • Must list all supported types, with separate functions per type.
  • Can't use dinosaur compilers (C90/C99).

The evil "X-macro" version (which I wouldn't really recommend) of the above would look like this:

#include <stdio.h>
#include <stdlib.h>

#define SUPPORTED_TYPES(X) \
  X(int)                   \
  X(char)                  \
  
#define define_vec_alloc(type)    \
type* vec_alloc_##type (size_t n) \
{ return malloc(sizeof(type[n])); }

SUPPORTED_TYPES(define_vec_alloc)

void* vec_alloc_dummy (size_t param){}
#define GENERIC_LIST(type) type: vec_alloc_##type,
#define vec_alloc(type, n) _Generic((type[]){0}[0], SUPPORTED_TYPES(GENERIC_LIST) default: vec_alloc_dummy)(n)

int main(void)
{
  int*  i = vec_alloc(int,  5);
  char* c = vec_alloc(char, 5);
}

Explanation here: https://stackoverflow.com/a/60454338/584518

This version is quite unreadable but avoids all forms of code repetition, and that's about the only benefit it got.

Lundin
  • 195,001
  • 40
  • 254
  • 396