Objective-C - iVar Scoped Method Variables?
Asked Answered
F

2

3

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.

Fingerbreadth answered 17/8, 2012 at 1:59 Comment(8)
I'm puzzled -- why can't it be a regular ivar that you only use in the one method? And then use code inspection to assure it's only used where intended? If you feel it necessary you can name it by the method -- eg, "foo_someVal".Interpenetrate
@HotLicks because that clutters up the iVar declaration of the object. Plus, adding an iVar for a method that may or may not be called is a bit overkill, don't you think? This is also useful for categories, where you CAN'T add an iVar to the class.Fingerbreadth
There are lots of things that clutter up things. Objective-C is more clutter than code. And how is adding the ivar for the method somehow more "overkill" than adding the method?Interpenetrate
@RichardJ.RossIII - why should this more likely be done with associated objects? You are writing your own compiler! As part of compilation why not just promote the ivar to an instance variable and avoid any collisions with name mangling. If you do that you mesh with existing instance variable support, and also existing debuggers will at least find it even if they show the mangled name. With a little pre-processing (say change the file extension and have Xcode use a script to pre-process to standard Obj-C) you could use this approach without even writing your own (full) compiler.Phytography
@Phytography my goal here is not to write my own preprocessor, but to leverage the existing one. It's much more useful to have code that anyone can just drop into a program with a header file and a few macros, than to go and attempt to create your own compiler.Fingerbreadth
@RichardJ.RossIII - I misunderstood your "I am currently working on an Objective-C++ implementation myself", I thought you were writing a compiler. But even if you are not don't discount the script approach - it is easy to pre-process a file, say with a ruby script, and others can drop it into their own projects. How easy it will be to write the script is another matter... Have fun!Phytography
You've never needed 15 lines of code to add a variable to your class. In fact, you've never needed more than one. You did need as many as three lines to declare a property when properties were introduced, but that's down to a single line now too with Xcode 4.4.Cordiecordier
@Cordiecordier I'm no longer describing JUST an iVar, but a special kind of name mangled iVar, that supports scoping. This is impossible to do with the current state of objective-c ivars, as I have to make it subclass compatible as well.Fingerbreadth
F
3

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];
}
Fingerbreadth answered 17/8, 2012 at 15:55 Comment(3)
@CodaFi thanks, I try :P. As I've said before, the objective-c runtime is a passion of mine, and manipulating it in a useful way is just a bonus.Fingerbreadth
For some reason, this just makes me think of Greenspun's Tenth Rule.Equivalence
@W'rkncacnter except that this algorithm is far from slow, but is very fast indeed.Fingerbreadth
C
1

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.

In that case, the variable is part of the object's state, and it's therefore most appropriate to use an instance variable (or a property). This is exactly what ivars are for, whether they're used in a dozen methods or just one.

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.

My advice is to not do it at all. If your goal is to avoid clutter, don't go needlessly trying to add a new storage class to the language.

However, if you're determined to pursue this line, I'd look at using blocks instead of associated objects. Blocks get their own copies of variables that are scoped to the lifetime of the block. For example, you can do this:

- (void)func
{
    __block int i = 0;
    void (^foo)() = ^{
        i++;
        NSLog(@"i = %d", i);
    };

    foo();
    foo();
    foo();
}

and the output you get is:

i = 1
i = 2
i = 3

Perhaps you can find a clever way to wrap that up in a macro, but it looks to me like a lot of trouble just to avoid declaring an instance variable.

Cordiecordier answered 17/8, 2012 at 4:47 Comment(3)
While I do agree with you that one should not use such constructions at all... how is your block example not using an iVar? Your void (^fooBlock)(void) declares an instance variable of type void (^)(void), pretty much like here.Obola
@ThorstenKarrer You're right -- I was focusing on the persistent nature of block variables, but you're right that you still have to keep track of the block itself. Removed the latter example, which pretty much just begs the question. I still think blocks are the best hope for a solution, but it's a ridiculous problem in the first place.Cordiecordier
This defeats the purpose. Yes, it's an ingenious use of blocks, but it's not an instance variable, but a function variable. I have a solution now, which I will post after I add some documentation on it.Fingerbreadth

© 2022 - 2024 — McMap. All rights reserved.