Objective-C++ and C Interoperability

This section documents modern Objective-C as implemented by Apple Clang in the current Xcode release — Objective-C 2.0 plus ARC, literals and subscripting, instancetype, Clang modules, lightweight generics, nullability and @available — as published at Apple Developer Documentation and the Clang Objective-C specifications, which are the references these pages are written and verified against.

This content was generated with the assistance of AI and should be verified against developer.apple.com before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

Objective-C is a strict superset of C, so calling C is not "interoperability" at all — it is just calling a function. **Objective-C** goes further: a `.mm` file is compiled as Objective-C *and* C together, so both object models coexist in one translation unit. This is how a cross-platform C++ engine gets a native Cocoa front end.

This page assumes the C and C++ languages themselves and covers only what changes when they meet Objective-C. For the languages in their own right, see the C Reference and the C++ Reference.

Objective-C++

.mm Files

Rename a file from .m to .mm and the compiler accepts C++ syntax as well:

// ImageProcessor.mm
#import "ImageProcessor.h"
#import <vector>
#import <string>
#import <memory>

@implementation ImageProcessor

- (NSArray<NSNumber *> *)histogramForData:(NSData *)data {
    std::vector<int> counts(256, 0);                  // C++ container

    const uint8_t *bytes = (const uint8_t *)data.bytes;
    for (NSUInteger i = 0; i < data.length; i++) {
        counts[bytes[i]]++;
    }

    NSMutableArray<NSNumber *> *result = [NSMutableArray arrayWithCapacity:counts.size()];
    for (int c : counts) {                            // C++11 range-for
        [result addObject:@(c)];                      // Objective-C literal
    }
    return [result copy];
}

@end

Note the mixing: an NSData and a std::vector in one method, Objective-C literals alongside a C++ range-based for.

The compiler flag is -x objective-c++, but the .mm extension selects it automatically. In Xcode, the file type does the same.

C++ Objects as Instance Variables

A C++ object can be an ivar, but only if the ivar is declared in the .mm — never in the .h, which must stay importable from ordinary Objective-C:

// ImageProcessor.h -- pure Objective-C, importable from any .m
#import <Foundation/Foundation.h>

@interface ImageProcessor : NSObject
- (NSArray<NSNumber *> *)histogramForData:(NSData *)data;
@end
// ImageProcessor.mm -- C++ lives here, invisible to clients
#import "ImageProcessor.h"
#import <memory>
#import "Engine.hpp"

@implementation ImageProcessor {
    std::unique_ptr<Engine> _engine;        // C++ member, private to this file
    std::vector<float>      _weights;
}

- (instancetype)init {
    self = [super init];
    if (self) {
        _engine = std::make_unique<Engine>();   // constructed here
        _weights.reserve(1024);
    }
    return self;
}

- (void)dealloc {
    // _engine's destructor runs automatically when the object is destroyed.
    // Nothing to write unless the C++ type needs explicit teardown.
}

@end

Three rules govern this:

  • C++ constructors and destructors do run. The compiler calls each ivar’s default constructor after alloc zeroes memory, and its destructor during deallocation — so RAII works.

  • A C++ ivar with no default constructor will not compile. Wrap it in a std::unique_ptr or std::optional.

  • Keep C++ out of the header. This is the "PIMPL at the language boundary" pattern, and it is what lets ordinary .m files use the class without being switched to .mm.

If a header genuinely must expose C++, guard it so a .m file gets a usable declaration:

#ifdef __cplusplus
    #include <string>
    class Engine;
#endif

@interface ImageProcessor : NSObject
#ifdef __cplusplus
- (void)configureWithEngine:(Engine *)engine;
#endif
@end

C++11 Lambdas versus Blocks

Both are closures; they belong to different object models:

Block C++ lambda

Syntax

^(int x){ … }

[](int x){ … }

Capture

Implicit; objects retained under ARC

Explicit capture list ([=], [&], [x])

Type

An Objective-C object

An unnamed class type

Storage

void (^)(int), or a copy property

auto, or std::function<void(int)>

Lifetime

Reference-counted

Ordinary C++ value semantics

Passed to

Cocoa APIs, GCD

STL algorithms, C++ APIs

// A lambda into an STL algorithm:
std::sort(values.begin(), values.end(), [](int a, int b) { return a > b; });

// A block into a Cocoa API:
[array enumerateObjectsUsingBlock:^(id obj, NSUInteger i, BOOL *stop) { … }];

// They compose: a block may capture C++ values, and a lambda may call Objective-C.
std::string prefix = "row ";
dispatch_async(dispatch_get_main_queue(), ^{
    NSLog(@"%s%d", prefix.c_str(), n);      // the block captured the std::string by value
});

A std::function can hold a block and a block can call a lambda, but be careful with capture-by-reference ([&]) in anything that outlives the current scope — unlike a block’s ARC-managed captures, a dangling reference is entirely your problem.

ARC and C++ Containers

ARC manages Objective-C pointers; C++ containers manage their elements. Storing one in the other works, but you must be explicit:

// An Objective-C object in a C++ container: ARC-aware in Objective-C++.
std::vector<id> objects;
objects.push_back(someObject);         // retained; released when the vector is destroyed

// A C++ object in an Objective-C collection: box it.
auto engine = std::make_shared<Engine>();
NSValue *boxed = [NSValue valueWithPointer:engine.get()];   // the NSValue does NOT own it

// A struct field may be a managed object pointer in both .m and .mm files;
// use __unsafe_unretained only when you intend to manage the lifetime, e.g.
// because the struct is malloc'd and no copy/destroy helper ever runs:
struct Holder {
    __unsafe_unretained id object;     // you manage the lifetime
};

The strong, weak and __unsafe_unretained qualifiers work on C member variables too. Where the ownership is genuinely shared across both worlds, the cleanest arrangement is usually to keep the Objective-C object in an Objective-C property and hand C a plain observer pointer with a documented lifetime.

Calling Plain C

No ceremony at all — C is already the language:

#import <math.h>
#import <stdlib.h>
#import "mylib.h"                       // a plain C library

- (double)computeFrom:(double)value {
    double root = sqrt(value);                        // libm
    char *buffer = (char *)malloc(256);               // manual: ARC does not manage this
    mylib_format(buffer, 256, root);
    NSString *text = [NSString stringWithUTF8String:buffer];
    free(buffer);                                     // your responsibility
    return root;
}

Two boundaries need attention, both discussed in Strings, Numbers and Values: strings (UTF8String returns a borrowed, autoreleased buffer — copy it if it must outlive the statement) and allocation (malloc/free is entirely yours).

extern "C" and Header Hygiene

C++ mangles function names; C does not. A header that may be included from either must say so, or the linker fails with "undefined symbol" on a function that plainly exists:

// mylib.h -- usable from C, Objective-C, C++ and Objective-C++
#ifndef MYLIB_H
#define MYLIB_H

#ifdef __cplusplus
extern "C" {
#endif

void   mylib_init(void);
double mylib_compute(double value);
void   mylib_format(char *buffer, size_t size, double value);

#ifdef __cplusplus
}
#endif

#endif /* MYLIB_H */

This idiom is worth applying to every C header you publish. The corresponding rules for your Objective-C headers:

  • Keep public headers free of C++ — otherwise every client file must become .mm.

  • Forward-declare with @class instead of importing, where possible.

  • Do not import framework umbrella headers from a header that plain C code might include.

  • Prefer Clang modules (@import) for framework imports; see Modules, Frameworks and Code Organization.

Core Foundation and Toll-Free Bridging

Core Foundation is a C API with its own reference-counting conventions (CFRetain, CFRelease) that ARC does not manage. Several CF types are the same objects as their Foundation counterparts at run time, so a cast is all that is needed — but under ARC the cast must state what happens to ownership:

NSString *s = @"Hello";

CFStringRef borrowed = (__bridge CFStringRef)s;                 // no transfer; do not CFRelease
CFStringRef owned    = (__bridge_retained CFStringRef)s;        // you own it; CFRelease it
CFRelease(owned);

CFStringRef created  = CFStringCreateWithCString(NULL, "hi", kCFStringEncodingUTF8);
NSString *managed    = (__bridge_transfer NSString *)created;   // ARC owns it now

The decision rule is Core Foundation’s own Create Rule: a function with Create or Copy in its name returns something you own, so hand it to ARC with bridge_transfer (or CFBridgingRelease). Anything else is borrowed, and bridge is correct. Full detail is in Automatic Reference Counting.

Bridged pairs include NSString/CFStringRef, NSArray/CFArrayRef, NSDictionary/CFDictionaryRef, NSSet/CFSetRef, NSData/CFDataRef, NSNumber/CFNumberRef, NSDate/CFDateRef, NSURL/CFURLRef and NSError/CFErrorRef. NSRunLoop/CFRunLoopRef and NSBundle/CFBundleRef are not bridged despite the matching names — check the documentation rather than assuming.

Wrapping an Opaque C Type

The most valuable interop pattern: hide a C or C++ resource inside an Objective-C class so the rest of the codebase never sees it, and so its lifetime is managed by ARC.

// Database.h -- no sqlite3 anywhere in sight
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface Database : NSObject

- (nullable instancetype)initWithPath:(NSString *)path error:(NSError **)error;
- (nullable NSArray<NSDictionary<NSString *, id> *> *)query:(NSString *)sql error:(NSError **)error;

- (instancetype)init NS_UNAVAILABLE;

@end

NS_ASSUME_NONNULL_END
// Database.m
#import "Database.h"
#import <sqlite3.h>

@implementation Database {
    sqlite3 *_db;                       // the opaque C handle, private
}

- (nullable instancetype)initWithPath:(NSString *)path error:(NSError **)error {
    self = [super init];
    if (self) {
        int rc = sqlite3_open(path.UTF8String, &_db);
        if (rc != SQLITE_OK) {
            if (error) {
                *error = [NSError errorWithDomain:MyDatabaseErrorDomain
                                             code:rc
                                         userInfo:@{ NSLocalizedDescriptionKey:
                                             @(sqlite3_errmsg(_db)) }];
            }
            sqlite3_close(_db);         // clean up before failing
            _db = NULL;
            return nil;
        }
    }
    return self;
}

- (void)dealloc {
    if (_db) {
        sqlite3_close(_db);             // the C resource is freed with the object
        _db = NULL;
    }
}

@end

What this buys: the handle’s lifetime is tied to an ARC-managed object, C errors become NSError`s, the C header is imported in exactly one file, and callers write ordinary Objective-C. Apply the same shape to a C++ engine (holding a `std::unique_ptr in a .mm), a file descriptor, or any malloc-ed resource.

Where the resource must be released on a particular thread, or where cleanup can fail, add an explicit close/invalidate method as well — dealloc runs at an unpredictable moment and must not block.

See Also