Accessing a std::map via a property getter returns garbage values

790 Views Asked by At

I have ran into this problem on a project and have boiled it down to the following sample code:

#import <map>

typedef std::map<int, int> exampleMap;
@interface ViewController ()

@property (nonatomic, assign) exampleMap map;
@end

@implementation ViewController

- (IBAction)iterateWithSelf:(id)sender
{
    for (exampleMap::iterator iter = self.map.begin(); iter != self.map.end(); ++iter)
    {
        NSLog(@"We should never hit this line as map is empty");
    }
}
- (IBAction)iterateWithIVar:(id)sender
{
    for (exampleMap::iterator iter = _map.begin(); iter != _map.end(); ++iter)
    {
        NSLog(@"We should never hit this line as map is empty");
    }
}

@end

- iterateWithIVar: executes fine and nothing is logged to the console. - iterateWithSelf: goes into the for loop, prints a line to the console and then crashes with EXC_BAD_ACCESS

I then tried inserting... self.map.insert(std::pair<int, int>(0, 1)); appears to affect nothing while _map.insert(std::pair<int, int>(0, 1)); adds an iteration to iterateWithIvar

Any ideas?

1

There are 1 best solutions below

1
On BEST ANSWER

The important thing to note is that self.map sends a message (i.e. calls a method). It does not access a structure member. It’s like the following C++ code:

class ViewController {
public:
  typedef std::map<int, int> ExampleMap;

private:
  ExampleMap _map;

public:
  ExampleMap map() { return _map; }
  setMap(const ExampleMap &o) { _map = o; }
};

followed by

for (auto iter = vc.map().begin(); iter != vc.map().end(); ++iter) {
  std::cerr << "We should never hit this line as map is empty" << std::endl;
}

That being the case, this is "expected behaviour". C++ objects are copied by value. Which means that every time you call self.map you get a new copy of your map. As it is invalid C++ to use iterators from one map to iterate over a second map, you get a crash.

In the second case, you directly access the instance variable, so you're dealing with the same object every time. There are several workarounds:

  1. Rewrite your class to provide accessors (e.g. -(int) numberOfItemsInMap, -mapItemForKey: etc.) which directly access the ivar.

  2. Change the accessors to no longer copy: Use an ivar for the map, manually write your own getter to return a pointer or reference to the map (If you use a pointer, you can declare the accessors as pointer properties anyway, but then use operator new to create it in your constructor).

  3. Rewrite -iterateWithSelf: to take an explicit copy of the map and then iterate over that (this is probably going to be inefficient, assuming that std::map is an efficient container for your use-case).

The first is probably the cleanest approach, as it also ensures your ObjC object is always aware of accesses/changes made to the underlying map, whereas directly exposing a pointer to the map allows anyone to modify it without your knowledge.