之前有段時間,我參與了一項使用了C++庫的Objective-C項目。寫了一篇關於混編的文章,結果卻出乎意料的成為Google搜尋中關於Objective-C++的最靠前的結果之一。
後來,Apple將基於LLVM的clang做為主選編譯器。其作用之一就是可以保證Objective-C的演化,而GCC的進化卻太慢了。之前文章就不太適用了,而且在這個過程,我也收到了一些回饋,這些都促使我寫了這篇文章。
回顧一下
簡言之,如果你有一些C++代碼或庫,你想在Objective-C項目使用它,這就是我們要研究的問題。 通常,C++代碼中會定義你要使用的一些類(class), 你可以簡單的把.m副檔名改為.mm就可以改為Objective-C++編譯,然後就可以很容易地混合使用C++和Objective-C的代碼。這是一個簡單的做法,但兩個世界確實很不一樣,如此這樣的深度混合有時會變地很棘手。
你可能會想使用等價的Objective-C類型和函數將C++代碼封裝(wrap)起來。比方說,你有一個名為CppObject的C++類(CppObject.h):
#include <string>class CppObject{public: void ExampleMethod(const std::string& str); // constructor, destructor, other members, etc.};
在Objectiv-C類允許定義C++類的成員變數,所以可以首先嘗試定義一個ObjcObject封裝類(ObjcObject.h):
#import <Foundation/Foundation.h>#import "CppObject.h"@interface ObjcObject : NSObject { CppObject wrapped;}- (void)exampleMethodWithString:(NSString*)str;// other wrapped methods and properties@end
然後在ObjcObject.mm中實現這些方法。不過,此時會在兩個標頭檔(ObjcObject.h&CppObject.h)中得到一個預先處理和編譯錯誤。問題出在#include和#import上。對於前置處理器而言,它只做文本的替換操作。所以#include和#import本質上就是遞迴地複製和粘貼引用檔案的內容。這個例子中,使用#import "ObjcObject.h"等價於插入如下代碼:
// [首先是大量Foundation/Foundation.h中的代碼]// [無法包含<string>],因為它僅存在於C++模式的include path中class CppObject{public: void ExampleMethod(const std::string& str); // constructor, destructor, other members, etc.};@interface ObjcObject : NSObject { CppObject wrapped;}- (void)exampleMethodWithString:(NSString*)str;// other wrapped methods and properties@end
因為class CppObject根本不是有效Objective-C文法, 所以編譯器就被搞糊塗了。 錯誤通常是這樣的:Unknown type name 'class'; did you mean 'Class'? 正是因為Objective-C中沒有class這個關鍵字. 所以要與Objective-C相容,Objective-C++類的標頭檔必須僅包含Objective-C代碼,絕對沒有C++的代碼 - 這主要是影響類型定義(就像例中的CppObject類)。
保持簡潔的標頭檔之前的文章已經提到一些解決方案.其中最好的一個是PIMPL,它也適用於現在的情況。這裡還有一個適用於clang的新方法,可以將C++代碼從Objective-C中隔開,這就是class
extensions中ivars的。
Class extensions (不要同categories弄混) 已經存在一段時間了: 它們允許你在class的介面外的擴充部分定義在@implementation段前,而不是在公用標頭檔中。 這個例子就可以聲明在ObjcObject.mm中:
#import "ObjcObject.h"@interface ObjcObject () // note the empty parentheses- (void)methodWeDontWantInTheHeaderFile;@end@implementation ObjcObject// etc.
GCC也支援這個操作。不過clang還支援添加ivar塊,也就是你還可以聲明C++類型的執行個體變數,既可以在class extension中,也可以在@implementation開始的位置。本例中的ObjcObject.h可以被精簡為:
#import <Foundation/Foundation.h>@interface ObjcObject : NSObject- (void)exampleMethodWithString:(NSString*)str;// other wrapped methods and properties@end
去掉的部分都移到實現檔案的class extension中 (ObjcObject.mm):
#import "ObjcObject.h"#import "CppObject.h"@interface ObjcObject () { CppObject wrapped;}@end@implementation ObjcObject- (void)exampleMethodWithString:(NSString*)str{ // NOTE: str為nil會建立一個空字串,而不是引用一個指向UTF8Stringnull 指標. std::string cpp_str([str UTF8String], [str lengthOfBytesUsingEncoding:NSUTF8StringEncoding]); wrapped.ExampleMethod(cpp_str);}
如果我們不需要interface extension來聲明額外的屬性和方法,ivar塊仍然可以放在@implementation開始位置:
#import "ObjcObject.h"#import "CppObject.h"@implementation ObjcObject { CppObject wrapped;}- (void)exampleMethodWithString:(NSString*)str{ // NOTE: str為nil會建立一個空字串,而不是引用一個指向UTF8Stringnull 指標. std::string cpp_str([str UTF8String], [str lengthOfBytesUsingEncoding:NSUTF8StringEncoding]); wrapped.ExampleMethod(cpp_str);}
定義的CppObject執行個體wrapped在ObjcObject建立時,CppObject的預設建構函數會被調用,而在ObjcObject被調用dealloc析構時,ObjcObject的解構函式也會被調用。如果ObjcObject沒有提供預設的建構函數,編譯就會失敗。
管理被封裝C++對象的生命週期解決方案是透過new關鍵字掌握建構過程, 比如:
@interface ObjcObject () { CppObject* wrapped; // 指標!會在alloc時初始為NULL.}@end@implementation ObjcObject- (id)initWithSize:(int)size{ self = [super init]; if (self) { wrapped = new CppObject(size); if (!wrapped) self = nil; } return self;}//...
如果是使用C++異常, 也可以使用 try {...} catch {...}把建立過程封裝起來. 相應地,還要顯式地釋放封閉對象:
- (void)dealloc{ delete wrapped; [super dealloc]; // 如果使用了ARC,這句就要略去}
作者接著提到了另一個方法,顯示分配一塊記憶體,然後在它的基礎上調用new來建立對象。首先聲明char wrapped_mem[sizeof(CppObject)]; 再使用wrapped = new(wrapped_mem) CppObject();建立了執行個體wrapped。釋放時if (wrapped) wrapped->~CppObject(); 這樣雖然可行,但不建議使用。
總結 一定要確保封裝的方法僅返回和使用C或Objective-C類型的傳回值及參數。同時不要忘記C++中不存在nil, 而NUL是不可用於解引用的。
反向:在C++代碼中使用Objective-C類這個問題同樣存在於標頭檔中。你不能因為引入Objective-C類型而汙染了C++標頭檔,或無法被純C++代碼所引用。比方說,我們想封裝的Objective-C類ABCWidget ,在ABCWidget.h聲明為:
#import <Foundation/Foundation.h>@interface ABCWidget- (void)init;- (void)reticulate;// etc.@end
這樣的類定義在Objective-C++中是沒有問題的,但在純C++的代碼是不允許的:
#import "ABCWidget.h"namespace abc{ class Widget { ABCWidget* wrapped; public: Widget(); ~Widget(); void Reticulate(); };}
一個純粹的C++編譯器在Foundation.h中的代碼和ABCWidget聲明位置出錯。
永恒的PIMPL有沒有這樣的東西作為一類擴充C + +,這樣的把戲將無法正常工作。 另一方面,PIMPL,工作得很好,實際上是比較常用的純C + +了。 在我們的例子中,我們減少到最低限度:C + +類
C++並沒有之前提到的class extension,但是卻有另一種較為常用的方式:PIMPL (Private Implementation, 私人實現)。這裡,將C++ class的定義精簡為:
namespace abc{ struct WidgetImpl; class Widget { WidgetImpl* impl; public: Widget(); ~Widget(); void Reticulate(); };}
然後在Widget.mm中:
#include "Widget.hpp"#import "ABCWidget.h"namespace abc{ struct WidgetImpl { ABCWidget* wrapped; }; Widget::Widget() : impl(new WidgetImpl) { impl->wrapped = [[ABCWidget alloc] init]; } Widget::~Widget() { if (impl) [impl->wrapped release]; delete impl; } void Widget::Reticulate() { [impl->wrapped reticulate]; }}
它的工作原理是,前置聲明。聲明這樣的結構或類對象的指標成員變數、結構或類就足夠了。
需要注意的是封裝的對象會在解構函式中釋放。即便對於使用了ARC的項目,我還是建議你對這樣的對C++/Objective-C重引用的檔案檢測掉它。不要讓C++代碼依賴於ARC。在XCode中可以針對個別檔案檢測掉ARC。Target properties->Build phase頁簽,展開'Compile Sources', 為特定檔案添加編譯選項-fno-objc-arc。
C++中封裝Objective-C類的捷徑您可能已經注意到,PIMPL解決方案使用兩個層級的間接引用。 如果封裝的目標類像本例中的一樣簡單,就可能會增大了複雜性。 雖然Objective-C的類型一般不能使用在純C++中,不過有一些在C中實際已經定義了。id類型就是其中之一,它的聲明在<objc/objc-runtime.h>標頭檔中。雖然會失去一些Objective-C的安全性,你還是可以把你的對象直接傳到C++類中:
#include <objc/objc-runtime.h>namespace abc{ class Widget { id /* ABCWidget* */ wrapped; public: Widget(); ~Widget(); void Reticulate(); };}
不建議向id對象直接發送訊息。這樣你會失去很多編譯器的檢查機制,特別是對於不同類中有著相同selector名字的不同方法時。所以:
#include "Widget.hpp"#import "ABCWidget.h"namespace abc{ Widget::Widget() : wrapped([[ABCWidget alloc] init]) { } Widget::~Widget() { [(ABCWidget*)impl release]; } void Widget::Reticulate() { [(ABCWidget*)impl reticulate]; }}
像這樣的類型轉換很容易在代碼中隱藏錯誤,再嘗試一個更好的方式。在標頭檔中:
#ifdef __OBJC__@class ABCWidget;#elsetypedef struct objc_object ABCWidget;#endifnamespace abc{ class Widget { ABCWidget* wrapped; public: Widget(); ~Widget(); void Reticulate(); };}
如果這個標頭檔被一個mm檔案引用,編譯器可以充分識別到正確的類。 如果是在純C++模式中引用,ABCWidget*是一個等價的id類型:定義為typedef struct objc_object* id; 。 #ifdef塊還可以被進一步放到一個可重用的宏中:
#ifdef __OBJC__#define OBJC_CLASS(name) @class name#else#define OBJC_CLASS(name) typedef struct objc_object name#endif
現在,我們可以前置聲明在標頭檔中一行就可以適用於所有4種語言: OBJC_CLASS(ABCWidget); 轉載請註明出處:http://blog.csdn.net/horkychen