Hatena::ブログ(Diary)

のき屋

2012-08-09

Objective-C の const なインスタンスについて

const 教 in C++

C++ だとメンバ関数をこう書きます。

const ClassRet ClassFunc::func(const ClassArg& src) const;

const だらけです。const 教とかいわれたりしますが、

  • 返り値を const にすることで instFunc.func(instArg) = instRet とかをコンパイルエラーにできる。
  • 引数を const にすることで const ClassArg を渡せる。(const じゃなくても分かりやすくて安全になる。)
  • 関数を const にすることで内部の不変性を保証して const ClassFunc でもこの関数を使えるようにする。

という意味があり、とても重要なことです。

const 教 in Objective-C

ところが、Objective-C ではこれらのどちらもできません。だから、こんなプログラムは const が完全にスルーされて、コンパイルは通って、実行もできます。

#import <Foundation/NSObject.h>

@interface MyClass : NSObject {
	int value;
}

- (void)changeValue:(int)valueNew;
@end

@implementation MyClass
- (void)changeValue:(int)valueNew
{
	value = valueNew;
}
@end

int main(void)
{
	const MyClass *inst = [[MyClass alloc] init];  // const なインスタンス
	[inst changeValue:10];  // const な領域の変更 (C 的にはできないはず・・・)
	[inst release];

	return 0;
}

もう少し説明すると MyClass の実体は struct objc_object という、クラスを指すポインタとインスタンスが持っている変数を持った構造体で、イメージとしては

typedef struct {
	struct objc_class *isa;  // どんなクラスか
	int value;  // メンバ変数
} MyClass;

なのですが、changeValue 以前と changeValue 後でその中身が書き換えられてしまいます。これは inst->value を見れば*1分かります。

原因 (詳細)

原因は「Objective-C だから」なのですが、もう少し詳しく言うと、

  • [foo bar] は foo を id 型の引数とする C の関数*2に変換される。
  • オブジェクトのポインタであれば、id 型に暗黙のキャストができる。たとえそれが const でも。

というのが原因で、

const MyClass *instConst = [[MyClass alloc] init];
id tmp = instConst;
MyClass *inst = tmp;

なんて Objective-C にとっては朝飯前です。さすが、Objective-C、心が広い・・・?

C++ 的思考だとこれは const_cast だし暗黙ですが、Objective-C ですからそういう考えではいけないようです。

そもそも

もしインスタンスを const にできたとしたら、どのメソッドを実行していいのか分かりません。C++ のような関数の const がないので changeValue で中身が不変なのかどうかはわからず、const なインスタンスで changeValue を実行していいか判断できません。Objective-C は C++ のような堅い言語ではないので、そういう面倒な保証はしません。そこが Objective-C の良さであり、分かりにくさでもあるのです。

変数を変更したくないなら、NSMutable... に対する NS... のような immutable なオブジェクトを作れば良いのです。

だから

  • const ClassName *instance はほとんど無意味。
  • const 教なら immutable 教とも仲良くしておく。
  • Objective-C と C++ は違うから Objective-C で const を使わなくても教義に反しない。

ということでした。

*1:value は @public にしていないのでコンパイル時に警告は出ます。もちろん、そこは Objective-C なので実行できます。

*2:返り値の型によって id objc_msgSend(id self, SEL _cmd, ...) とか double objc_msgSend_fpret(id self, SEL _cmd, ...) とか void objc_msgSend_stret(void * stretAddr, id self, SEL _cmd, ...) とか。IMP 型も似たような感じ。