JAVAとBREWのソース互換性について005

だいぶ実機でのチェックも済み、Object型くらいなら公開しても大丈夫かなという状況になりました。
VS6.0だとコンパイル通るのに、ARMコンパイラだとエラーになる場面が多すぎるんですよね。苦労しました。
特に「曖昧な定義です」系。
 
さて、まず目指すはJAVAとのソース互換性です。
やねうらお先生が作ったスマートポインターは素晴らしいものですが、やはりobj->hoge();という記述になってしまうのが問題です。
残念ながら . のオーバーライドは無いようです。ちぇ。
というわけで、頑張って「ポインター(のふりをしたクラス)にメソッドを作る」ようにしなければobj.hoge();は記述できないということになります。
ポインター(のふりをしたクラス)を今後擬似ポインターと呼びます。
 
次にインスタンスをどう実装するかです。
「参照されている限り消えない実体」であり「色んなところから参照される実体」です。
つまり擬似ポインターが指している、唯一の実体ですね。
例えばJAVAで…。


String str1 = new String("ほげ");
String str2 = str1;
とやっても"ほげ"自体は1つしか生成されないわけです。
さて、これをC++で実装してみましょう。

class Object{
protected:
ObjectData* m_pData;
public:
// 省略
};
こうやって、実際の"ほげ"にはm_pDataからアクセスすればいいわけです。

str2 = str1;
した時にはm_pDataをコピーすればいいわけですね。
そして今回使うのは単純な参照カウント式なのでその際参照カウントを増加させます。
参照カウントとは、実体側が「俺、一体いくつのポインターから参照されてるんだろう」というデータを保持しておき、どこからも参照されなくなったら自身をdeleteするというものです。
では実体側を実装してみましょう。

class ObjectData{
public:
int m_nCnt;
ObjectData() : m_nCnt(0){}
virtual ~ObjectData(){}
};
m_nCntがカウンターですね。自身を参照するポインターが増えるごとに+1され、減るごとに-1されます。
そして0になったら擬似ポインター側がObjectDataをdeleteしてくれます。
Stringクラスを作る時には

class StringData : public ObjectData{
public:
char* m_pszString;
StringData(){
m_pszString = NULL;
}
virtual ~ObjectData(){
if (NULL != m_pszString)
delete [] m_pszString;
}
};
class String : piblic Object{
// 省略
}:
こんな感じに継承させます。
そしてm_pszStringのように、必要な変数を追加します。

 
ではObjectとObjectDataをまとめてどうぞ。


class ObjectData{
public:
int m_nCnt;
ObjectData() : m_nCnt(0){}
virtual ~ObjectData(){}
};

class Object{
protected:
ObjectData* m_pData;

int inc();
int dec();
void insert_pointer(Object* pObject);
void insert_reference(const Object& obj);

private:
Object& operator*(){return *this;};
Object* operator&(){return this;};
public:

/*
コンストラクタ. デストラクタ.
*/
Object() : m_pData(NULL){};
Object(const Object& obj){
m_pData = obj.m_pData;
inc();
}
Object(Object* obj)
m_pData = NULL;
insert_pointer(pObject);
}
virtual ~Object(){
dec();
}

/*
Object型の代入演算子.
*/
Object& operator =(const Object& obj){
insert_reference(obj);
return *this;
}

/*
比較演算子.
*/
friend bool operator==(const Object& obj, void *p);
friend bool operator==(void *p, const Object& obj);
friend bool operator!=(const Object& obj, void *p);
friend bool operator!=(void *p, const Object& obj);

};

ObjectDataについては解説したので、Objectについて解説します。

int inc();
int dec();
参照カウントの増減を行なう関数です。
decには参照カウントが0になったらObjectDataをdeleteするようにしてあります。

Object& operator*(){return *this;};
Object* operator&(){return this;};
これらは*objや&objされると困るのでprivateに隠しておきました。

Object() : m_pData(NULL){};
Object(const Object& obj){
m_pData = obj.m_pData;
inc();
}
Object(Object* obj){
m_pData = NULL;
insert_pointer(pObject);
}
virtual ~Object(){
dec();
}
まず、基底コンストラクタとしてはm_pDataのNULL初期化が必須ですよね。
次のコピーコンストラクタではinsert_referenceの呼び出しを行います。
アドレス渡しについては、JAVAでの…。

String str = new String("ほげ");
を再現するためです。この記述はC++では…。

String str(new String("ほげ"));
になるのです。中ではinsert_pointerを呼んでいます。
デストラクタはdecを呼び出して参照数を減らします。


Object& operator =(const Object& obj){
insert_reference(obj);
return *this;
}
こちらはinsert_referenceを呼んでいます。
では最後に、insert_reference, insert_pointerの実装を…。

void Object::insert_reference(const Object& obj){
// ↑constにしないと、ARMコンパイラがうるさい!

// 同一ポインターなら無視
if (this == &obj)
return;

// 今までもっていた参照を解放
dec();

// ポインターのコピー
m_pData = obj.m_pData;

// 参照カウントを増加させる
inc();
}


void Object::insert_pointer(Object* pObject){

// 今までもっていた参照を解放
dec();

// NULLの代入だった場合は、解放のみ
// これは obj = null; を再現するため
if (NULL == pObject){
return;
}

// ポインタをコピー
m_pData = pObject->m_pData;

// 参照カウントを増加させる
inc();

// 参照元を削除
// obj = new Object で生成されたObject自体は不要なため
// 欲しいのはm_pDataの先に作られた実体だけだもの
delete pObject;
}

基本的に、今までの参照を解放し、新しいポインターを参照するようにします。
今までの御主人様をポイして新しい御主人様に尽くすわけです。
その際今までの御主人様には「あなたのメイドは一人減りましたわよ!」と通知してあげます。(decの中で行なっています)
メイドが一人も居なくなると御主人様は「もう生きてる望みも無いんじゃー!」と嘆き、最後の一人が「それなら出て行くついでに殺してあげるわ!!」と殺します。(delete m_pData;)
 
…。ちょっと例えに無理がありましたか。
ところでこれ、パッと見アップキャストとダウンキャストはどうなのよ思えます。私も思いました。
だってObjectもStringも「擬似ポインターであって、ポインターじゃないじゃん。ポインターじゃなきゃキャストはできないじゃん」ですよね。
え? 思わない?
思わないような人は、この日記を読んで得るものはありませんw
 
さて、解説です。

Object obj;
String str = new String("ほげ");
obj = str;
str = obj;
ですが、どちらも成功します。
何故なら = 代入を行なった際に実際に呼び出されるのはinsert_referenceであり、insert_referenceはただ単にm_pDataのアドレスコピー(と参照数の増加)をしているに過ぎないからです。
なら…。

obj = str;
bool b = ((String)obj).equals("ほげ");
これはどうだ!
……できます!
これ、作るまで私も知らなかったんですけど…。

(String)obj

String strTmp(obj);
の略みたいですよ。実際はstrTmpという名前は無く、無名のString変数が作られるんですが。
こんなことも知らずにC++使ってのか!! すいません!!
というわけで、コピーコンストラクタでも結局insert_referenceを呼び出してるだけなので上と同じ理由で成功します。
その後すぐにstrTmpは破壊されます。
やったね。これでキャストもやりたい放題。
 
今うちの環境では…。

String str = new String("ほげ");

str += "あひょー!";
str += "ぷげら" + str + "ちょめ";
str = str + "あれ?";

こんな感じに暴れまわっています。
newやdeleteが頻繁に呼ばれるので、きちんと自前のメモリープールを作ってエラーが起こらないようにしましょうね。
MALLOCやFREEを使っていては万が一のメモリー確保失敗でどうにもならなくなってしまいます。
この辺りについては過去日記を読んでください。BREWで日記内検索かければひっかかります。
 
では最後に細かいポイント。
#define null (Object*)0
としておくと楽です。
単に0にしてると、ARMコンパイラが「曖昧だこら!」とエラーを出します。
nullという記述はJAVAの名残でしょうから、Objectにしか代入しませんよね。なので(Object*)0でOKと。
 
やはり実際に作ってみないと勉強にならないものです。
これを作っただけでも、C++の挙動について知らないことだらけだったことを痛感しました。
これからも頑張ります(`・ω・´)
 
何かこの記事に間違いあったら、どんどん指摘してください。
といっても、ここ尋ねてくるプログラマなんて3人くらいしかいないやヽ(*´ー`)ノ

[追記]
void operator =(const Object& obj);

Object& operator =(const Object& obj);
に書き直しました。
Effective C++によると、voidを返り値とした場合代入の連鎖(a = b = c;)が出来なくなってしまうと書かれているのですが、試したところ可能でした。ARMコンパイラでもOK。
うーむ。でも念のため書き直しておきましたとさ。