Search code examples
objective-cmethodsscopeobjective-c++ivar

Objective-C - iVar Scoped Method Variables?


I was messing around in Objective-C earlier, and I ran into a quite common situation:

I had a class, which was not a singleton, that needed a variable shared between method calls, like static, but each instance needed it's own variable. However, this variable only needed to be used in one particular method, we'll call it -foo.

What I'd love to do, is have a macro, let's call it ivar, which lets me do the following:

@implementation MyClass 

-(foo)
{
    ivar int someVal = 10; // default value, ivar scoped variable.
}

-(bar)
{
    someVal = 5; // error, outside of `foo`'s scope.
}

@end

How the variable is defined does not matter to me (either a macro like OBJC_IVAR(Type, Name, Default) or ivar someType someName = value), as long as it meets the following requirements:

  • Has thread safety
  • Can have variable of same name (but different value) in another method
  • Type-less (doesn't matter what type the variable is)
  • Default Value support
  • Variable can be declared in one line (I shouldn't have to write 15 lines of code just to put a variable in my code)

I am currently working on an Objective-C++ implementation myself, I was just wondering if anyone else had any thoughts (or existing tools) on how to do this.

Obviously, this doesn't have to be done with a true iVar. More likely, this should be done with associated objects at run-time, which also manages deallocation for us.


Solution

  • After a lot of time spent, I believe I have a fully working solution in Objective-C++. Some of the features:

    • The variables are unique. As long as they have a different scope, their values are independent
    • Each instance has it's own values
    • Thread safety (accomplished by associated objects)
    • Simple variable declaration:

      • Macro overloading: only specify the information that you need
      • Possible ways to define an OBJC_IVAR:

        OBJC_IVAR(); // creates a warning, does nothing
        OBJC_IVAR(Name); // creates an ivar named 'Name' of type 'id'
        OBJC_IVAR(Type, Name); // creates an ivar named 'Name' of type 'Type'
        OBJC_IVAR(Type, Name, Default); // creates an ivar named 'Name', of type 'Type', and a default value of 'Default' (which is only executed once);
        
    • Full Type Support with C++ templates (__weak, __strong, __autoreleasing, volatile, etc. are all supported)

    • Subclasses do not share variables with their superclasses (so no chance for conflicts, variables really are limited to their scope).
    • Can be used in singletons without issue
    • Is fast, takes ~15-30 CPU cycles to look up a variable, and once it's looked up, takes as long as any other variable to set it.
    • Most of the hard work is done by the pre-processor, which allows for faster code
    • Just drag-and-drop into an existing Xcode project, doesn't rely on a custom processor

    Some minor cons to the implementation:

    • Objects must have an ownership specifier (limitation with C++ references: Reference to non-const type 'id' with no explicit ownership). Is easily fixed by adding __strong, __weak, or __autoreleasing to the type of the variable

    • Implementation is hard to read. Because it relies so much on C++ templates and Objective-C working together in harmony, it's difficult to just change 'one thing' and hope for it to work. I have added extensive comments to the implementation, so hopefully that frees some of the burden.

    • Method swizzling can confuse this majorly. Not the largest of issues, but if you start playing around with method swizzling, don't be surprised if you get unexpected results.

    • Cannot be used inside a C++ object. Unfortunately, C++ doesn't support runtime attributes, like objective-c does, so we cannot rely upon our variables being cleaned up eventually. For this reason, you cannot use OBJC_IVAR while inside a C++ object. I would be interested in seeing an implementation for that, though.

    • #line can mess this up drastically, so don't use it.

    Version History

    • 1.0: Initial Release
    • 1.1: Updated OBJC_IVAR_NAME to rely only on the preprocessor. As a result, we cannot use __func__.

    So, without further ado, here is the code:

    OBJC_IVAR.hpp

    //
    //  OBJC_IVAR.h
    //  TestProj
    //
    //  Created by Richard Ross on 8/17/12.
    //  Copyright (c) 2012 Ultimate Computer Services, Inc. All rights reserved.
    //
    #ifndef OBJC_IVAR_HPP
    #define OBJC_IVAR_HPP
    
    #import <Foundation/Foundation.h>
    #import <objc/runtime.h>
    
    #import "NSValue+CppObject.h"
    
    // Argument counting algorithm. Not too complex
    #define __NARG(_1, _2, _3, _4, _5, VAL, ...) VAL
    #define NARG(...) __NARG(__VA_ARGS__, 5, 4, 3, 2, 1, 0)
    
    // Different implementations based on number of parameters passed in
    #define __OBJC_IVAR(N, ...) _OBJC_IVAR_ ## N (__VA_ARGS__)
    #define _OBJC_IVAR(N, ...) __OBJC_IVAR(N, __VA_ARGS__)
    
    // Usage: OBJC_IVAR(Type (optional), Name (required), Default (optional))
    #define OBJC_IVAR(...) _OBJC_IVAR(NARG(__VA_ARGS__), __VA_ARGS__)
    
    // create a unique name. we use '__COUNTER__' here to support scoping on the same line, for compressed source code
    #define __OBJC_IVAR_STRINGIFY_NAME(file, line, name, counter) @file ":" #line " " #name ":" #counter
    #define _OBJC_IVAR_NAME(file, line, name, counter) __OBJC_IVAR_STRINGIFY_NAME(file, line, name, counter)
    #define OBJC_IVAR_NAME(name) _OBJC_IVAR_NAME(__FILE__, __LINE__, name, __COUNTER__)
    
    // old style creation. advantage: uses __func__ to determine calling function
    // #define OBJC_IVAR_NAME(Name) [NSString stringWithFormat:@"%s:%i %s:%s:%i", __FILE__, __LINE__, __func__, #Name, __COUNTER__]
    
    // implemenations for each of the overloads
    #define _OBJC_IVAR_0(...) _Pragma("message \"Cannot call OBJC_IVAR with 0 params!\"")
    #define _OBJC_IVAR_1(Name) _OBJC_IVAR_2(__strong id, Name)
    
    // first major implemenation. because we do no assignment here, we don't have to check for is_set
    #define _OBJC_IVAR_2(Type, Name) Type& Name = (_OBJC_IVAR::IMPL<Type>(self, OBJC_IVAR_NAME(Name)))
    
    // this is where things get fun. we have 'OBJC_IVAR_CUR_NAME', instead of calling OBJC_IVAR_NAME
    // multiple times, because we must ensure that COUNTER does not change during the course of the macro
    // this is the 'inner bowels' of C, and it's quite hacky. Returns a reference to an associated object
    // which is wrapped in a NSValue. Note that we only evaluate 'default' once throught the course of the
    // application's cycle, so you can feel free to put intensive loading code there.
    static NSString *_OBJC_IVAR_CUR_NAME;
    #define _OBJC_IVAR_3(Type, Name, Default) Type& Name = (_OBJC_IVAR::IS_SET(self, (_OBJC_IVAR_CUR_NAME = OBJC_IVAR_NAME(Name))) ? _OBJC_IVAR::IMPL<Type>(self, _OBJC_IVAR_CUR_NAME) : _OBJC_IVAR::IMPL<Type>(self, _OBJC_IVAR_CUR_NAME, Default))
    
    // namespace to wrap al lof our functions
    namespace _OBJC_IVAR
    {
        // internal dictionary of all associated object names, so that we don't run
        // into memory management issues.  we use a set here, because we should never
        // have duplicate associated object names.
        static NSMutableSet *_names = [NSMutableSet set];
    
        // wraps a value and a reference to a value. used over std::reference_wrapper,
        // as that doesn't actually copy in the value passed. That is required for what
        // we are doing, as we cannot be assigning to constants.
        template<typename T>
        class Wrapper {
        private:
            // private value wrapped by this object.
            T _value;
            // private reference wrapped by this object. should always point to _value.
            T& _ref;
    
        public:
            // default constructor. assumes 'T' has a valid 0-argument constructor
            Wrapper() : _value(), _ref(_value) { }
    
            // argument constructor. makes sure that value is initialized properly
            Wrapper(T val) : _value(val), _ref(_value) { }
    
            // returns the reference wrapped by this object
            operator T& () {
                return _ref;
            }
    
            T& get() {
                return _ref;
            }
        };
    
        // interns a name. because objc_getAssociatedObject works only by comparing
        // pointers (and +stringWithFormat: isn't guaranteed to return the same pointer),
        // we have to make sure that we maintain a list of all valid associated object
        // names. these are NOT linked to specific objects, which allows us to reuse some
        // memory
        inline NSString *name_intern(NSString *name)
        {
            // intern the value. first check if the object has been interned already,
            // and if it is, return that interned value
            if (id tmpName = [_names member:name])
            {
                name = tmpName;
            }
    
            // if we haven't interned this value before, then add it to the list and return it.
            else
            {
                [_names addObject:name];
            }
    
            return name;
        }
    
        // check and see if the requested iVar has been set yet. used for default value setting
        BOOL IS_SET(id target, NSString *name)
        {
            // first intern the name
            name = name_intern(name);
    
            // check if the object has this property. objc_getAssociatedObject will ALWAYS
            // return NULL if the object doesn't exist. Note the bridged cast. This is because
            // objc_getAssociatedObject doesn't care what you throw into the second parameter,
            // as long as it is a pointer. That gives us the flexibility at a later date, to,
            // for example, just pass a pointer to a single byte, and pull out the value that
            // way. However, we pass in a NSString pointer, because it makes it easy for us to
            // use and to re-use later.
            id val = objc_getAssociatedObject(target, (__bridge const void *) name);
    
            return val != nil;
        }
    
        // the actual implementation for setting the iVar. luckily this code isn't too hacky,
        // but it is a bit confusing.
        template<typename T>
        Wrapper<T>& IMPL(id target, NSString *name)
        {
            // first intern the name
            name = name_intern(name);
    
            // define a reference. we use pointers & new here, because C++ memory managment is
            // weird at best. Most of the time, you should be using RAII, but when dealing with
            // templates & objective-c interpolation, it is almost required that you use pointers
            // with new.
            Wrapper<T> *reference = nullptr;
    
            // check and see if the object already contains this property, if so, return that value
            NSValue *result = objc_getAssociatedObject(target, (__bridge const void *) name);
            if (result == nil)
            {
                // at this point, we need to create a new iVar, with the default constructor for the type.
                // for objective-c objects this is 'nil', for integers and floating point values this is 0,
                // for C++ structs and classes, this calls the default constructor. If one doesn't exist,
                // you WILL get a compile error.
                reference = new Wrapper<T>();
    
                // we now set up the object that will hold this wrapper. This is an extension on NSValue
                // which allows us to store a generic pointer (in this case a C++ object), and run desired
                // code on -dealloc (which will be called at the time the parent object is destroyed), in
                // this case, free the memory used by our wrapper.
                result = [NSValue valueWithCppObject:reference onDealloc:^(void *) {
                    delete reference;
                }];
    
                // finally, set the associated object to the target, and now we are good to go.
                // We use OBJC_ASSOCIATION_RETAIN, so that our NSValue is properly freed when done.
                objc_setAssociatedObject(target, (__bridge const void *) name, result, OBJC_ASSOCIATION_RETAIN);
            }
    
            // from result, we cast it's -cppObjectValue to a Wrapper, to pull out the value.
            reference = static_cast<Wrapper<T> *>([result cppObjectValue]);
    
            // finally, return the pointer as a reference, not a pointer
            return *reference;
        }
    
        // this is pretty much the same as the other IMPL, but it has specific code for default values.
        // I will ignore everything that is the same about the two functions, and only focus on the
        // differences, which are few, but mandatory.
        template<typename T>
        Wrapper<T>& IMPL(id target, NSString *name, const T& defVal)
        {
            name = name_intern(name);
    
            Wrapper<T> *reference = nullptr; // asign to be the default constructor for 'T'
    
            NSValue *result = objc_getAssociatedObject(target, (__bridge const void *) name);
            if (result == nil)
            {
                // this is the only difference. Instead of constructing with the default constructor,
                // simply pass in our new default value as a copy.
                reference = new Wrapper<T>(defVal);
                result = [NSValue valueWithCppObject:reference onDealloc:^(void *) {
                    delete reference;
                }];
    
                objc_setAssociatedObject(target, (__bridge const void *) name, result, OBJC_ASSOCIATION_RETAIN);
            }
    
            reference = static_cast<Wrapper<T> *>([result cppObjectValue]);
            return *reference;
        }
    }
    
    #endif // OBJC_IVAR_HPP
    

    NSValue+CppObject.h

    //
    //  NSValue+CppObject.h
    //  TestProj
    //
    //  Created by Richard Ross on 8/17/12.
    //  Copyright (c) 2012 Ultimate Computer Services, Inc. All rights reserved.
    //
    
    #import <Foundation/Foundation.h>
    
    // Extension on NSValue to add C++ object support. Because of the difficulty
    // involved in templates, I took the easy way out and simply passed in a block
    // of code to be run at dealloc.
    @interface NSValue (CppObject)
    
    // create a new NSValue instance that holds ptr, and calls 'deallocBlock' on destruction.
    +(id) valueWithCppObject:(void *) ptr onDealloc:(void (^)(void *)) deallocBlock;
    -(id) initWithCppObject:(void *)  ptr onDealloc:(void (^)(void *)) deallocBlock;
    
    // get the held pointer of this object. I called it -cppObjectValue, so
    // there was no confusion with -pointerValue.
    -(void *) cppObjectValue;
    
    @end
    

    NSValue+CppObject.m

    //
    //  NSValue+CppObject.m
    //  TestProj
    //
    //  Created by Richard Ross on 8/17/12.
    //  Copyright (c) 2012 Ultimate Computer Services, Inc. All rights reserved.
    //
    
    #import "NSValue+CppObject.h"
    
    // the concrete NSValue subclass for supporting C++ objects. Pretty straight-forward interface.
    @interface ConcreteCppObject : NSValue
    {
        // the underlying object that is being pointed to
        void *_object;
        // the block that is called on -dealloc
        void (^_deallocBlock)(void *);
    }
    
    @end
    
    @implementation ConcreteCppObject
    
    // object initialization
    +(id) valueWithCppObject:(void *)ptr onDealloc:(void (^)(void *))deallocBlock
    {
        return [[self alloc] initWithCppObject:ptr onDealloc:deallocBlock];
    }
    
    -(id) initWithCppObject:(void *)ptr onDealloc:(void (^)(void *))deallocBlock
    {
        if (self = [super init])
        {
            _object = ptr;
            _deallocBlock = deallocBlock;
        }
    
        return self;
    }
    
    // required methods for subclassing NSValue
    -(const char *) objCType
    {
        return @encode(void *);
    }
    
    -(void) getValue:(void *)value
    {
        *((void **) value) = _object;
    }
    
    // comparison
    -(BOOL) isEqual:(id)compare
    {
        if (![compare isKindOfClass:[self class]])
            return NO;
    
        return [compare cppObjectValue] == [self cppObjectValue];
    }
    
    // cleanup
    -(void) dealloc
    {
        // this should manage cleanup for us
        _deallocBlock(_object);
    }
    
    // value access
    -(void *) cppObjectValue
    {
        return _object;
    }
    
    
    @end
    
    // NSValue additions for creating the concrete instances
    @implementation NSValue (CppObject)
    
    // object initialization
    +(id) valueWithCppObject:(void *)ptr onDealloc:(void (^)(void *))deallocBlock
    {
        return [[ConcreteCppObject alloc] initWithCppObject:ptr onDealloc:deallocBlock];
    }
    
    -(id) initWithCppObject:(void *)ptr onDealloc:(void (^)(void *))deallocBlock
    {
        return [[self class] valueWithCppObject:ptr onDealloc:deallocBlock];
    }
    
    // unless the NSValue IS a ConcreteCppObject, then we shouldn't do anything here
    -(void *) cppObjectValue
    {
        [self doesNotRecognizeSelector:_cmd];
    
        return nil;
    }
    
    @end
    

    Example Usage:

    #import "OBJC_IVAR.hpp"
    
    @interface SomeObject : NSObject
    
    -(void) doSomething;
    
    @end
    
    @implementation SomeObject
    
    -(void) doSomething
    {
        OBJC_IVAR(__strong id, test, @"Hello World!");
        OBJC_IVAR(int, test2, 15);
    
        NSLog(@"%@", test);
        NSLog(@"%i", test2 += 7);
    
        // new scope
        {
            OBJC_IVAR(int, test, 100);
    
            NSLog(@"%i", ++test);
        }
    
        [self somethingElse];
    }
    
    -(void) somethingElse
    {
        OBJC_IVAR(int, newVar, 7);
    
        NSLog(@"%i", newVar++);
    }
    
    @end
    
    int main()
    {
        SomeObject *obj = [SomeObject new];
    
        [obj doSomething];
        [obj doSomething];
        [obj doSomething];
    }