Cocos2d-x の CREATE_FUNC をマシな実装にした

2014-01-28 14:13:00

Cocos2d-x には CREATE_FUNC というマクロがあります。これは以下の実装になっています。

#define CREATE_FUNC(__TYPE__) \
static __TYPE__* create() \
{ \
    __TYPE__ *pRet = new __TYPE__(); \
    if (pRet && pRet->init()) \
    { \
        pRet->autorelease(); \
        return pRet; \
    } \
    else \
    { \
        delete pRet; \
        pRet = NULL; \
        return NULL; \
    } \
}

これを以下の様に使うことで、autorelease 済みの TestLayer オブジェクトを生成できます。

class TestLayer : public cocos2d::Layer {
public:
    bool init();
    CREATE_FUNC(TestLayer);
};

ただ、これには問題があります。 マクロを見れば分かるように、createinit 関数はパラメータを持ちません。 つまり、初期化する際に引数を渡したい場合、CREATE_FUNC を利用できません。

もし値を渡したい場合、CREATE_FUNC マクロがやっていることを手動で書く必要があります。

class TestLayer : public cocos2d::Layer {
public:
    bool init(int n, std::string s);
    static TestLayer* create(int n, std::string s) {
        auto p = new TestLayer();
        if (p->init(n, s)) {
            p->autorelease();
            return p;
        } else {
            delete p;
            return nullptr;
        }
    }
};

これはちょっと書くのが面倒過ぎます。 さらに init 関数のオーバーロードが増えると、create 関数の実装も増え、更に面倒になります。

これを解決するため、以下のクラスを作りました。

#include<utility>

template<class Derived>
struct create_func {
    template<class... Args>
    static Derived* create(Args&&... args) {
        auto p = new Derived();
        if (p->init(std::forward<Args>(args)...)) {
            p->autorelease();
            return p;
        } else {
            delete p;
            return nullptr;
        }
    }
};

Variadic Templates や Perfect Forwarding を使っているので、C++11 限定です。Cocos2d-x 3.0 を使うなら問題にならないでしょう。

これは以下の様に利用します。

class TestLayer : public cocos2d::Layer, create_func<TestLayer> {
public:
    bool init(int n);
    bool init(int n, std::string s);
    using create_func::create;
};
auto p = TestLayer::create(10); // init(10) が呼ばれる
auto p2 = TestLayer::create(10, "aaa"); // init(10, "aaa") が呼ばれる

CRTP を使っているため、create_func<TestLayer> と書く手間が発生します。が、それを書くだけで create 関数に値を渡すことでき、init 関数のオーバーロードもできます。

create 関数を書くのが面倒になったら、このクラスを利用することを考えましょう。

Cocos2d-x 3.0 beta の Vector がリークする

2014-01-26 11:21:00

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 を自作して利用するのでした。

新しいブログ作りました

2014-01-25 03:28:00

新しいブログ作りました。旧ブログはこちら

このブログはお手製です。Yesod で作りました。いろいろ機能は足りてないですけど、ちょっとずつ拡張しつつブログ書きつつで進めていきます。

2015/11/17追記

Yesod 使うのをやめて CppCMS で作り直しました。