混合 C/C++/Objective-C 开发中的互操作性问题,第二部分






4.62/5 (14投票s)
一篇关于混合 C/C++/Objective-C 开发中互操作问题的文章
引言
本文概述了我们在 C、Objective-C、C++ 和 Objective-C++ 代码互操作时可能遇到的几个复杂问题。所有代码示例都在 XCode 4.6.2(Apple LLVM 4.2 (Clang) 和 LLVM GCC 4.2)以及最新的 XCode 5.0 的编译器上进行了测试。由于 XCode 5.0 不再包含 GCC 编译器,因此示例仅在最新的 Apple LLVM 5.0 (Clang) 上进行了检查。如果最新的 Clang 结果不同,我会明确说明。
背景
在许多情况下,例如,我们需要从 Objective-C 代码中使用 C++ 代码,或者从 Objective-C 调用 C 函数,本文将对此进行介绍。您还应该了解 Objective-C++ 是什么。它是一种在同一个文件中混合 Objective-C 和 C++ 代码并使其能够相互操作的方法。您当然可以在 XCode 中使用它——将您的文件名命名为 .mm 扩展名。
问题 1. 在 Objective-C++/C++ 代码中包含模板 MAX/MIN
描述
假设您在 CppTemplate.h 文件中有以下代码
template<class T>
inline const T &MAX(const T &a, const T &b)
{ return b > a ? (b) : (a); }
文件 ObjCppClass.h
#import <Foundation/Foundation.h>
@interface ObjCppClass : NSObject
@end
文件 ObjCppClass.mm
#import "ObjCppClass.h"
#include "CppTemplate.h"
@implementation ObjCppClass
@end
尝试构建时,您会遇到类似 Clang 的编译错误
«CppTemplate.h:17:17: Expected unqualified-id CppTemplate.h:17:17: Expected ')'»
如果将 CppTemplate.h 包含在 .cpp 文件中,则不会出现错误。
解决方案
这个问题 arises from the fact that there are macros called MAX/MIN in Objective-C, which look like this (from a file NSObjCRuntime.h)
#define MAX(x,y) ((x) > (y) ? (x) : (y))
所以模板实例化会变成如下形式
template<class T>
inline const T& ((const T& a) > (const T& b) ? (const T&a) : (constT&b)) (const T& a, const T& b) { ... }
这完全没有意义且无法编译。
要修复此错误,请将模板重命名,或使用
std::max
。问题 2. ARC 禁止在结构体中使用 Objective-C 类型的对象
描述
下面是一个包含两个 Objective-C 类 NSString 对象的结构体
typedef struct SStrings
{
NSString* firstName;
NSString* secondName;
} SStrings;
在 ARC 项目(自动引用计数)中,您将收到编译错误 «main.m: 15:15: ARC forbids Objective-C objects in structs or unions»,即使您为该特定文件在项目设置中设置了 -fno-objc-arc 标志。
解决方案
首先,我应该简要解释一下 ARC(自动引用计数)是什么。它是一种机制,使开发者无需手动管理内存。也就是说,编译器会在代码中插入 retain/release/autorelease
的调用,而不是您自己。
ARC 机制介于垃圾回收器和手动内存管理之间。与垃圾回收器一样,ARC 使开发者无需编写 retain/release/autorelease
的调用。但是,与垃圾回收器不同的是,ARC 不识别强循环引用(retain
)。互相拥有强引用的两个对象即使没有其他引用指向它们,ARC 也不会将其处置。开发者仍然需要避免或销毁对象到对象的强循环引用。ARC 仅由 Clang 支持。
现在回到主题。要避免此错误,您需要在结构体中声明 NSString
对象之前加上 __unsafe_unretained
属性:
typedef struct SStrings{
__unsafe_unretained NSString* firstName;
__unsafe_unretained NSString* secondName;
} SStrings;
接下来,我应该解释一下 __unsafe_unretained
是什么。
默认情况下,ARC 中的所有对象都是 __strong
类型的。这意味着当您将一个对象赋值给一个变量时,它的引用计数会增加,并且只要有对它的引用,该对象就会被保留。这为循环引用提供了可能。例如,当一个对象将另一个对象作为类变量时,而第二个对象又以强引用的方式指向第一个对象(例如作为代理),那么这两个对象将永远不会被释放。
__unsafe_unretained
和 __weak
限定符正是为此目的而设计的。它们最常用于代理。这意味着代理实例仍然指向第一个对象,但对于这个对象,引用计数不会增加,从而打破了循环引用并允许两个对象被释放。
这两种修饰符都可以阻止对象的保留,但方式略有不同。对于 __weak
,在对象被移除后,变量将被赋为 nil
,这是一种非常安全的方式。正如其名称所示,带有 __unsafe_unretained
限定符的变量在对象移除后仍会指向该对象所在的内存。这可能导致在访问已释放的对象时崩溃。
那么,为什么您可能想使用 __unsafe_unretained
呢?不幸的是,__weak
仅支持 iOS 5.0 和 Lion 作为平台。如果您想在 iOS 4.0 和 Snow Leopard 上运行应用程序,则必须使用 __unsafe_unretained
限定符。
现在假设您可以在使用 ARC 的情况下编写这样的代码
typedef struct {
__strong NSObject *obj;
int ivar;
} SampleStruct;
那么您可以编写如下代码
SampleStruct *thing = malloc(sizeof(SampleStruct));
问题在于 malloc
不会将返回的内存清零。因此 thing->obj
是一个随机值,不一定是 NULL
。然后您像这样赋值:
thing->obj = [[NSObject alloc] init];
实际上,ARC 会将此代码转换为类似如下的内容
NSObject *temporary = [[NSObject alloc] init];
[thing->obj release];
thing->obj = temporary;
这里的问题是,您的程序刚刚向一个随机值发送了 release
。应用程序很可能会在此处崩溃。
您可能会说 ARC 应该识别 malloc
的调用并负责将 obj
设置为 nil
以防止这种情况。关键在于 malloc 可以被封装到其他函数中:
void *customAllocate(size_t size) {
void *p = malloc(size);
if (!p) {
// malloc failed. Try to free up some memory.
clearCaches();
p = malloc(size);
}
return p;
}
好的,现在 ARC 也应该知道您的函数 customAllocate
,而且它可能是一个您以二进制形式收到的静态库。
您的应用程序也可以使用自定义内存分配器,这些分配器会重用旧的分配而无需使用 free
和 malloc
。因此,即使更改 malloc 使其清零分配的内存,也不会起作用。ARC 应该了解您程序中的所有特殊分配器。
要可靠地实现这一点将非常困难。因此,ARC 的创建者们选择放弃,并禁止在结构体中使用 __strong
。
这就是为什么您只能在结构体中放置带有 __unsafe_unretained
限定符的对象,以告诉 ARC “不要尝试控制此变量引用的对象的拥有权”。
仅对 Clang 有效,因为 GCC 完全不支持 ARC。
问题 3. 带有块的代码在 Objective-C 中编译成功,但在 Objective-C++ 中无法编译
描述
首先,关于“块”的几点说明。“块”这个词有点歧义,所以我只是说我不是指 C 中存在的、用大括号组合成一个单元的运算符组。我指的是 Apple 提出的对 Objective-C 语言的补充,它允许使用匿名函数(或 lambda)。
因此,一个典型的简单块看起来是这样的
void (^block)() = ^{ printf("Hello world\n"); }
它只是打印字符串。
大括号前的井号 (^) 符号区分了我们的语句和经典的运算符块。定义了块之后,您可以如下调用它
block ();
您会在控制台上看到打印的行“Hello world”。
现在让我们回到我们的主题。假设您在 Objective-C 中有以下代码
@interface TestClass1 : NSObject
- (void)test;
@end
@implementation TestClass1
- (void)test
{
void (^d_block)(void) =
^{
int n;
};
}
@end
C++ 中的情况大致相同
class TestClass2
{
public:
void TestIt();
};
void TestClass2::TestIt()
{
void (^d_block)(void) =
^{
int n;
};
}
Objective-C++ 中的情况也是如此
class TestClass3
{
public:
void TestIt();
};
void TestClass3::TestIt()
{
void (^d_block)(void) =
^{
int n;
};
}
Clang 编译了所有 3 个代码片段,但分别生成了“Unused variable 'n'”警告 3 次。在 C++ 版本中,Clang 应该生成一个错误消息,因为块是 Objective-C 的特性,但它并没有这样做。这看起来像是一个 bug,但更像是一个特性。由于块被认为是有用的工具,因此它们被设计成可以在 C++ 代码中识别。
GCC 在 Objective-C++ 和 C++ 版本中都发出了几乎相同的抱怨
«TestClass2.cpp:15: 'int TestClass2::n' is not a static member of 'class TestClass2'» «TestClass3.mm:15: 'int TestClass3::n' is not a static member of 'class TestClass3'».
在 C++ 代码中,GCC 应该说它也不知道块。这似乎也是一个特性。
解决方案
这些是 GCC 中的 bug。关于这个主题有一个 bug:http://lists.apple.com/archives/xcode-users/2011/Mar/msg00232.html。您可以通过使变量为静态变量或切换到 Clang 来解决此问题:
class TestClass3
{
static int n;
public:
void TestIt();
};
void TestClass3::TestIt()
{
void (^d_block)(void) =
^{
};
}
问题 4. 无法将块分配给 C++11 lambda
描述
Clang 支持 C++11。这会带来一些有趣的问题。
您可以将 lambda 分配给块。
void (^block)() = []() -> void {
NSLog(@"Inside Lambda called as block 1!");
};
block();
您可以将块分配给 std::function
。
std::function<void(void)> func = ^{
NSLog(@"Block inside std::function");
};
func();
但是,您无法将块分配给 lambda。
auto lambda = []() -> void {
NSLog(@"Lambda!");
};
lambda = ^{ // error!
NSLog(@"Block!");
};
lambda();
然后您将收到编译错误 «main.mm: 40:12: No viable overloaded '='»。
解决方案
此问题没有解决方案。您不能将一个 lambda 分配给另一个 lambda(即使结构相同)。
auto lambda1 = []() { return 1; };
auto lambda2 = []() { return 1; };
lambda1 = lambda2; //Error
编译时错误 «main.mm: 67:13: No viable overloaded '='» 发生。
Lambda 甚至不能分配给自己。
auto lambda = []() -> void { printf("Lambda 1!\n"); };
lambda = lambda;
代码也无法编译,出现错误 «main.mm: 63:12: Overload resolution selected implicitly-deleted copy assignment operator»。
每个 lambda 都有其自己实现定义的类型。
对于 XCode 5 中的 Clang 来说,情况都是一样的,但错误消息略有不同。
Visual Studio 2010 编译了将 lambda 分配给自己的操作,但 IntelliSense 产生了一个奇怪的错误消息
«IntelliSense: function" lambda [] void () ->void :: operator = (const lambda [] void () -> void &) "(declared at line 9) cannot be referenced - it is a deleted function».
原因是 Visual Studio 2010 不支持已删除的方法。
关于已删除方法的简要离题。它们管理默认行为。
现在,标准的“不可复制”惯用法可以显式表达如下
class X {
// ...
// Noncopyable
X& operator=(const X&) = delete;
X(const X&) = delete;
};
反之,您可以显式声明您希望使用复制的默认行为
class Y {
// ...
// Default copy semantics
Y& operator=(const Y&) = default;
Y(const Y&) = default;
};
结论
很明显,不同语言的代码互操作并非易事,但遗憾的是有时却是必需的。我希望我的文章能有趣、有用,并帮助许多人避免犯类似的错误。