Cocos2d-x 3.0 beta では CCArray に取って代わる Vector というクラスが作られました。

便利になって良いと思うのですが、不満な点が2つほどあります。

  • operator[] が定義されていない
  • iterator 経由でアクセスするとリークする

どちらも根は同じ問題です。つまり、Object を生のポインタのまま格納していることことが問題なのです。

現在の最新版のソースでは、以下のようになっています。

template<class T>
class Vector {
...
private:
  std::vector<T> _data;
};
Vector<Sprite*> v;
v.pushBack(Sprite::create(...));

Sprite は、Object を継承したクラスなので、メンバなどで保持する際には retain, release が必須です。

Vector クラスは pushBack する際に retain したり、デストラクタで release を呼び出したりと、結構頑張って参照カウントを狂わせないようにしています。

しかし std::vector<Sprite*> という形でメンバに持っている以上、限界はあります。

まず、以下のようなコードが書けません。

template<class T>
class Vector {
public:
  T& operator[](size_t n) { return _data[n]; }
private:
  std::vector<T> _data;
};

これは、以下のようなコードを書くと簡単にリークしてしまうからです。

Vector<Sprite*> v;
v.pushBack(Sprite::create(...));
v[0] = Sprite::create(...); // v[0] の要素を release していないのでリークする
                            // おまけに代入したオブジェクトを retain していないのでアクセス違反を起こす

もう一点、iterator に対して代入ができません。

Vector<Sprite*> v;
...
v.erase(std::remove_if(v.begin(), v.end(), []() { ... }),
        v.end());

Vector が提供している iterator は vector::iterator の typedef であるため、retain, release を正しく呼び出さずにリーク、あるいはアクセス違反を起こします。

これらの問題を解決する一番簡単な方法は、intrusive_ptr のようなクラスを使う(あるいは自作する)ことです。

template<class T>
class Vector {
...
private:
  std::vector<intrusive_ptr<T>> _data;
};

これだけで上記の問題は解決します。operator[] では intrusive_ptr<T>& を返すため、代入してもリークしないし、iterator の各要素は intrusive_ptr<T> 型になるので、代入してもリークしません。

デメリットとしては、ユーザが intrusive_ptr に対する知識を知らないといけないということです。 しかし、intrusive_ptr を使わないというのは、そのデメリットを上回るだけのデメリットがあると考えられます。 手動での参照カウントのミスによってバグを埋め込み、修正に時間を掛けるよりは、最初に intrusive_ptr について調べておいて安全に使ったほうが、トータルに掛ける時間は少なくなると考えられます。 また、Objective-C が ARC (Auto Reference Counting) を導入し、参照カウントの手間を無くすという方向に進んでいる中で、手動参照カウントなんていう時代遅れなことをするのは、ちょっと最新のライブラリとしては残念過ぎるのではないかと思います。

正直、なぜ Cocos2d-x が intrusive_ptr を使わないのかが不思議でなりません。これだけ利用者がいるなら、きっと誰か提案しているはずで、何か理由があって却下されたと思うのですが、コミュニティとかを探すだけの気力も無いので、大人しくオレオレ intrusive_ptr を自作して利用するのでした。