Surprising Failures Putting a C Array Inside an Objective-C Struct Property

arrays, ivar, objective-c, properties

Solution

`struct` objects are always copied by value, not by reference. This means that when your `struct` is returned via an accessor method, that returned object is a copy of the one in your object instance. I suspect this comes from C where it makes no difference in the scenario of a standalone function that also shares that return type:

struct sample
{
    int arr[4];
};

struct sample FunctionThatReturnsSample(void)
{
    static struct sample s = { { 0, 1, 2, 3 } };
    return s;
}

int main(void)
{
    FunctionThatReturnsSample().arr[3] = 4;

    printf("%d\n", FunctionThatReturnsSample().arr[3]);
    // still prints "3"
}

Problem

I know Objective-C doesn't allow you to use C arrays as property types. I get the compiler error I expect in that case. But I'm surprised at the behavior I'm seeing with regards to C arrays inside struct properties: - No compilation errors or warnings. - Unexpected address of the address itself (gdb's `info malloc` doesn't know about it, not sure if it's uninitialized memory or what. But I would expect a crash or apparently-working albeit with memory corruption). - Assignment becomes a no-op. I boiled it down this example code: ``` #import <Foundation/Foundation.h> #ifndef sizeofA #define sizeofA(array) (sizeof(array)/sizeof(array[0])) #endif @interface IncludeCArrayDirectly : NSObject // Doesn't even compile // Uncomment below to see the compilation error for yourself. //@property(nonatomic, assign) int8_t f[9]; // ERROR: Property cannot have array or function type 'int8_t [9]' @end @interface IncludeCArrayInStruct : NSObject // Compiles (no warning) and runs but is amazingly broken. @property(nonatomic, assign) int normalProperty; @property(nonatomic, assign) struct { int f[9]; } p; - (void*)normalPropertysAddress; @end @interface IncludeCArrayInIvar : NSObject { // Totally works. @public int normalIvar; int8_t f[9]; } @end int main(int argc, const char *argv[]) { @autoreleasepool { {{ IncludeCArrayInStruct *a = [IncludeCArrayInStruct new]; // Notice a.p.f's address is off in 0x7fffxxxx-land: printf("&a = %p, &a.normalProperty = %p, a.p.f = %p\n", a, [a normalPropertysAddress], a.p.f); printf("a.p.f[4] BEFORE %d\n", a.p.f[4]); a.p.f[4] = 42; printf("a.p.f[4] AFTER %d\n", a.p.f[4]); assert(a.p.f[4] == 0); // Surprise! Assertion passes. Assignment above is a no-op. // Dump all of a.p.f just to take a better look: for (unsigned i = 0; i < sizeofA(a.p.f); i++) { printf("a.p.f[%d] == %d\n", i, a.p.f[i]); } }} {{ IncludeCArrayInIvar *b = [IncludeCArrayInIvar new]; // All these addresses are about what you'd expect: printf("&b = %p, &b.normalIvar = %p, b.f = %p\n", b, &b->normalIvar, b->f); printf("b->f[4] BEFORE %d\n", b->f[4]); b->f[4] = 42; printf("a->f[4] AFTER %d\n", b->f[4]); assert(b->f[4] == 42); // No surprise here, above assignment worked. // Dump all of b.f just to take a better look: for (unsigned i = 0; i < sizeofA(b->f); i++) { printf("b->f[%d] == %d\n", i, b->f[i]); } }} } return 0; } @implementation IncludeCArrayDirectly @end @implementation IncludeCArrayInStruct - (void*)normalPropertysAddress { return &_normalProperty; } @end @implementation IncludeCArrayInIvar @end ``` Any explanations to my puzzle points above?

Original source