8
\$\begingroup\$

I recently have been working on moving from projects written in Java and C++ to C and realized that the lack of std::vector in C makes it a bit more difficult to create multi-dimensional arrays. Also, without the std::string in C, it's harder to store arrays of strings and keep track of their sizes. To automate the process, I created a typedef'd struct: varray. varray attempts to emulate some basic functions of std::vector, e.g. storing sizes of the elements in an easily accessible location and allowing for allocation of multi-dimensional objects with "relative ease".

I've attached the header "varray.h" and a bit of sample code with comments to demonstrate its use. Please note that at this point, I haven't tried to dynamically expand the arrays, though I'll look into it as I practice more with the language.

If there is any feedback (syntax, memory allocation, anything), I'd greatly appreciate it - I'm still just getting started with C, C++, and Java.

varray.h

#ifndef VARRAY_H
#define VARRAY_H
#define VARRAY_DISPLAYDEBUG 0
#define VARRAY_STRSIZE(x) ((sizeof(char) * strlen(x)) + 1)
#include <stdlib.h>
#include <string.h>
#include <stdio.h> 

typedef struct {
    int* getInt;
    char* getChar;
    double* getDbl;
    char* str;
    int size;
} varray;

typedef enum {
    v_char = 0,
    v_int = 1,
    v_double = 2,
    v_varray = 3,
    v_tdvarray = 4
} varrayType;

void* createArray(varrayType arrayType, int arraySize);
varray varrayPush(const char* data);
varray allocateNumArray(varrayType type, const int size);
varray allocateCharArray(const int size);
varray* allocateVarray(const int size);
varray** allocateTDVarray(const int size);

inline void* createArray(varrayType arrayType, int arraySize) {
    varray* target;
    varray** vtarget;
    varray*** tdvtarget;
    if (arrayType == v_char) {
        target = malloc(sizeof(varray) * arraySize);
        *target = allocateCharArray(arraySize);
        return target;
    }
    else if (arrayType == v_int || arrayType == v_double) {
        target = malloc(sizeof(varray) * arraySize);
        *target = allocateNumArray(arrayType, arraySize);
        return target;
    }
    else if (arrayType == v_varray) {
        vtarget = malloc(sizeof(varray*) * arraySize);
        *vtarget = allocateVarray(arraySize);
        return *vtarget;
    }
    else if (arrayType == v_tdvarray) {
        tdvtarget = malloc(sizeof(varray**) * arraySize);
        *tdvtarget = allocateTDVarray(arraySize);
        return *tdvtarget;
    }
    else {
        return NULL;
    }
}   

inline varray varrayPush(const char* data) {
    varray target;
    if (VARRAY_DISPLAYDEBUG) {
        printf("%s%d%s%s\n", "Allocating array with size: ", (int)VARRAY_STRSIZE(data) - 1, " with contents: ", data);
    }
    target.str = malloc(VARRAY_STRSIZE(data));
    strcpy(target.str, data);
    if (VARRAY_DISPLAYDEBUG) {
        printf("%s%s\n", "String created successfully. Contents reads: ", target.str);
        printf("%s%d\n", "Memory address: ", (int)target.str);
    }
    target.size = VARRAY_STRSIZE(data);
    return target;
}

inline varray allocateNumArray(varrayType type, const int size) {
    int i;
    varray target;
    if (type == v_int) {
        if (VARRAY_DISPLAYDEBUG) {
            printf("%s%d\n", "Allocating array of type v_int with size ", size);
        }
        target.getInt = malloc(sizeof(int) * size);
        for (i = 0; i < size; i++) {
            target.getInt[i] = 0;
        }
    }
    else if (type == v_double) {
        if (VARRAY_DISPLAYDEBUG) {
            printf("%s%d\n", "Allocating array of type v_double with size ", size);
        }
        target.getDbl = malloc(sizeof(double) * size);
        for (i = 0; i < size; i++) {
            target.getDbl[i] = 0.0;
        }
    }
    target.size = size;
    return target;
}

inline varray allocateCharArray(const int size) {
    varray target;
    int i;
    if (VARRAY_DISPLAYDEBUG) {
         printf("%s%d\n", "Allocating array of type v_char with size ", size);
    }
    target.getChar = malloc(sizeof(char) * size);
    for (i = 0; i < size; i++) {
        target.getChar[i] = 0;
    }
    target.size = size;
    return target;
}

inline varray* allocateVarray(const int size) {
    varray* target = malloc(sizeof(varray) * (size + 1));
    if (VARRAY_DISPLAYDEBUG) {
        printf("%s%d\n", "Allocated array of type v_varray with size ", size);
    }
    target[0].size = size;
    return target;
}

inline varray** allocateTDVarray(const int size) {
    varray** target = malloc(sizeof(varray*) * (size + 1));
    if (VARRAY_DISPLAYDEBUG) {
        printf("%s%d\n", "Allocated array of type v_tdvarray with size ", size);
    }
    target[0] = malloc(sizeof(varray));
    target[0][0].size = size;
    return target;
}

#endif

Example implementation

#include "varray.h"

void varrayDemo() {
    varray sampleInt;
    /*
     * Functions as a normal array but also stores
     * the size of the array at sampleInt.size.
     * Accessing data elements used the syntax
     * VARRAY.getInt[POSITION]
     */
    varray* sampleString;
    /*
     * Since a C string is an array of char, creating
     * an array of varray objects can easily store multiple
     * strings. The size of the varray array can be accessed
     * via VARRAY[0].size Individual elements can be 
     * accessed at VARRAY[POSITION].str with position >= 1
     */
    varray** sampleContainer;
    /*
     * The container of varrays can store multiple data types
     * at each element's position. For example, the syntax
     * VARRAY[1][1].str will return the string, if stored.
     * while VARRAY[2][2].getDbl[POSITION] returns the double
     */
    sampleInt = *(varray*)createArray(v_int, 5);
    /*
     * The function createArray(TYPE, SIZE) initializes the array
     * NOTE: I am not sure if there is a more "aesthetically pleasing"
     * way to initialize the array, since createArray returns a void
     * pointer. When initialized, all values are set to 0
     */
    printf("The size of sampleInt is: %d\n", sampleInt.size);
    printf("The data at position 0 is: %d\n", sampleInt.getInt[0]);

    sampleString = createArray(v_varray, 2);
    /*
     * Each varray can contain one string. Initialize each element
     * with the createArray command.
     */
    printf("The size of sampleString is: %d\n", sampleString[0].size);
    /*
     * As noted above, the size is stored in a varray at position 0
     * in the container
     */
    sampleString[1] = varrayPush("This is a sample string");
    sampleString[2] = varrayPush("This is also a sample string!"); 
    /*
     * To store a string within a varray, the function varrayPush is used
     * with the desired string as the argument. The function initializes
     * another varray object within the container.
     */
    printf("The string at position 1 is: %s\n", sampleString[1].str);
    printf("The size of the string stored at position 1 is: %d\n", sampleString[1].size); 
    printf("The char at position 5 in sampleString[1] is: %c\n", sampleString[1].str[5]);

    sampleContainer = createArray(v_tdvarray, 2);
    sampleContainer[1] = createArray(v_varray, 1);
    sampleContainer[1][1] = *(varray*)createArray(v_double, 5);
    sampleContainer[1][1].getDbl[4] = 3.14;
    sampleContainer[2] = createArray(v_varray, 1);
    sampleContainer[2][1] = varrayPush("yet another sample string");
    /*
     * As noted with the original sampleInt example, the *(varray*)
     * syntax is used again.
     */
    printf("sampleContainer[1][1] has size: %d with data %lf at position 4\n", sampleContainer[1][1].size, sampleContainer[1][1].getDbl[4]);
    printf("sampleContainer[2][1] has size %d with string \"%s\"\n", sampleContainer[2][1].size, sampleContainer[2][1].str); 
}
\$\endgroup\$
2
  • \$\begingroup\$ Committed some changes suggested by @vnp Fixed initial allocation of memory in createArray, changed size from type int to size_t, implemented calloc to zero out memory in arrays, and used strdup in the varrayPush function. \$\endgroup\$ Commented Feb 10, 2016 at 3:18
  • \$\begingroup\$ Created the following with the updated code (suggestions from vnp) GitHub/vector.h \$\endgroup\$ Commented Feb 10, 2016 at 3:36

3 Answers 3

7
\$\begingroup\$
  • Too much memory

          target = malloc(sizeof(varray) * arraySize);
          *target = allocateCharArray(arraySize);
    

    allocates that many varray entries, and uses just one of them. If you insist on double indirection,

          target = malloc(sizeof(varray));
    

    is just enough. OTOH I don't see a need for a double indirection at all.

  • All pointers smell the same

    There is no need to have pointers to all possible types. A single void * data would serve the same purpose. You should have getWhatever as accessor functions. You may want to associate the accessor function with the varray instance, eg:

          typedef struct varray {
              void * (*get)(struct varray * v, size_t index);
              void * data;
              size_t size;
          }
    
          struct varray * allocateCharArray(size_t size)
          {
              ....
              v->get = vGetChar;
          }
    

    where

          void * vGetChar(varray * v, size_t index) {
              return (char *) v->data + index;
          }
    
  • Missing deallocations

    Client has no resources to deallocate varrays.

  • Use standard library

    To zero out allocated memory, use calloc. To copy a string, use strdup.

\$\endgroup\$
4
  • \$\begingroup\$ I committed some of the suggestions provided; however, I'm not entirely sure that I understand the meaning of void * (*get)(struct varray * v, size_t index). If using getWhatever, what would the syntax be for setting and accessing the elements? - Thanks \$\endgroup\$ Commented Feb 10, 2016 at 3:21
  • \$\begingroup\$ strdup() is not a Standard C function. It's in POSIX, which might be sufficiently portable, but that's not a given. \$\endgroup\$ Commented Sep 25, 2023 at 12:51
  • 1
    \$\begingroup\$ vnp, "All pointers smell the same" --> Yes for the types OP has, yet for growth: void * is not certainly sufficient for a function pointer. \$\endgroup\$ Commented Sep 30, 2023 at 15:18
  • \$\begingroup\$ @TobySpeight strdup() will certainly become a standard function in C23, so we have that future standard option. \$\endgroup\$ Commented Sep 30, 2023 at 15:20
4
\$\begingroup\$

C99 enables you to use flexible array members in struct-ures. I believe they are more suitable for your needs.

Perhaps you might consider

struct chararray {
  varrayType vtype; // when vtype is vchar 
  unsigned len;
  char chararr[];
};
struct intarray {
  varrayType vtype; // when vtype is vint
  unsigned len;
  int intarr[];
};
/// etc...
union varrayptr {
  varrayType* vtypep;
  chararrar* charrp;
  intarray* intarrp;
};

As an example, here is a safe accessor to the n-th char

int safe_nth_char (union varrayptr vp, int rk) {
  if (!vp.vtypep || *vp.vtypep != vchar) return EOF;
  unsigned l = vp->charrp.len;
  if (rk < 0  || rk >= (int)l) return EOF;
  return vp->charrp.chararr[rk];
}

Read more about tagged unions.

But I don't understand what exactly do you want. Some "universal" any type?

Of course, with flexible array members, you would reallocate a new pointer, not reuse the same one, when you need to grow the array.

Another possibility is to generate some C code (using some tool, either home grown, or a preprocessor like GPP or GNU m4). On Linux and most POSIX system, you could even generate some temporary C code at runtime, compile it as a plugin, and dlopen (and dlsym) that plugin. In practice, on Linux this can be done many thousands of time. A variant could be to generate machine code at runtime using libgccjit or (in C++ asmjit) or GNU lightning.

At last, C++ has been designed as interoperable with C. You can call C++ code (extern "C") from C code. You could use some conservative garbage collector, e.g. Boehm-GC.

\$\endgroup\$
1
  • \$\begingroup\$ flexible array members would make resizing the array difficult. It's memory address would change \$\endgroup\$
    – nishantjr
    Commented Feb 10, 2016 at 8:53
2
\$\begingroup\$

There's no way to enable the logging without modifying the header.

Consider allowing VARRAY_DISPLAYDEBUG to be set before inclusion (to enable for specific translation units, or to set it via compiler options in debugging builds):

#ifndef VARRAY_DISPLAYDEBUG
#define VARRAY_DISPLAYDEBUG 0
#endif

When it's enabled, debugging output should go to stderr rather than mixed in with program output on stdout.


The sizeof operator measures in units of char, so multiplying by sizeof (char) is always a no-op - you can remove the visual clutter in the definition of VARRAY_STRSIZE().


The malloc() function may return a null pointer (yes, even on Linux, depending on ulimit values), and this code is littered with undefined behaviour wherever that happens. You must not dereference the pointer you get back until you have confirmed it's valid. Use a static analyser (e.g. gcc -fanalyzer) to make sure you fix them all.


Consider using _Generic functions. I haven't used this feature of modern C yet, but it's probably worth looking into for type-agnostic code such as this.

\$\endgroup\$

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.