0

I came across this code, and I can't really figure out how/why it works:

void cistore(int bucketsize, int data[][bucketsize])
{
}

int main()
{
    return 0;
}

What exactly is going on here? I'd expect the C compiler (in this case gcc) to only allow this if bucketsize is determinable at compile-time. But even when there is no way of knowing bucketsize at compile time, gcc doesn't complain. How does gcc handle this?

2 Answers2

3

What exactly is going on here?

Since C99, C has had support for variable-length arrays, whose lengths are determined at runtime. In C99 it was a mandatory feature, but since C11 it has been an optional feature. Many modern C compilers support it, with the notable exception of Microsoft's.

In

void cistore(int bucketsize, int data[][bucketsize])

, data is declared as a pointer to an array of bucketsize elements of type int. The pointed-to object is a variable-length array, at least from the perspective of the function.

I'd expect the C compiler (in this case gcc) to only allow this if bucketsize is determinable at compile-time.

Surprise!

But even when there is no way of knowing bucketsize at compile time, gcc doesn't complain. How does gcc handle this?

How would GCC handle it if the pointed-to array had explicit length? As far as the "how" goes, I don't expect that GCC needs to make too many adjustments.

Or if you're asking about the semantics, they are pretty much what one would expect once they get over any shock about VLAs being a thing. The pointed-to object is an array whose length on any given call to the function is specified by the value of the bucketsize argument. That can differ from call to call.

Here's an extended version of your example code that demonstrates:

void cistore(int bucketsize, int data[][bucketsize])
{
}

int main()
{
    int d1[5][5];
    int d2[4][6];
    int (*d3)[42] = NULL;

    cistore(5, d1);
    cistore(6, d2);
    cistore(42, d3);

    return 0;
}
John Bollinger
  • 160,171
  • 8
  • 81
  • 157
1

The variable data is a variable length array, which do not necessarily need to have a integer constant size when declared as a function parameter:

If expression is not an integer constant expression, the declarator is for an array of variable size.

In fact, the size is implicitly ignored at function prototype scope:

If the size is *, the declaration is for a VLA of unspecified size. Such declaration may only appear in a function prototype scope, and declares an array of a complete type. In fact, all VLA declarators in function prototype scope are treated as if expression were replaced by *.

("Expression" is what is usually between the square brackets and specifies the size.)

In other words, at a function prototype, the size does not need to be known at all and still the type is considered to be complete. Only in the function definition does the size need to be specified as some non-constant integer expression. In the call to the function, the integer expression may be const or non-const I would imagine this (effectively) works similarly to when VLAs are declared and defined in a loop, i.e.:

Each time the flow of control passes over the declaration, expression is evaluated (and it must always evaluate to a value greater than zero), and the array is allocated (correspondingly, lifetime of a VLA ends when the declaration goes out of scope).

(Emphasis mine.)

...except that the size is now evaluated at the time of the function call.

In your particular case, you basically have a variably-modified type:

Variable-length arrays and the types derived from them (pointers to them, etc) are commonly known as "variably-modified types" (VM). Objects of any variably-modified type may only be declared at block scope or function prototype scope.

And yes, the code does compile. An example:

#include <inttypes.h>
#include <stdio.h>

struct data_el
{
    int n;
};

// Function prototype. Not specifying a size is allowed
void cistore(uint32_t bucketsize, struct data_el data[][*]);

// Function definition.
// Must specify the VLA's size as a non-constant integer expression
void cistore(uint32_t bucketsize, struct data_el data[][bucketsize])
{
    (*data)[1].n = 5;
    printf("%d\n", (*data)[1].n);
}

int main(void)
{
    // Function call. Use any (const or non-const) integer expression.
    uint32_t bucketsize = 2U;
    struct data_el data[bucketsize];
    cistore(bucketsize, &data);
}

Interestingly, Clang compiles this without warnings, whereas GCC warns about type conflicts:

vlaquestion.c:14:50: warning: argument 2 of type ‘struct data_el[][bucketsize]’ declared as a variable length array [-Wvla-parameter]                                                                        
   14 | void cistore(uint32_t bucketsize, struct data_el data[][bucketsize])                                                                                                                                 
      |                                   ~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~                                                                                                                                  
vlaquestion.c:10:50: note: previously declared as an ordinary array ‘struct data_el[][0]’                                                                                                                    
   10 | void cistore(uint32_t bucketsize, struct data_el data[][*]);                                                                                                                                         
      |                                   ~~~~~~~~~~~~~~~^~~~~~~~~

And finally, some useful reading on the somewhat controversial VLAs: Why aren't variable-length arrays part of the C++ standard? At some point, even an effort was made to make the Linux kernel VLA free.

Yun
  • 3,056
  • 6
  • 9
  • 28