3

In certain contexts (for example, when hooking into internals for testing) it would be convenient to be able to create copies of instances of classes which do not adopt NSCopying (do not implement -copyWithZone:). How can this be achieved? (Note: it would not be enough to implement the protocol in a category because not all of a class' instance variables are visible in its header.)

I've tried iterating over the object's Ivars, and (1) for object types recursing (or retaining), and, (2) for primitive types, obtaining the address of the ivar in the original instanced and the copy that's being made and memcpying the buffer starting at the source ivar's address to the destination ivar's address.

@interface NSObject (ADDLCopy)

- (id)addl_generateCopyDeep:(BOOL)deepCopy;

@end

@implementation NSObject (ADDLCopy)

//modified from http://stackoverflow.com/a/12265664/1052673
- (void *)addl_ivarPointerForName:(const char *)name
{
    void *res = NULL;

    Ivar ivar = class_getInstanceVariable(self.class, name);

    if (ivar) {
        res = (void *)self + ivar_getOffset(ivar);
    }

    return res;
}


- (id)addl_generateCopyDeep:(BOOL)deepCopy;
{
    id res = [[self.class alloc] init];

    unsigned int count = 0;
    Ivar *ivars = class_copyIvarList(self.class, &count);
    for (unsigned int i = 0; i < count; i++) {
        Ivar ivar = ivars[i];
        //We need to try here because of a bug with NSGetSizeAndAlignment
        //which prevents bitfields from being handled properly and results
        //in an exception being thrown.  See this link for more discussion:
        //http://lists.apple.com/archives/cocoa-dev/2008/Sep/msg00883.html
        @try {
            NSUInteger size = 0;
            const char *encoding = ivar_getTypeEncoding(ivar);
            NSGetSizeAndAlignment(encoding, &size, NULL);
            char firstEncodingCharacter[2];
            strncpy(firstEncodingCharacter, encoding, 1);
            firstEncodingCharacter[1] = 0;
            if (strcmp(firstEncodingCharacter, "@") == 0) {
                if (deepCopy) {
                    id original = object_getIvar(self, ivar);
                    id copy = [original addl_generateCopyDeep:deepCopy];
                    object_setIvar(res, ivar, copy);
                } else {
                    id original = object_getIvar(self, ivar);
                    object_setIvar(res, ivar, original);
                }
            } else {
                const char *name = ivar_getName(ivar);
                void *bytesSource = [self addl_ivarPointerForName:name];
                void *bytesTarget = [res addl_ivarPointerForName:name];
                memcpy(bytesTarget, bytesSource, size);
            }
        }
        @catch (NSException *exception) {}
        @finally {}
    }
    free(ivars);

    return res;
}

@end

This works somewhat, but there are a couple of glaring issues which I am aware of, not to mention those that I'm not aware of. First of all, it does not copy ivars inherited from superclasses. Secondly, it does not work for collections. In addition, because of a bug in NSGetSizeAndAlignment, it does not work for bitmasks. Additionally, it fails to consider associated objects.

How can an instance of an object (and all the objects that it owns) be recursively copied? If this is not entirely possible (dealing with an owned object which is retained and stored in a void pointer seems rather difficult if even possible), to what extent can it be done?

Nate Chandler
  • 4,533
  • 1
  • 23
  • 32

1 Answers1

8

In short, you can't.

"Deep" copying is an extremely domain specific problem.

For example, do you copy the delegate of an object or do you leave the delegate of the copy pointed at the same object (or do you set it to nil and require the copying client to set it)?

For that one example, there are about a zillion other one-off bits of per-context business logic related to copying (which is why the notion of "deep copying" is not implemented on the collection classes).

And all of the above assumes you actually have the implementation details. For classes implemented in the frameworks, there is no way to copy them safely because there simply isn't the data available to even know what bits of memory need to be copied, much less how.

You'll need to limit the scope and definition of the copy operation, then pursue a solution.

bbum
  • 162,346
  • 23
  • 271
  • 359