【Objective-C++】NSObjectラッパークラスで快適コーディングを実現する

Objective-CのオブジェクトをC++のクラスでラップすることで、==による比較処理やa.equals(b)形式によるメソッド呼び出し式を実現することができます。今回紹介するテクニックによって、Java言語やSwift言語のような簡潔な記述や高い開発効率を実現することが可能になります。

目次

NSObject 基本ラッパークラス

実装方法は以下の通りです。ns_basic_objectは基本クラスです。実際にはns_basic_objectを継承したクラスをObjective-Cオブジェクト毎に作成して利用します。ns_objectがその継承例です。

template<typename T> struct ns_basic_object {
   T obj;
   ns_basic_object() : obj(nil) {}
   ns_basic_object(T obj) : obj(obj) {}
   operator T() const { return obj; }
   BOOL equals(NSObject* v) const { return [obj isEqual:v]; }
// BOOL operator==(const T& v) const { return  [obj isEqual:v]; }
// BOOL operator!=(const T& v) const { return ![obj isEqual:v]; }
protected:
   ~ns_basic_object() {}
};

struct ns_object : ns_basic_object<NSObject*> {
   using ns_basic_object<NSObject*>::ns_basic_object;
};

Objective-CオブジェクトからC++オブジェクトへの変換は暗黙的に行われます。

   ns_object obj = [NSObject new];
   
   NSObject* nsobj = obj; // 変換(ns_object → NSObject)
   obj = nsobj;           // 変換(NSObject  → ns_object)
   
   printf("%d", obj.equals(obj));         // 1
// printf("%d", (obj == obj));            // 1
// printf("%d", (obj != [NSObject new])); // 1
   printf("%d", [obj isEqual:obj]);       // 1
   
   obj = nil;
   printf("%d", obj.equals(obj));   // 0
// printf("%d", (obj == obj));      // 0
// printf("%d", (obj != nil));      // 1
   printf("%d", [obj isEqual:obj]); // 0
   
   obj = @"str";
   printf("%d",  obj.equals(@"str"));       // 1
   printf("%d", !obj.equals(@"999"));       // 1
// printf("%d",  obj == (NSObject*)@"str"); // 1
// printf("%d",  obj != nsobj);             // 1

C++の機能により、比較演算子==による比較や、.記法よるメソッド呼び出しを実現することが可能になりました。またメッセージ式([obj isEqual:obj])による呼び出しにも対応しています。C++オブジェクトからObjective-Cオブジェクトへの暗黙的な変換も可能です。

比較演算子を有効にするには、必要に応じて==!=の行をコメントアウトします。ただしポインタ比較が行えなくなるため注意が必要です。

NSString ラッパークラス

先程定義した基本ラッパークラス(ns_basic_object)を継承することで、NSObject以外の様々な型に対応することもできます。

以下はNSStringに対応したns_stringクラスの例です。NSStringのメッセージ式をラップしたメンバ関数(メソッド)をいくつか定義しています(size, substr, 添字演算子, etc.)。

struct ns_string : ns_basic_object<NSString*> {
   using ns_basic_object<NSString*>::ns_basic_object;
   
   BOOL equals(NSString* s) const { return [obj isEqualToString:s]; }
// BOOL operator==(NSString* v) const { return [obj isEqualToString:v]; }
   
   NSUInteger size() const { return [obj length]; }
   ns_string substr(NSUInteger loc, NSUInteger len) const { return [obj substringWithRange:NSMakeRange(loc, len)]; }
   const char* c_str() const { return obj.UTF8String; }
   unichar operator[](NSUInteger i) const { return [obj characterAtIndex:i]; }
};
ns_string s = @"NSString";

printf("%ld\n", s.size());    // "8"
printf("%c%c", s[0], s[1]);   // "NS"
puts(s.substr(2, 6).c_str()); // "String"
puts(s.replace(@"NS", @"ns").c_str()); // "nsString"

[s substringFromIndex:0];          // メッセージ式
[s componentsSeparatedByString:s]; // 暗黙の型変換(ns_string --> NSString*)

s = nil;
puts(s.substr(2, 6).c_str()); // "(null)"

NSObject *nsobj = s;
ns_basic_object<NSString*> obj = s;

何度も言いますが、メッセージ式([s substringFromIndex:0])の利用も可能です。ラッパークラスはObjective-Cオブジェクトとの互換性があるため、既存の型を一括でラッパクラスに置き換えても、既存のコードはそのまま機能することになります。

- (void)print:(NSString *s) { puts([s UTF8String]); }
↓
- (void)print:(ns_string s) { puts([s UTF8String]); }

私は普段NSString *Stringという別名を付けていたため、移行は比較的容易に行えました。

// Before
typedef NSString * String;
- (void)print:(String s) { puts([s UTF8String]); }

// After
typedef ns_string String;
- (void)print:(String s) { puts([s UTF8String]); }

ns_stringを活用する場合には、今後のリファクタリングの手間を考慮して型の別名を活用することをオススメします。

using String = ns_string;

- (void)print:(String s) { puts([s UTF8String]); }
参考までに、C++の世界ではtypedefの代わりにusingが使われます。

NSStringのオートボクシングを実現する

NSStringの特性や変換コストを考えると、あまりオススメはできませんが、C言語スタイル文字列に対するオートボクシングの実現も可能です。以下の変換コンストラクタ(ns_string(const char* s))を先程のns_stringクラスに追加することで、文字列リテラルからの初期化に対応することができます。

using String = struct ns_string : ns_basic_string<NSString*> {
   using ns_basic_string<NSString*>::ns_basic_string;
   
   //「C言語スタイル文字列 → NSString型」への変換コンストラクタ
   ns_string(const char* s) : ns_basic_string<NSString*>::ns_basic_string(@(s)) {}
   // おまけ
   NSUInteger length() { return [obj length]; }
};

String string = ""; // オートボクシング
printf("%ld",  string.length());

これによって、Java言語と同等の記述が実現できるようになりました。

NSNumber ラッパークラス

struct ns_number : ns_object<NSNumber*> {
   using ns_object<NSNumber*>::ns_object;
   ns_number(int i)       : ns_object(@(i)) {}
   ns_number(CGFloat f)   : ns_object(@(f)) {}
   ns_number(NSInteger l) : ns_object(@(l)) {}
   explicit operator int()       { return obj.intValue; }
   explicit operator CGFloat()   { return obj.doubleValue; }
   explicit operator NSInteger() { return obj.integerValue; }
   bool operator<(NSNumber* b)  { return [obj compare:b] == NSOrderedAscending; }
   bool operator>(NSNumber* b)  { return [obj compare:b] == NSOrderedDescending; }
// bool operator==(NSNumber* b) { return [obj compare:b] == NSOrderedSame; }
// bool operator!=(NSNumber* b) { return [obj compare:b] != NSOrderedSame; }
   bool operator==(int b)       { return obj.intValue == b; }
   bool operator==(CGFloat b)   { return obj.doubleValue == b; }
   bool operator==(NSInteger b) { return obj.integerValue == b; }
};

基本型への暗黙変換は抑制しています。代わりに、基本型との演算処理(ns_number(@9) == 9)が行えるようになります。

using Number = ns_number;

Number a = @1;
Number b = @2;
Number c = 3;
Number d = 3.14;

NSInteger i = (NSInteger)a;
Number e = i;

assert(a == a && a != b);
assert(a < b && b > a);
assert(a == 1 && a == i && d == 3.14);
assert(1 == NSInteger(a));
// assert(d == @3.14 && d != @3 && a == @1);

名前空間の定義

実際には名前空間を用いて以下のような運用を行うと良いでしょう。

/* Foundation++.h */

#import <Foundation/Foundation.h>

namespace ns {
   template<typename T> struct basic_object { /* 定義省略 */ };
   struct object : basic_object<NSObject*>  { /* 定義省略 */ };
   struct string : basic_object<NSString*>  { /* 定義省略 */ };
   struct number : basic_object<NSNumber*>  { /* 定義省略 */ };
}

/* main.mm */

#import <Foundation/Foundation.h>
#import "Foundation++.h"

using String = ns::string;
using Number = ns::number;

int main() {
   String s = @"hello world";
   puts(s.c_str());
}

汎用化の技法

親クラスの定義を小クラス側で再利用するための技法も紹介しておきます。テンプレート機能でこれを実現します。

template<class T> struct ns_basic_string : ns_object<T> {
   using ns_object<T>::ns_object;
   
   BOOL equals(NSString* s) const { return [this->obj isEqualToString:s]; }
// BOOL operator==(T v) const { return [this->obj isEqualToString:v]; }
   
   ns_basic_string<NSString*> substr(NSUInteger loc, NSUInteger len) const {
      return [this->obj substringWithRange:NSMakeRange(loc, len)];
   }
   ns_basic_string<NSString*> replace(NSString* target, NSString* replacement) const {
      return [this->obj stringByReplacingOccurrencesOfString:target withString:replacement];
   }
   
   NSUInteger size() const { return [this->obj length]; }
   const char* c_str() const { return this->obj.UTF8String; }
   unichar operator[](NSUInteger i) const { return [this->obj characterAtIndex:i]; }
};

struct ns_string : ns_basic_string<NSString*> {
   using ns_basic_string<NSString*>::ns_basic_string;
   ns_string(const ns_basic_string<NSString*>& s) : ns_basic_string<NSString*>::ns_basic_string(s) {}
}; /// using ns_string = ns_basic_string<NSString*>;

struct ns_string_m : ns_basic_string<NSMutableString*> {
   using ns_basic_string<NSMutableString*>::ns_basic_string;
   ns_string_m(NSString* s) : ns_basic_string<NSMutableString*>::ns_basic_string([s mutableCopy]) {}
   ns_string_m& append(NSString* s) { return [obj appendString:s], *this; }
};

最適化と値渡し

以下の関数は、最適化が働くと、いずれも同一の実行コードに変換されます。

NSUInteger size(ns_string s) { return s.size(); }        // 値渡し
NSUInteger size(const ns_string& s) { return s.size(); } // 参照渡し
NSUInteger size(NSString* s) { return [s length]; }      // ポインタ渡し(ObjC生オブジェクト)

メソッドの場合も同じです、ラッパークラスの内部ポインタのみがポインタ渡しされます。つまりいずれも実引数をNSString* sと宣言した場合と同一の実行コードに変換されるということです。

ただし、最適化が行われなかった場合やデバッグビルド時には、値渡しは余計なコンストラクタ呼び出しやデストラクタ呼び出しを引き起こすため、値渡しの利用はオススメできません。

代わりに、参照渡しとObjective-Cオブジェクトのポインタ渡しを上手く使い分けることで、上記の問題を回避することができます。

// Before
ns_string replace(ns_string target, ns_string replacement) {
   return [this->obj stringByReplacingOccurrencesOfString:target withString:replacement];
}
- (ns_string)sub:(ns_string s) {
  return s.replace(@"a", @"b");
}

// After
ns_string replace(NSString* target, NSString* replacement) const {
   return [this->obj stringByReplacingOccurrencesOfString:target withString:replacement];
}
- (ns_string)sub:(const ns_string& s) {
   return s.replace(@"a", @"b");
}

本イディオムの目的

Objective-CのオブジェクトをC++のクラスでラップする本テクニックは、Objective-CやCocoaフレームワークの冗長的な記述を回避する目的に利用できる他、C++製の既存フレームワークやテンプレートライブラリの資産を活用する目的にも利用できます。

例えば、以下のC++標準文字列クラス(std::string)向けに作られた既存のテンプレート関数(print_string)は、今回作成したns_stringにも適用することができます。

template<class T> void print_string(const T& v) {
   puts(v.c_str());
}

print_string(std::string("a"));
print_string(ns_string(@"a"));  // OK: 互換性あり
std::stringのメンバ関数c_strと同名のメンバ関数がns_string側に定義されているため、ns_stringはstd::stringと一部共通のインターフェースを持っているとみなされます。

このように、本イディオムは、既存のコードやロジックを再利用する目的としても活用できます。

begin, end関数の多重定義や演算子オーバロードを活用すれば、C++標準テンプレートライブラリのアルゴリズムを活用することも可能になります。

仮想テーブルについて

今回紹介した基本クラスns_basic_objectではvirtualキーワードによる仮想関数の宣言を用いていません。「# 最適化と値渡し」のゼロオーバヘッドの特性を実現する意図があります。

仮想関数や純粋仮想関数を用いてより柔軟な設計を実現することもできますが、本イディオムはあくまでObjective-Cオブジェクトの緩い拡張/より良い拡張(Better Objective-C)を目的としています。より柔軟な設計や、仮想関数を用いた抽象クラス等の設計を用いる場合には、多くのケースでは仮想デストラクタによる対処が必要となるため注意して下さい。

参考

広告