0

I found a very strange bit of code in Apple's CoreAudioBaseTypes.h no less,

struct AudioBufferList
{
    UInt32      mNumberBuffers;
    AudioBuffer mBuffers[1]; // this is a variable length array of mNumberBuffers elements
};

My question is how does Apple make mBuffers variable length (indeed they do if you use the code it works).

I tried the following sample and I get compilation error "Array type 'int [1]' is not assignable"

struct Object
{
    int size;
    int array[1]; // this is a variable length array!!
};

#include <iostream>

int main(int argc, const char * argv[]) {
    Object o;
    o.size = 2;
    o.array = (int*)malloc( o.size*sizeof(int) ); //"Array type 'int [1]' is not assignable"
}
bobobobo
  • 64,917
  • 62
  • 258
  • 363
  • 2
    You don't allocate memory for the array. You over-allocate memory for the `Object`, e.g. `struct Object *o = malloc(sizeof(struct Object) + (size-1)*sizeof(int));` – user3386109 Dec 19 '20 at 01:08
  • @user3386109 ewww!!!!!!!!!!! why would you do it this way and not just using a straight pointer for the array? – bobobobo Dec 19 '20 at 01:08
  • 2
    See https://en.wikipedia.org/wiki/Flexible_array_member. This doesn't actually use a C99 flexible array member (presumably to support C89 or C++), but the idea is the same. – interjay Dec 19 '20 at 01:10
  • 3
    Search for `[c] flexible array member` for umpteen duplicates. – user3386109 Dec 19 '20 at 01:11
  • Using a pointer would require an extra allocation, and an extra pointer dereference on each access. – interjay Dec 19 '20 at 01:11
  • @interjay But you are technically going out of bounds of your array intentionally here – bobobobo Dec 19 '20 at 01:12
  • Yup. Something C allows you to do. Just make sure you stick to POD types. – user4581301 Dec 19 '20 at 01:19
  • 4
    @bobobobo `int array[1]` and accessing the array out-of-bounds was a hack, used before flexible array members were officially added to the language. Nowadays, it's just `int array[]`, and there are rules for using the feature. – user3386109 Dec 19 '20 at 01:23
  • @bobobobo [Funny you should ask](https://stackoverflow.com/questions/246977/is-using-flexible-array-members-in-c-bad-practice). – WhozCraig Dec 19 '20 at 01:38
  • @user3386109: The C standard “allows” it because it does not prohibit programs from doing things for which it does not define the behavior, but it is the compiler that supports it, by defining the behavior as an extension to the C standard. – Eric Postpischil Dec 19 '20 at 03:09
  • 3
    The term for this is ["struct hack"](https://stackoverflow.com/questions/3711233/is-the-struct-hack-technically-undefined-behavior). And as others have noted, it's obsolete - use a flexible array member. – Andrew Henle Dec 19 '20 at 03:10
  • 2
    And apparently it's worse than obsolete - see the comments following [this answer](https://stackoverflow.com/a/247040/4756299). As it's strictly-speaking undefined behavior to access an array outside of its defined bounds, GCC has apparently taken to producing binaries that won't behave as expected. – Andrew Henle Dec 19 '20 at 03:16

1 Answers1

1

IMHO its some kind of lets say "hacky". At least when seeing this for the 1st time its not so obvious. But its not that hard as soon you once got it.

I will give an example and try to visualize it. Hopefully this way its simpler to spot how it works beneath.

struct Numbers {
    int size;
    int nums[1];  // <- variable length array
};

int main() {
    int size = 4;
    struct Numbers *fourNums = NULL;

    /* Allocate enough space for our struct PLUS extra space which will
     * represent our array later. */
    fourNums = malloc( sizeof(*fourNums) + (size-1)*sizeof(fourNums->nums[0]) );
    fourNums->size = size;

    /* Use it as it would be an array. */
    for( int i=0 ; i<fourNums->size ; ++i ){
        fourNums->nums[i] = 101 + i;
    }
}

After our allocation this will look like:

   .-- sizeof(int) for our "int size" member
   |
   |      .-- sizeof(int[1]) for our "int num[1]" member
   |      |
   |      |          .--  sizeof(int[3]) which we allocate additionally.
   |      |          |    HINT: One less than "size", because
   |      |          |    num[0] is already present in struct itself.
   v      v          v
+------+------+--------------------+
|      |      |                    |
+------+------+--------------------+

And then when we fill it this will end up like:

   .-- our value for "size"
   |
   |      .-- num[0] will end up here.
   |      |
   |      |          .-- num[1] through num[3] get
   |      |          |   stored here.
   v      v          v
+------+------+--------------------+
|    4 |  101 |  102 ,  103 ,  104 |
+------+------+--------------------+

Effectively the 1st array entry gets placed at the space which is the strange looking num[1] in our struct. But all other indexes get placed beyond that structure. This works, because we did allocate more memory than the structure actually is in size and then we simply use that space behind to store our array there ;)


Just to give some more insight. When seeing arrays, we often see them declared as pointers. So I'll also visualize this scenario to point out some differences:

struct Numbers {
    int size;
    int *nums;  // <- This time we're using a pointer.
};

int main() {
    int size = 4;
    struct Numbers *fourNums = NULL;

    /* This time we need TWO allocations. Or we may choose to place the object
     * on the stack instead. But in this example I'll use anoter alloc. */
    fourNums = malloc(sizeof *fourNums); // <- No additional space needed.
    fourNums->size = size;
    fourNums->nums = malloc( size * sizeof(*fourNums->nums) ); // <- But need another allocation for the array.

    /* Accessing feels the same. */
    for( int i=0 ; i<fourNums->size ; ++i ){
        fourNums->nums[i] = 101 + i;
    }
}

This time the allocation looks different:

   .-- sizeof(int) for our "int size" member
   |
   |      .-- sizeof(int*) for our pointer-to-array
   |      |
   |      |             .--  sizeof(int[4]) our array but
   |      |             |    somewhere else in memory.
   v      v             v
+------+------+     +---------------------------+
|      |      |     |                           |
+------+------+     +---------------------------+

Then also when filled looks different:

   .-- our "size". This is the same as before.
   |
   |      .-- This time, we need an additional pointer
   |      |   to our array (there is no need for this
   |      |   in our 1st approach).
   |      |
   |      |             .-- Our array. This time we need to
   |      |             |   store ALL elemens here.
   v      v             v
+------+------+     +------+------+------+------+
|    4 |0xABBA|---->|  101 |  102 |  103 |  104 |
+------+------+     +------+------+------+------+

Edit: Using sizeof *ptr instead sizeof type as suggested in comments. Thx.

hiddenAlpha
  • 390
  • 4
  • 10
  • Aside: `fourNums = malloc( sizeof(*fourNums) + (size-1)*sizeof(int) );` makes more sense as `fourNums = malloc( sizeof(*fourNums) + (size-1)*sizeof foruNums->nums[0]);`. Good and consistent to use `sizeof *ptr` than `sizeof(type)` in both places. – chux - Reinstate Monica Dec 19 '20 at 06:36