1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

OpenSiv3DでTOMLの値をclassの変数に割り当てるマクロを作った[闇]

Last updated at Posted at 2020-04-27

はじめに

若干ネタぎみなくらい闇実装です。


TOMLReaderで取得した値をclassの変数に入れていたのですが、毎回自分で書くのが面倒くさくなってきたので、もっと楽にできないかなぁと考えていました。
C++には動的なリフレクションもないので、toml側のkeyの文字列から自動で変数と紐づけることはできないのですが、Go言語のJSON読み込みみたいな感じにできんかなぁと思考錯誤していました。

// ちなみに、Goのはこーゆーかんじ
type Hoge struct {
    A    string    `json:"a"`
    B    string    `json:"b"`
}

↑ほど楽にはできないと思いましたが、こんな感じを目指すことにしました。

下準備

TOMLReaderで読み込んだ値をclassに変換するための共通の窓口を作ります。
今回はstd::hashのような感じで、関数オブジェクトのtemplate特殊化での実装としました。

TOMLBind.hpp
template<class Type>
struct TOMLBind
{
    // Type operator()(const s3d::TOMLValue& toml);
};

template<class Type>
Type TOMLBinding(const s3d::TOMLValue& toml)
{
    return TOMLBind<Type>{}(toml);
}
/*
 example:
     template<>
     struct TOMLBind<Hoge>
     {
         Hoge operator()(const s3d::TOMLValue& toml){
             // something
             Hoge ret;
             ret.hoge = toml[U"hoge"].get<s3d::int32>();
             return ret;
         }
     }
*/

これで自身の作ったclass事にTOMLBindの特殊化をしてあげれば、TOMLReaderで読み込んだ値をclassに変換できるようになりました。

auto hoge = TOMLBinding<Hoge>(TOMLReader(U"hoge.toml"));

ただ、これだとTOMLBindの実装を自分で書かなければいけないので手間です。
次からはこのTOMLBindの特殊化を自力でしなくていいようにしていきます。

自動で割り当てる

変数と文字列をどう紐づけるか

先にも述べたようにC++で変数名や関数名と文字列を動的に紐づけるのが難しく、TOMLのkey名からそれにあう変数に代入したり、セッターを呼ぶなんてことは難しいです。

なので静的にId番号をふっておくことで無理やり紐づけることにしました。

TOMLBind.hpp
template<int Num>
struct TOMLBindId
{
    enum
    {
        Value = Num
    };
    const s3d::TOMLValue& toml;
};
イメージ
struct Hoge
{
    int32 a;
    // ID1は変数a
    void operator()(const TOMLBindId<1>& id) {
        a = id.toml[U"a"].get<int32>();
    }
    String b;
    // ID2は変数b
    void operator()(const TOMLBindId<2>& id) {
        b = id.toml[U"b"].get<String>();
    }
};

Idをどうふるか

標準の組み込みマクロである__LINE__でふることにします。
(別に手動で管理してもいいんだけど面倒くさいし)

__LINE__だと他のファイルとかぶることが起こるのでもはやIdではなくなってしまうのですが
型 x Id の組み合わせでIdが被ることは、意図的に同じ行にしない限りは起こらないと思いますので、今回はよしとします。
(非標準ではあるが__COUNTER__であれば被らない)

イメージ
struct Hoge
{
    int32 a;
    void operator()(const TOMLBindId<__LINE__>& id) {
        a = id.toml[U"a"].get<int32>();
    }
};

割り当てるclass型がもつIdの範囲を決める

Idをふれたのはいいですが、ではその型の変数と紐づくId番号は何と何があるか知る方法がありません。
困ったのでID探索範囲を定義しちゃいます。

これも__LINE__マクロを使用してclassの最初と最後にIDを振っておきます。

(ファイルの行数の限度Maxを決め打ちしちゃってよければ1~Maxまで探索するというのも可能かもしれない)
(現状いけてなさを感じるので範囲をもう少しうまく取得したいなと思ってはいますが難しそう)
2020/5/7追記
どのみち__LINE__を使っている時点で限度はあるので
限度決め打ち版も作成しました。
実装まとめにコードをつけておきます。

イメージ
struct Hoge
{
    // ** ここから **
    using TOML_BIND_BEGIN_ID = TOMLBindId<__LINE__>;
    int32 a;
    void operator()(const TOMLBindId<__LINE__>& id) {
        a = id.toml[U"a"].get<int32>();
    }
    // ** ここまで **
    using TOML_BIND_END_ID = TOMLBindId<__LINE__>;
};

総当たりじゃ

IDをふってあるもののみ対応するようにconceptを定義しておきました。

TOMLBind.hpp
// 自動割り当てIdがふってあるか
template<class Type>
concept IsAutoTOMLBindable = requires(Type a)
{
    typename Type::TOML_BIND_BEGIN_ID;
    typename Type::TOML_BIND_END_ID;
};

// そのIdでの割り当てが可能か
template<class Type, int Num>
concept IsTOMLBindCallable = requires(Type a, TOMLBindId<Num> id)
{
    { a(id) } -> std::same_as<void>;
};

自動割り当て用のtemplate関数オブジェクトを定義して
再起的に次の割り当て関数も呼ぶというのをBEGIN_ID~END_IDまで行います。

TOMLBind.hpp
// 次のIDを取得
// FIXME constevalがコンパイラ対応しだい修正
template <IsAutoTOMLBindable Type, int Num>
constexpr /* consteval */ int NextTOMLBindId()
{
    if constexpr(Num == Type::TOML_BIND_END_ID::Value) {
        return Type::TOML_BIND_END_ID::Value;
    } else if constexpr (IsTOMLBindCallable<Type, Num + 1>) {
        return Num + 1;
    } else {
        return NextTOMLBindId<Type, Num + 1>();
    }
}

template <class Type, class Id = typename Type::TOML_BIND_BEGIN_ID>
struct AutoTOMLBind {};

template <IsAutoTOMLBindable Type, int Num>
struct AutoTOMLBind<Type, TOMLBindId<Num>>
{
    void operator()([[maybe_unused]]Type& ret, const s3d::TOMLValue& toml)
    {
        if constexpr (IsTOMLBindCallable<Type, Num>) {
            // 割り当て関数があったなら呼ぶ
            ret(TOMLBindId<Num>{toml});
        }
        // 次のId取得
        [[maybe_unused]] constexpr int nextId = NextTOMLBindId<Type, Num>();
        if constexpr (!std::is_same_v<typename Type::TOML_BIND_END_ID, TOMLBindId<nextId>>) {
            // 次のIdがENDでなければ再起的に呼ぶ
            AutoTOMLBind<Type, TOMLBindId<nextId>>{}(ret, toml);
        }
   }
};

最後に忘れちゃいけないのが下準備で作ったTOMLBindの特殊化ですね

TOMLBind.hpp

// 自動割り当てIdがふってあれば
template<IsAutoTOMLBindable Type>
struct TOMLBind<Type>
{
    Type operator()(const s3d::TOMLValue& toml)
    {
        Type ret;
        AutoTOMLBind<Type>{}(ret, toml);
        return ret;
    }
};

変数への代入

これでIdを振ってある関数をすべてTOMLReaderに対して呼べるようになりました。
あとはclassのもつ変数への代入部分をいい感じにします。

まず、tomlはarrayがあったりするのでその辺をよしなにしたり、
TOMLBind定義済みのclassの変数をメンバに持った際に再起的にとれるように
TOMLValueのgetは直接使わず、ラッパー関数を作ります

そのためのconceptを定義します

TOMLBind.hpp
template<class Type>
struct IsSivArray_impl : std::false_type {};

template<class Type, class Allocator>
struct IsSivArray_impl<s3d::Array<Type, Allocator>> : std::true_type {};

// Siv3DのArrayクラスか
template<class Type>
concept IsSivArray = IsSivArray_impl<Type>::value;


// TOMLBind可能か
template<class Type>
concept IsTOMLBindable = requires(const s3d::TOMLValue& toml)
{
    { TOMLBind<Type>{}(toml) }->std::same_as<Type>;
};

↑のconceptオーバーロードしたりしてデータ取得の関数を作ります

TOMLBind.hpp


// データ取得
template <class Value>
Value GetData(const s3d::TOMLValue& toml)
{
    if constexpr (IsTOMLBindable<Value>) {
        // 取得する型がTOMLBind可能であればBindして返す
        return TOMLBind<Value>{}(toml);
    } else {
        return toml.get<Value>();
    }
}

// Arrayの場合
template <IsSivArray Value>
Value GetData(const s3d::TOMLValue& toml)
{
    using Elm = typename Value::value_type;
    Value ret;
    if (toml.getType() == s3d::TOMLValueType::Array) {
        for (const auto& object : toml.arrayView()) {
            ret << GetData<Elm>(object);
        }
    } else if (toml.getType() == s3d::TOMLValueType::TableArray) {
        for (const auto& object : toml.tableArrayView()) {
            ret << GetData<Elm>(object);
        }
    }
    return ret;
}

これで任意の型をTomlValueから変換できるようになったので、あとはKeyを合わせて、この関数を呼ぶだけです。

イメージ
struct Hoge
{
    // ** ここから **
    using TOML_BIND_BEGIN_ID = TOMLBindId<__LINE__>;
    int32 a;
    void operator()(const TOMLBindId<__LINE__>& id) {
        // a = id.toml[U"a"].get<int32>();
        a = GetData<int32>(id.toml[U"a"]);
    }
    // ** ここまで **
    using using TOML_BIND_END_ID = TOMLBindId<__LINE__>;
};

これで実装自体は完了なので、あとはマクロ化してまとめます。

マクロにする

# define TOML_BIND_BEGIN using TOML_BIND_BEGIN_ID = TOMLBindId<__LINE__>
# define TOML_BIND_END using TOML_BIND_END_ID = TOMLBindId<__LINE__>
# define TOML_BIND_PARAM(Value, TOMLKey)\
]] void operator()(const TOMLBindId<__LINE__>& id)\
{\
    using Type = decltype(Value);\
    Value = GetData<Type>(id.toml[U##TOMLKey]);\
}[[

ちなみに、好みで属性ちっくにかけるように無意味な[[]]をつけていますが、不要です。
また、汎用性をもたせるためにValue, Keyと二つの引数にしてますが全く同じでよければ引数は一つですみますね。
(BOOST_PP_OVERLOADのようなことをして1つの時はValueの値でアクセスするとかしてもいいかも)

イメージ
struct Hoge
{
    // ** ここから **
    TOML_BIND_BEGIN;

    [[TOML_BIND_PARAM(a, "a")]]
    int32 a;

    // ** ここまで **
    TOML_BIND_END;
};

サンプル

コンパイル試してないから動かないかも

サンプルコード
Main.cpp
# include <Siv3D.hpp>
# include "TOMLBind.hpp"

// TomlValue to Point
template<>
struct TOMLBind<Point>
{
    Point operator()(const s3d::TOMLValue& toml)
    {
        Point p;
        p.x = toml[U"x"].get<int32>();
        p.y = toml[U"y"].get<int32>();
        return p;
    }
};

struct Item
{
    TOML_BIND_BEGIN;

    [[TOML_BIND_PARAM(label, "label")]]
    String label;

    [[TOML_BIND_PARAM(pos, "pos")]]
    Point pos;

    TOML_BIND_END;
};

struct Config
{
    struct Window
    {
        TOML_BIND_BEGIN;

        [[TOML_BIND_PARAM(title, "title")]]
        String title;

        [[TOML_BIND_PARAM(width, "width")]]
        int32 width;

        [[TOML_BIND_PARAM(height, "height")]]
        int32 height;

        [[TOML_BIND_PARAM(sizable, "sizable")]]
        bool sizable;

        TOML_BIND_END;
    };

    TOML_BIND_BEGIN;

    [[TOML_BIND_PARAM(window, "Window")]]
    Window window;

    [[TOML_BIND_PARAM(sceneBackGround, "Scene.background")]]
    ColorF sceneBackGround;

    [[TOML_BIND_PARAM(values, "Array.values")]]
    Array<int32> values;

    [[TOML_BIND_PARAM(items, "Items")]]
    Array<Item> items;

    TOML_BIND_END;
};
void Main()
{
    // TOML ファイルからデータを読み込む
    const TOMLReader toml(U"example/config/config.toml");

    if (!toml) // もし読み込みに失敗したら
    {
        throw Error(U"Failed to load `config.toml`");
    }
    // Config型にbind
    Config config = TOMLBinding<Config>(toml);

    Window::SetTitle(config.window.title);
    Window::Resize(config.window.width, config.window.height);
    Window::SetStyle(config.window.sizable ? WindowStyle::Sizable : WindowStyle::Fixed);
    Scene::SetBackground(config.sceneBackGround);

    Print << config.values;

    // アイテム描画用のフォント
    const Font font(30, Typeface::Bold);

    while (System::Update())
    {
        // アイテムを描画
        for (const auto& item : config.items)
        {
            const Rect rect(item.pos, 180, 80);

            rect.draw();

            font(item.label).drawAt(rect.center(), ColorF(0.25));
        }
    }
}

実装まとめ

ヘッダーオンリーで、すぐ使えるコード

TOMLBind.hpp
TOMLBind.hpp
# pragma once
# include <concepts>
# include <Siv3D/TOMLReader.hpp>

template<class Type>
struct TOMLBind
{
    // Type operator()(const s3d::TOMLValue& toml);
};

template<class Type>
Type TOMLBinding(const s3d::TOMLValue& toml)
{
    return TOMLBind<Type>{}(toml);
}


/*

 example:

     template<>
     struct TOMLBind<Hoge>
     {
         Hoge operator()(const s3d::TOMLValue& toml){
             // something
             return hoge;
         }
     }
*/

namespace detail
{
    template<int Num>
    struct TOMLBindId
    {
        enum
        {
            Value = Num
        };
        const s3d::TOMLValue& toml;
    };

    template<class Type>
    concept IsTOMLBindable = requires(const s3d::TOMLValue& toml)
    {
        { TOMLBind<Type>{}(toml) }->std::same_as<Type>;
    };

    template<class Type>
    concept IsAutoTOMLBindable = requires(Type a)
    {
        typename Type::TOML_BIND_BEGIN_ID;
        typename Type::TOML_BIND_END_ID;
    };

    template<class Type>
    struct IsSivArray_impl : std::false_type {};

    template<class Type, class Allocator>
    struct IsSivArray_impl<s3d::Array<Type, Allocator>> : std::true_type {};

    template<class Type>
    concept IsSivArray = IsSivArray_impl<Type>::value;

    template<class Type, int Num>
    concept IsTOMLBindCallable = requires(Type a, TOMLBindId<Num> id)
    {
        { a(id) } -> std::same_as<void>;
    };

    // FIXME constevalがコンパイラ対応しだい修正
    template <IsAutoTOMLBindable Type, int Num>
    constexpr /* consteval */ int NextTOMLBindId()
    {
        if constexpr(Num == Type::TOML_BIND_END_ID::Value) {
            return Type::TOML_BIND_END_ID::Value;
        } else if constexpr (IsTOMLBindCallable<Type, Num + 1>) {
            return Num + 1;
        } else {
            return NextTOMLBindId<Type, Num + 1>();
        }
    }

    template <class Type, class Id = typename Type::TOML_BIND_BEGIN_ID>
    struct AutoTOMLBind {};

    template <IsAutoTOMLBindable Type, int Num>
    struct AutoTOMLBind<Type, TOMLBindId<Num>>
    {
        void operator()([[maybe_unused]]Type& ret, const s3d::TOMLValue& toml)
        {
            if constexpr (IsTOMLBindCallable<Type, Num>) {
                ret(TOMLBindId<Num>{toml});
            }
            [[maybe_unused]] constexpr int nextId = NextTOMLBindId<Type, Num>();
            if constexpr (!std::is_same_v<typename Type::TOML_BIND_END_ID, TOMLBindId<nextId>>) {
                AutoTOMLBind<Type, TOMLBindId<nextId>>{}(ret, toml);
            }
        }
    };

    template <class Value>
    Value GetData(const s3d::TOMLValue& toml)
    {
        if constexpr (IsTOMLBindable<Value>) {
            return TOMLBind<Value>{}(toml);
        } else {
            return toml.get<Value>();
        }
    }

    template <IsSivArray Value>
    Value GetData(const s3d::TOMLValue& toml)
    {
        using Elm = typename Value::value_type;
        Value ret;
        if (toml.getType() == s3d::TOMLValueType::Array) {
            for (const auto& object : toml.arrayView()) {
                ret << GetData<Elm>(object);
            }
        } else if (toml.getType() == s3d::TOMLValueType::TableArray) {
            for (const auto& object : toml.tableArrayView()) {
                ret << GetData<Elm>(object);
            }
        }
        return ret;
    }
}

template<::detail::IsAutoTOMLBindable Type>
struct TOMLBind<Type>
{
    Type operator()(const s3d::TOMLValue& toml)
    {
        Type ret;
        ::detail::AutoTOMLBind<Type>{}(ret, toml);
        return ret;
    }
};

# define TOML_BIND_BEGIN using TOML_BIND_BEGIN_ID = ::detail::TOMLBindId<__LINE__>
# define TOML_BIND_END using TOML_BIND_END_ID = ::detail::TOMLBindId<__LINE__>
# define TOML_BIND_PARAM(Value, TOMLKey)\
]] void operator()(const ::detail::TOMLBindId<__LINE__>& id)\
{\
    using Type = decltype(Value);\
    Value = ::detail::GetData<Type>(id.toml[U##TOMLKey]);\
}[[

行数決め打ち版 TOMLBind.hpp

500行以内のソースコードなら対応可能

TOMLBind.hpp
    template<class Type>
    struct TOMLBind
    {
        // Type operator()(const s3d::TOMLValue& toml);
    };

    template<class Type>
    Type TOMLBinding(const s3d::TOMLValue& toml)
    {
        return TOMLBind<Type>{}(toml);
    }
    namespace detail
    {
        inline constexpr int BINDABLE_MAX_LINES = 500;
        template<int Num>
        struct TOMLBindId
        {
            enum
            {
                Value = Num
            };
            const s3d::TOMLValue& toml;
        };

        template<class Type>
        concept IsTOMLBindable = requires(const s3d::TOMLValue& toml)
        {
            { TOMLBind<Type>{}(toml) }->std::same_as<Type>;
        };


        template<class Type>
        struct IsSivArray_impl : std::false_type {};

        template<class Type, class Allocator>
        struct IsSivArray_impl<s3d::Array<Type, Allocator>> : std::true_type {};

        template<class Type>
        concept IsSivArray = IsSivArray_impl<Type>::value;

        template<class Type, int Num>
        concept IsTOMLBindIdCallable = requires(Type a, TOMLBindId<Num> id)
        {
            { a(id) } -> std::same_as<void>;
        };

        // FIXME constevalがコンパイラ対応しだい修正
        template <class Type, int Num>
        constexpr /* consteval */ int NextTOMLBindId()
        {
            if constexpr (Num == BINDABLE_MAX_LINES) {
                return BINDABLE_MAX_LINES;
            } else if constexpr (IsTOMLBindIdCallable<Type, Num + 1>) {
                return Num + 1;
            } else {
                return NextTOMLBindId<Type, Num + 1>();
            }
        }

        template<class Type>
        concept IsAutoTOMLBindable = (NextTOMLBindId<Type, 1>() != BINDABLE_MAX_LINES);

        template <class Type, class Id = TOMLBindId<NextTOMLBindId<Type, 1>()>>
        struct AutoTOMLBind {};

        template <IsAutoTOMLBindable Type, int Num>
        struct AutoTOMLBind<Type, TOMLBindId<Num>>
        {
            void operator()([[maybe_unused]]Type& ret, const s3d::TOMLValue& toml)
            {
                if constexpr (IsTOMLBindIdCallable<Type, Num>) {
                    ret(TOMLBindId<Num>{toml});
                }
                [[maybe_unused]] constexpr int nextId = NextTOMLBindId<Type, Num>();
                if constexpr (nextId != BINDABLE_MAX_LINES) {
                    AutoTOMLBind<Type, TOMLBindId<nextId>>{}(ret, toml);
                }
            }
        };

        template <class Value>
        Value GetData(const s3d::TOMLValue& toml)
        {
            if constexpr (IsTOMLBindable<Value>) {
                return TOMLBind<Value>{}(toml);
            } else {
                return toml.get<Value>();
            }
        }

        template <IsSivArray Value>
        Value GetData(const s3d::TOMLValue& toml)
        {
            using Elm = typename Value::value_type;
            Value ret;
            if (toml.getType() == s3d::TOMLValueType::Array) {
                for (const auto& object : toml.arrayView()) {
                    ret << GetData<Elm>(object);
                }
            } else if (toml.getType() == s3d::TOMLValueType::TableArray) {
                for (const auto& object : toml.tableArrayView()) {
                    ret << GetData<Elm>(object);
                }
            }
            return ret;
        }
    }

    template<detail::IsAutoTOMLBindable Type>
    struct TOMLBind<Type>
    {
        Type operator()(const s3d::TOMLValue& toml)
        {
            Type ret;
            detail::AutoTOMLBind<Type>{}(ret, toml);
            return ret;
        }
    };

# define TOML_BIND_PARAM(Value, TOMLKey)\
]]  void operator()(const detail::TOMLBindId<__LINE__>& id)\
{\
    static_assert(__LINE__ - 2 < detail::BINDABLE_MAX_LINES);\
    using Type = decltype(Value);\
    Value = detail::GetData<Type>(id.toml[U##TOMLKey]);\
}[[

これを使用した場合はBEGINとENDの指定は不要になります。

struct Hoge
{
    [[TOML_BIND_PARAM(a, "a")]]
    int32 a;
};
# まとめ

疲れた

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?