c++iosobjective-cobjective-c++

How do I correctly store an (Objective-C) SKProduct* in a C++ std::map?


I have a std::map<std::string, SKProduct*> which I populate like this:

// Assume s_map is always accessed in a thread safe way    
static auto s_map = std::map<std::string, SKProduct*>{};

-(void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response {
  auto map = std::map<std::string, SKProduct*>{};
  const auto product_id = std::string(
    [product.productIdentifier UTF8String]
  );
  for(SKProduct* product in response.products) {
    if(product != nil) {
      map[product_id] = product;
      [map[product_id] retain]; 
    }
  }
  s_map = map;
}

Later (when a purchase is made) I find the SKProduct* like this:

auto make_purchase(const product_id_t& product_id) -> void {
  // Note that the whole map is copied
  const std::map<std::string, SKProduct*> map = s_map;
  const auto product_it = map.find(product_id);
  if(it == map.end()) {
    return;
  }
  // Here somewhere I get a crash objc_retain_x0
  SKProduct* product = product_it->second;
  [product retain];
  SKPayment* payment = [SKPayment paymentWithProduct: product];
  [payment retain]; 
  // Continue the purchase from here on...
}

Am I doing something wrong when storing/retrieving the SKProduct* from the std::map? I'm not familiar with the Objective-C model of reference counting.

(Note that the code is a bit simplified for clarity compared to the original code)


Solution

  • For the question in the title, I would encapsulate this part into a custom container class to wrap the memory management inside. Here is the minimalistic implementation example:

    template<typename Key> // Key type must not be a retain-release object
    class id_unordered_map {
        std::unordered_map<Key, id> m_underlying_map;
    
        void clear() {
            for (const auto& element: m_underlying_map) {
                [element.second release];
            }
            m_underlying_map.clear();
        }
    
    
    public:
        id_unordered_map() = default;
    
        id_unordered_map(const id_unordered_map& rhs) {
            for (const auto& element: rhs.m_underlying_map) {
                // makes a shallow copy
                m_underlying_map[element.first] = [element.second retain];
            }
        };
    
        id_unordered_map(id_unordered_map&& rhs) {
            for (const auto& element: rhs.m_underlying_map) {
                m_underlying_map[element.first] = [element.second retain];
            }
            rhs.clear();
        }
    
        id_unordered_map& operator=(const id_unordered_map& rhs) {
            clear();
            for (const auto& element: rhs.m_underlying_map) {
                // makes a shallow copy
                m_underlying_map[element.first] = [element.second retain];
            }
            return *this;
        }
    
        id_unordered_map& operator=(id_unordered_map&& rhs) {
            clear();
            for (const auto& element: rhs.m_underlying_map) {
                m_underlying_map[element.first] = [element.second retain];
            }
            rhs.clear();
            return *this;
        }
    
        void setObject(const Key& key, id object) {
            removeObject(key);
    
            if (object) {
                m_underlying_map[key] = [object retain];
            }
        }
    
        id getObject(const Key& key) {
            if (auto it = m_underlying_map.find(key); it != m_underlying_map.end()) {
                return it->second;
            } else {
                return nil;
            }
        }
    
        void removeObject(const Key& key) {
            if (auto it = m_underlying_map.find(key); it != m_underlying_map.end()) {
                [it->second release];
                m_underlying_map.erase(it);
            }
        }
    
        ~id_unordered_map() {
            clear();
        }
    };
    

    I suggest a shallow copy approach here, as it's consistent with how Cocoa own collections work. Making a deep copy is considered an exceptional case and requires as separate method (e.g. initWithDictionary:copyItems: constructor of NSDictionary)

    However personally I don't feel like there is an apparent mistake in the provided code which makes the app to crash. The error you are observing commonly happens when a message is sent to an object which is not set to nil but already released. And provided no release messages are sent to the objects in the map in between the functions, your SKProduct object has to survive.

    Here are a few things to consider though:


    Summing it up, your SKProductsRequestDelegate should looks something like this (the product here is artificial, so i make it on the fly in the response):

    NS_ASSUME_NONNULL_BEGIN
    
    @interface TDWObject ()<SKProductsRequestDelegate>
    
    @property (strong, readonly, nonatomic) dispatch_queue_t productsSyncQueue;
    @property (assign, nonatomic) id_unordered_map<std::string> products;
    @property (strong, readonly, nonatomic) NSMutableSet<SKProductsRequest *> *pendingRequests;
    
    @end
    
    NS_ASSUME_NONNULL_END
    
    @implementation TDWObject
    
    @synthesize products = _products;
    
    #pragma mark Lifecycle
    
    - (instancetype)init {
        if (self = [super init]) {
            _productsSyncQueue = dispatch_queue_create("the.dreams.wind.property_access.products",
                                                        DISPATCH_QUEUE_CONCURRENT);
            _pendingRequests = [[NSMutableSet set] retain];
        }
        return self;
    }
    
    - (void)dealloc {
        [_pendingRequests release];
        [_productsSyncQueue release];
        [super dealloc];
    }
    
    #pragma mark Properties
    
    - (id_unordered_map<std::string>)products {
        __block id_unordered_map<std::string> *data;
        dispatch_sync(_productsSyncQueue, ^{
            // Take by pointer here, to avoid redundant copy
            data = &_products;
        });
        return *data; // makes a copy for observers
    }
    
    - (void)setProducts:(id_unordered_map<std::string>)products {
        dispatch_barrier_async(_productsSyncQueue, ^{
            _products = std::move(products);
        });
    }
    
    #pragma mark Actions
    
    - (void)requestProducts {
        SKProductsRequest *productRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:[NSSet setWithArray:@[
            @"the.dreams.wind.sampleSKU1"
        ]]];
        productRequest.delegate = self;
        [productRequest start];
        [_pendingRequests addObject:productRequest];
    }
    
    - (void)makePurchase {
        SKProduct *product = [_products.getObject("the.dreams.wind.sampleSKU1") retain];
        // Just checking that the object exists
        NSLog(@"%@", product);
        [product release];
    }
    
    #pragma mark SKProductsRequestDelegate
    
    - (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response {
        [_pendingRequests removeObject:request];
    
        decltype(_products) newProducts;
        // Artificial Products
        for (NSString *identifier in response.invalidProductIdentifiers) {
            newProducts.setObject(identifier.UTF8String, [[SKProduct new] autorelease]);
        }
        self.products = newProducts;
    
    }
    

    You can see here that access/reading of the map is synchronised with use of GCD and Objective-C properties, which, I admit, is horribly ineffective when it comes to C++ objects accessed by value. You will want to optimise it BUT it should work without crashes I believe.

    P.S. you usually also want to synchronise reading from/writing to pendingRequests set, but it's not relevant in context of the question, thus I omitted this part.


    You may also consider just taking the products arrays by reference, without employing C++ objects, which should work just fine and is much more straightforward:

    - (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response {
        [_pendingRequests removeObject:request];
        NSArray<SKProduct *> *products = [response.products retain];
        ...
    }