ぱらなのブログ

C++で任意のオブジェクトを吐き出すものが作りたい

2Dアクションゲームの設計を考えていたときにマップチップを生成したりする際のデータベース的なのが必要になったので。

std::unordered_mapとテンプレートで殴ることでそれっぽい動作ができます。
std::functionを使うとオブジェクトの生成時に初期化もしたり、管理がうまくできていい感じです。
Siv3Dのシーンマネージャーを理解して得たものそのままなんですが、そこまでじっくり読んで理解するのも大変なので書く価値もあるかなと思い書いてみます。

前提とか

共通の基底クラスを継承させたクラスがある状態を想定。
基底クラスをInterface、それぞれ派生クラスをA,B,Cのように表記します。

辞書を作る

まあ辞書なのでunordered_mapを使いますね。

普通に使うと、例えば<int, string>のように指定するとstringしか取り出せませんが、値のテンプレート引数をインターフェースにすることで任意のオブジェクトを出せる配列にできます。

std::unordered_map<string, Interface*> index;

まあ生ポインタなんて使うわけがないのでスマポに

std::unordered_map<string, std::shared_ptr<Interface>> index;

なんで一度生ポインタを使って見せたのかというと、段階を踏んでいかないと後々意味不明になるからです

とりあえずこれでも取り回しは悪くないですね。目的によっては充分実用圏内でしょう

std::unordered_map<string, std::shared_ptr<Interface>> index;

//登録
index["えー"] = std::make_shared<A>();
index.try_emplace("びー", std::make_shared<B>());

これの問題はオブジェクトの再生成が行われないということです。ポインタをぶん投げているだけなわけですから、これを中心に置いたらただのグローバル変数になるわけです。

再確保するにはどっかでnewを呼ぶ必要がありますが、要するにmake_sharedを配列に突っ込めば解決しますね。

辞書をstd::function<>にすればmake_sharedの配列が組めるので大丈夫ですね

std::unordered_map<string, std::function<std::shared_ptr<Interface>()>> index;

index.try_emplace("え", std::make_shared<A>);

auto hoge = index.at("え")();

これで完成。stringじゃなくてenumにしたいとかだったら書き変えるか、テンプレートにでも置き換えておけばいいでしょう。

あらゆるオーバーヘッドは気にしたら負けです。はい。

これでコード内では任意の文字列から任意のオブジェクトを生成することができます。完全に辞書として動かすならシングルトン化したりするとよさそうですね。


さて初期化の数値を色々特別に設定する必要があるときはさらにめんどくさいです。が、それが目的です。

要するに初期化にコンストラクタだけでなく、別のメンバも呼んで初期化する必要があり、それをしてから返したいときですね。

つまりauto hoge = make_shared<A>();ってやったあとに
hoge.init();とかを呼ぶことで初期化してからhogeを返したいってときです。要するにSiv3Dのシーンマネージャーです。

auto hoge = make_shared<A>();

hoge.init( initdata );

return hoge;

↑こうやりたい

なのでこれをそのまま関数(ラムダ式)にすればごり押すことができます。

ざっとサンプルコード書きました

#include <iostream>
#include <unordered_map>
#include <memory>
#include <functional>

using namespace std;

class Interface
{
private:

    int x;

protected:

    int get_x()
    {
        return x;
    }

public:

    void init(int _x)
    {
        x = _x;
    }

    virtual void output() = 0;
};

class A : public Interface
{
public:
    void output() override
    {
        cout << "Aのoutput" << endl;
    }
};

class B : public Interface
{
public:

    void output() override
    {
        cout << "Bだよ!!" << endl;
    }

};

using InitData = int;

class Index
{
private:

    std::unordered_map<string, std::function<std::shared_ptr<Interface>()>> index;

    template<class T>
    void add(const string& str, InitData data)
    {
        auto f = [&]()
        {
            auto hoge = make_shared<T>();

            hoge->init(data);

            return hoge;
        };

        index.try_emplace(str, f);
    }

public:

    Index()
    {
        InitData i = 10;
        add<A>("えー", i);
        add<B>("びー", i);
    }

    std::shared_ptr<Interface> at(const string& str)
    {
        return index.at(str)();
    }
};

int main()
{
    Index i;
    i.at("えー")->output();
    i.at("びー")->output();
}

InitDataってなんだよって思うかもしれませんが、ポインタが飛んで来るときに使いたくなると思います。自身を保持するクラスのthisポインタとかですね。

ツッコミどころはあると思いますが、何がしたいのかは分かると思います。というか分からなかったらこんなわけわかんないものは使えないでしょうけど……

このままだと生成コストがやばいのでstatic変数を上手く使ったりシングルトンにしないと使えないんですが、誰か私にちゃんとコンパイルが通るシングルトンを伝授してください。

慣れないはてなブログへの愚痴

ところでこれmarkdownで書いてるんですが、qiitaのに慣れてるので改行がめんどくさいですね。途中から空行挟んで改行するようにしたのでちょっと見た目が変わってますがあんまり気にしないでください。