読者です 読者をやめる 読者になる 読者になる

generic function call

サイト本体では既に告知していますが、コミケ受かりました。スペースは 3日目東ホ-44b primitive です。
予定より大きく遅れていますが、前回よりは色々進歩したものを出せる予定です。


最近の状況。
それっぽく動く水車、raymarching による背景、以前 書いたブラウザから RTS モード (兼レベルエディタ) がそれなりに形になってきたところ。
第三者がブラウザ経由でザコ敵を操作することで、サーバープレイヤーにはザコ敵が普段見せない動きで急襲してきたり、たまに助けてくれたりするように見える、ようになる予定です。(マウスドラッグでキャラ移動はさすがにレベルエディタ専用)



今作では汎用関数呼び出し機能が用意されており、地味ながら根幹を支える重要な役割を果たしています。なんとなくこれについて書いてみます。
例えば、実装を知らないオブジェクトのメンバ関数を呼びたいケースがしばしばあります。(interface 以下の実装見せたくないけど、interface に関数増やしたくないとか) また、関数呼び出しのコンテキストを保存して後で呼びたいケース、関数を ID で呼びたいケースもしばしばあります。(プレイバックしたい場合、ネットワーク経由で関数呼びたい場合とか) こういう状況のための機能です。

仕組みとしてはとても単純で、関数に ID をつけ、その ID と void* の引数&戻り値で対応する関数を呼ぶようにするだけです。
例えば以下のような class を対応させるには

class Hoge
{
public:
    void setPosition(const vec3 &pos);
    const vec3& getPosition() const;
    void doSomething(int arg1, int arg2);
};

このようにします。

enum FunctionID {
    FID_setPosition,
    FID_getPosition,
    FID_doSomething,
};
class ICallable {
public:
    virtual ~ICallable() {}
    virtual bool call(FunctionID fid, const void *args, void *ret=nullptr) { return false; }
};

class Hoge : public ICallable
{
public:
    void setPosition(const vec3 &pos);
    const vec3& getPosition() const;
    void doSomething(int arg1, int arg2);

    bool call(FunctionID fid, const void *args, void *ret=nullptr) override
    {
        switch(fid) {
        case FID_setPosition: setPosition(*(vec3*)args); return true;
        case FID_getPosition: if(ret){*(vec3*)ret=getPosition();} return true;
        case FID_doSomething: doSomething(std::get<0>(*(std::tuple<int,int>*)args), std::get<1>(*(std::tuple<int,int>*)args)); return true;
        }
        return false;
    }
};

void usage(ICallable *hoge)
{
    vec3 pos;
    std::tuple<int,int> args(0,0);
    hoge->call(FID_getPosition, nullptr, &pos);
    hoge->call(FID_setPosition, &pos);
    hoge->call(FID_doSomething, &args);
}

FID_setPosition とかの enum のリストは string symbol とかを使えば省略できますが、個人開発では大した手間でもないので最適化の一環でこのままにしています。

機能的には上の実装で十分で、あとは必要な class の必要な関数に同様の処理を書くだけです。
しかし、このままだと switch(fid) {} の中を書くのがめんどくさそうな感じです。いかにもミスしやすそうな上、ポインタの cast はミスったら分かりにくいバグを生む可能性があります。
こういうのはがんばって template をこね回せば型安全にしつつ記述を一般化できるので、安全のためにもそうしてみます。
手順としては、関数の型 (普通の関数,メンバ関数,constメンバ関数 * 引数の数) それぞれについて template で実装しつつ、入り口となる関数は overload しまくってユーザー視点では 1 種類になるように保つ、という感じです。

// 長くなるのでメンバ関数引数 1 個版のみ例示

// template 関数は特殊化できないため、内部実装用 struct を用意
template<class R, class C, class A0>
struct Call_MemFn1
{
    typedef R (C::*F)(A0);
    void call(F f, C &o, void *r, const void *a)
    {
        // ValueHolder<R> は R から参照と const を取っ払って値で保持する代物
        // ValueList は std::tuple の類似品だが、参照と const を取っ払って値で保持する。引数の受け渡しはこれで行う想定。
        typedef ValueHolder<R> RT;
        typedef ValueList<A0> Args;
        Args &args = *(Args*)a;
        if(r) { *(RT*)r=(o.*f)(args.a0); }
        else  {         (o.*f)(args.a0); }
    }
};
// 戻り値が void の場合は特殊化しないといけない
template<class C, class A0>
struct Call_MemFn1<void, C, A0>
{
    typedef void (C::*F)(A0);
    void call(F f, C &o, void *r, const void *a)
    {
        typedef ValueList<A0> Args;
        Args &args = *(Args*)a;
        (o.*f)(args.a0);
    }
};
template<class R, class C, class A0>
inline void BinaryCall(R (C::*f)(A0), C &o, void *r, const void *a)
{
    Call_MemFn1<R,C,A0>().call(f,o,r,a);
}
// こういうのを
// (非メンバ関数、メンバ関数、const メンバ関数 の 3 バリエーション) * (引数の数)
// 分用意する。
// さすがに人力ではしんどいのでスクリプトなりマクロなりで自動生成させる。

(完全な実装例。こちらは参照をポインタで扱うバリエーションなんかも用意しています。)
ものすごく泥臭い実装で、なんかもっと上手いやり方がありそうな気もしますが、とりあえずこれを使うと前述の switch の中がスマートに書けるようになります。

class Hoge : public ICallable
{
public:
    // ... 

    virtual bool call(FunctionID fid, const void *args, void *ret=nullptr)
    {
        switch(fid) {
        case FID_setPosition: BinaryCall(&Hoge::setPosition, *this, ret, args); return true;
        case FID_getPosition: BinaryCall(&Hoge::getPosition, *this, ret, args); return true;
        case FID_doSomething: BinaryCall(&Hoge::doSomething, *this, ret, args); return true;
        }
        return false;
    }
};

これはマクロで包むことで大きく簡略化できます。

#define CallBlock(...)\
    virtual bool call(FunctionID fid, const void *args, void *ret=nullptr)\
    {\
        typedef std::remove_reference<decltype(*this)>::type this_t;\
        switch(fid) {\
        __VA_ARGS__\
        }\
        return false;\
    }

#define CallMethod(name) case FID_##name: BinaryCall(&this_t::name, *this, ret, args); return true;
#define Call(obj,func,...) obj->call(FID_##func, __VA_ARGS__)


class Hoge : public ICallable
{
public:
    // ... 

    CallBlock(
        CallMethod(setPosition)
        CallMethod(getPosition)
        CallMethod(doSomething)
    )
};

void usage(ICallable *hoge)
{
    vec3 pos;
    ValueList<int,int> args(0,0);
    Call(hoge, getPosition, nullptr, &pos);
    Call(hoge, setPosition, &pos);
    Call(hoge, doSomething, &args);
}

CallBlock の実装に若干注意が必要です。メンバ関数ポインタは &class_name::func_name と書く必要があり、CallBlock マクロは this の型を知る必要があります。
幸い C++11 では std::remove_reference::type で this の型を取れるため、ユーザーに書かせずに済みます。
また、親 class や別のオブジェクトの call() へリダイレクトさせたいケースもあるので、実際には CallBlock() はもう少し複雑になります。
ともあれ、これだけ楽に書ければ実運用に耐えられそうです。

virtual 関数呼び出しよりもコストは高くつきますが、よほどの回数回さない限り問題にはならないと思われます。レベルエディタとの通信、オブジェクト同士の通信、リプレイなどの実装に役立っています。