Defining a C function that takes a 2D array with variables for dimension sizes

Viewed 117

Edit: Turns out the compiler I'm using doesn't support variable length arrays so I have no way of achieving the notation I desire using MSVC


I have a function which takes in an array of strings and a query string, and returns the index of the string in the array that matches the query.

int findStringIndex(char query[], int strLength, char* strArray, int numStrings) {
    for (int i = 0; i < numStrings; i++) {
        for (int j = 0; j < strLength; j++) {

            // Skip to next word if there is a mismatch
            if (query[j] != *(strArray+ (i * strLength) + j))
                break;

            if (query[j] == '\0' && *(strArray + (i * strLength) + j) == '\0')
                return i;
        }
    }
    return -1;
}

Notably, both the length of the string and the size of the array vary, since I am using this function in several different places with differently sized strings. Currently, this approach has two problems:

  • Ugly array access notation *(strArray+ (i * strLength) + j)) rather than something like strArray[i][j]
  • When I call the function and pass the array of strings as the third argument, I get the warning that the argument I pass "differs in levels of indirection" from char*

Is there a way for me to tell the compiler to accept a variable as the size of one of the array's axes so that I can use the notation strArray[i][j]?

Also, how should I define the function so that I don't get the "levels of indirection" warning?

Edit: As a clarification, the string arrays are not ragged. They have constant sized dimensions, but different arrays that I want to use the function on have different sizes. The code runs fine and achieves the desired behavior in its current state, I just want to make sure I'm writing things the right way

Here are two examples (different string sizes) of arrays I might use with this function:

char instructionStrings[NUM_INSTRUCTIONS][INST_MAX_CHARS] = {
    "nop", "lit", "litn", "copy", "copyl", "asni", /* etc */
};

char typeStrings[NUM_TYPES][TYPE_MAX_CHARS] = {
    "null", "int8", "int16", "int32", "int", "real32", "real"
};

Where INST_MAX_CHARS and TYPE_MAX_CHARS are different values. Then I would call the function like findStringIndex(userInput, TYPE_MAX_CHARS, typeStrings, NUM_TYPES); for the second example

3 Answers

If your compiler supports variable length arrays then the function can be declared and defined the following way as it is shown in the demonstrative program below. Note that not all compilers support variable length arrays (notably MSVC), in which case there is no way to get the desired notation.

#include <stdio.h>
#include <string.h>

size_t findStringIndex( size_t m, size_t n, char a[m][n], const char *s ) 
{
    size_t i = 0;

    while ( i < m && !( strcmp( a[i], s ) == 0 ) ) ++i;
    
    return i;
}

int main(void) 
{
    enum { M1 = 3, N1 = 10 };
    
    char a1[M1][N1] =
    {
        "Hello", "World", "Everybody"
    };
    
    const char *s = "Hello";
    
    size_t pos = findStringIndex( M1, N1, a1, s );
    
    if ( pos != M1 )
    {
        printf( "\"%s\" is found at position %zu.\n", s, pos );
    }
    else
    {
        printf( "\"%s\" is not found.\n", s );
    }
    
    s = "World";
    
    pos = findStringIndex( M1, N1, a1, s );
    
    if ( pos != M1 )
    {
        printf( "\"%s\" is found at position %zu.\n", s, pos );
    }
    else
    {
        printf( "\"%s\" is not found.\n", s );
    }
    
    s = "Everybody";
    
    pos = findStringIndex( M1, N1, a1, s );
    
    if ( pos != M1 )
    {
        printf( "\"%s\" is found at position %zu.\n", s, pos );
    }
    else
    {
        printf( "\"%s\" is not found.\n", s );
    }
    
    s = "Bye";
    
    pos = findStringIndex( M1, N1, a1, s );
    
    if ( pos != M1 )
    {
        printf( "\"%s\" is found at position %zu.\n", s, pos );
    }
    else
    {
        printf( "\"%s\" is not found.\n", s );
    }
    
    return 0;
}

The program output is

"Hello" is found at position 0.
"World" is found at position 1.
"Everybody" is found at position 2.
"Bye" is not found.
  1. Use the correct type for sizes : size_t

  2. You can use "normal" indexes by using pointers to arrays.

int findStringIndex(char query[], size_t strLength, char (*strArray)[strLength], size_t numStrings) {
    for (size_t i = 0; i < numStrings; i++) {
        for (size_t j = 0; j < strLength; j++) {

            // Skip to next word if there is a mismatch
            if (query[j] != strArray[i][j])
                break;
    /* ..... */

I assume that you pass the 2D char array (not array of pointers)

Other answers have covered how you could get the array access syntax you want using variable length arrays (VLA).

If you are working on systems without VLA support, you probably need to continue to use the implementation close to what you have already shown.

However, there are a few workarounds.

Workaround 1: Make all the string sizes the same

If you are using this function on typically small strings. , then instead of allowing INST_MAX_CHARS and TYPE_MAX_CHARS to be different values, stipulate that all arrays passed to this function have to have the same value for the second dimension. So, in practice, it would be the max of all the string lens over instruction strings and type strings. (You may have to implement your own MAX macro.)

#define X_MAX_CHARS MAX(INST_MAX_CHARS, TYPE_MAX_CHARS)

char instructionStrings[NUM_INSTRUCTIONS][X_MAX_CHARS] = {
    "nop", "lit", "litn", "copy", "copyl", "asni", /* etc */
};

char typeStrings[NUM_TYPES][X_MAX_CHARS] = {
    "null", "int8", "int16", "int32", "int", "real32", "real"
};

Then, your function could look like:

int findStringIndex(char query[], char (* strArray)[X_MAX_CHARS], int numStrings) {
    for (int i = 0; i < numStrings; i++) {
        if (strcmp(query, strArray[i]) == 0) return i;
    }
    return -1;
}

Workaround 2: Use _Generic

Since C 2011, C has defined a type selection mechanism call _Generic. Clang and GCC have supported it since the feature was introduced, and so do recent versions of MSVC. Obviously, if you are not using at least version Visual Studio 2019 version 16.8 Preview 3, then you will not have this feature.

Using _Generic, you could detect the size of the second dimension, and call a distinct function to use it.

#define findStringIndex(Q, A, N) \
        _Generic((A), \
            const char (*)[INST_MAX_CHARS] : findStringIndex_I(Q, A, N), \
            const char (*)[TYPE_MAX_CHARS] : findStringIndex_T(Q, A, N), \
            default                        : -1)

Where, findStringIndex_I and findStringIndex_T are each defined to handle arrays of strings that they know how to support. You could create a macro to automate the creation of the function, in case you need to add many such functions.

#define DEFINE_FIND_STRING_INDEX(SUFFIX, STRING_SZ) \
        int findStringIndex_ ## SUFFIX ( \
                char query[], \
                char (* strArray)[STRING_SZ], \
                int numStrings) { \
            for (int i = 0; i < numStrings; i++) { \
                if (strcmp(query, strArray[i]) == 0) return i; \
            } \
            return -1; \
        }

DEFINE_FIND_STRING_INDEX(I, INST_MAX_CHARS)
DEFINE_FIND_STRING_INDEX(T, TYPE_MAX_CHARS)

Workaround 3:

While not as generic as _Generic, since you are only dealing with the size of strings, you could accomplish the same thing using conditional expressions. By selecting on the size of the first element of the provided array, which essentially gives you the size of the second dimension, you can determine the appropriate function to call.

#define findStringIndex(Q, A, N) \
        ((sizeof((A)[0]) == INST_MAX_CHARS) ? findStringIndex_I(Q, A, N) : \
        ((sizeof((A)[0]) == TYPE_MAX_CHARS) ? findStringIndex_T(Q, A, N) : \
        -1))

As with _Generic, the individual functions to be called are implemented separately.

Related