C++ glaze JSONライブラリの紹介と条件-値フィルタリング

# 本文

これは[C++ Advent Calendar 2024](https://qiita.com/advent-calendar/2024/cxx)の18日目の記事です。

前日は[@Hourier](https://qiita.com/Hourier)さんの[リテラルの型エイリアスは止めろ、マジで止めろ](https://qiita.com/Hourier/items/795b4ceb6aa798b858b5)でした。

- [本文](#本文)
  - [glaze JSONライブラリの紹介](#glaze-jsonライブラリの紹介)
    - [特徴](#特徴)
    - [インストール](#インストール)
    - [使用例](#使用例)
    - [`std::variant<Enum, std::string>`への読み込み](#stdvariantenum-stdstringへの読み込み)
  - [条件-値フィルタリング](#条件-値フィルタリング)
    - [前置き](#前置き)
    - [前置き2](#前置き2)
    - [実装例](#実装例)
    - [実装例の感想](#実装例の感想)
  - [結論](#結論)

ここで示したコードは、GCC14.2 -std=c++23で動作確認しました。

## glaze JSONライブラリの紹介

[glaze JSONライブラリ](https://github.com/stephenberry/glaze)は、やや癖があるものの慣れると便利なJSONライブラリです。(筆者所感)

### 特徴

- ヘッダオンリー
- リフレクション
- 構造体 <-> JSON文字列の相互変換
- 名前空間は`glz`
- [その他より詳しい情報はここ](https://github.com/stephenberry/glaze/blob/main/README.md)

### インストール

私の環境ではいつものおまじないでインストールできました。
`N`は適当に書き換えてください。

```bash
mkdir build && cd build && cmake .. && sudo make install -j N
```

### 使用例

`./data.json`を構造体`kcv::data_type`に読み、`kcv::data_type`をJSON文字列に変換し、これを標準出力に出力します。エラーが発生すればその旨を標準出力に出力します。

```json: ./data.json
{
  "key": "value",
  "vector": [1, 2, 3],
  "tuple": ["C++", 23]
}
```

```cpp: ./source/main.cpp
// std
#include <cassert>
#include <cstddef>
#include <filesystem>
#include <fstream>
#include <optional>
#include <print>
#include <string>

// glz
#include <glaze/glaze.hpp>

namespace kcv {

struct data_type {
    std::string key;
    std::vector<int> vector;
    std::tuple<std::string, int> tuple;
    std::optional<int> opt;
};

}  // namespace kcv

int main() {
    // ファイルをbufferに読む
    const auto buffer = []() static -> std::string {
        const auto fname = std::filesystem::path{"./data.json"};
        auto temp        = std::string{};
        temp.resize_and_overwrite(
            std::filesystem::file_size(fname),
            [&fname](char* data, std::size_t size) -> std::size_t {
                std::ifstream{fname}.read(data, size);
                return size;
            }
        );
        return temp;
    }();

    // bufferをdataに読み, dataをJSON文字列に変換して標準出力に出力する.
    // エラーが発生すればその旨を標準出力に出力する.
    auto data = kcv::data_type{};
    if (const auto error = glz::read_json(data, buffer); error) {
        std::println("{}", glz::format_error(error, buffer));
    } else if (const auto result = glz::write_json(data); result.has_value()) {
        std::println("{}", glz::prettify_json(result.value()));
    } else {
        std::println("{}", glz::format_error(result));
    }

    // オプショナルサポートあり
    assert(data.opt == std::nullopt);
}
```

### `std::variant<Enum, std::string>`への読み込み

以下の例のように、なぜか`std::varinat<Enum, std::string>`へ読み込むことができません。

```cpp: ./source/main.cpp
// std
#include <algorithm>
#include <print>
#include <string>
#include <string_view>
#include <utility>
#include <variant>
#include <vector>

// glz
#include <glaze/glaze.hpp>

namespace kcv {

constexpr auto buffer = R"([
    { "value": 1 },
    { "value": "2" }
])";

enum class enum_type : int {};

struct data_type final {
    std::variant<kcv::enum_type, std::string> value;
};

}  // namespace kcv

int main() {
    // bufferをdataに読み, "value"の値を標準出力する.
    // エラーが発生すればその旨を標準出力に出力する.
    auto data = std::vector<kcv::data_type>{};
    if (const auto error = glz::read_json(data, kcv::buffer); error) {
        std::println("{}", glz::format_error(error, kcv::buffer));
    } else {
        std::ranges::for_each(data, [](const auto& e) static -> void {
            struct visitor {
                static void operator()(kcv::enum_type x) {
                    std::println("{}", std::to_underlying(x));
                }
                static void operator()(std::string_view x) {
                    std::println("{}", x);
                }
            };
            std::visit(visitor{}, e.value);
        });
    }
}
```

```: 実行例
2:16: no_matching_variant_type
       { "value": 1 },
                  ^
```

特殊化を定義･追加すると期待通りに動作します:

```cpp: ./source/main.cpp
template <>
struct glz::meta<kcv::data_type> {
    using T = kcv::data_type;

    // clang-format off
    static constexpr auto value = object(
        "value", custom<
                    [](T& dst, const std::variant<std::underlying_type_t<kcv::enum_type>, std::string>& src) /* no static */ -> void {
                        switch (src.index()) {
                            case 0: dst.value = kcv::enum_type{std::get<0>(src)}; return;
                            case 1: dst.value =                std::get<1>(src) ; return;
                            default:
                                std::unreachable();
                        }
                    },
                    &T::value
                >
    );
    // clang-format on
};
```

```: 実行例
1
2
```

## 条件-値フィルタリング

### 前置き

世界的超大人気ブラウザゲーム『艦隊これくしょん -艦これ- 』([推定AU4000万人以上・公式発表](https://x.com/C2_STAFF/status/1854435025531625747))は11周年を迎え、毎年4月23日には周年をインクリメントします。ごく最近のニュースとしては、20のゲームサーバが俺達の課金で強化されました。おっと、震電改も配布されましたよ。
そんな艦これでは、攻略を有利に進めるべく、マスクされた内部データを明かさんとユーザ有志による検証が盛んに行われており、日々、日本語のみならず英語･中国語などで検証報告が行われています。検証項目として「ダメージ式」、「命中率」、「航空戦St.2 AACI撃墜」などが最たる例として挙げられます。その他にも「攻防順序の偏り」、「羅針盤経路探索」など枚挙に暇がありません。
その中でも私はダメージ式の検証を行っています...がこの話は長くなるのでここまで。

<!-- 艦これでは、期間限定海域(イベント)などで、特定の条件を満たした場合に攻撃力が上昇するシステムがあります。「特効」というやつです。ありがちですね。 -->
艦これには「ある艦娘にある装備を搭載している場合、あるボーナスを付与する」装備ボーナスと呼ばれるシステムがあります。
これを実装したいという要求が当然発生するわけですが、どのように実装しましょう?
愚直に`if文`を積み重ねても実装できますが、ユーザ有志が作成する便利ツールは我らが`C++`だけでなく`Rust`や`JavaScript`でも書かれていますから、移植性があると嬉しいです。
そこで、`JSON`の出番となります。
配列の各要素は`OR`、構造体の各要素は`AND`としてド・モルガン的論理演算を考えることで、なんかいい感じにうまいこと、こう、できるらしいです。

C++erとしては実行速度重視となるため、実行時にJSONファイルを読むなんてことはしたくないかもしれませんが、最近私が知って興奮して書きたくなったので紹介します。
この技法の名前、一般的に何と呼ばれているのでしょうかね?

### 前置き2

(公開前追記)

いきなり160行もの実装例を示しても難しいと思うので、簡略化した例でフィルタリングを説明します。

ある整数`x`に対して、条件`condition`を満たしたとき値`value`を`x`に加算し、`x`の値を確認します。
素直に書けば次のようになると思います。

```cpp
#include <print>

int main() {
    int x = 42;

    const bool condition = true;
    const int value      = 123;
    if (condition) {
        x += value;
    }

    std::println("{}", x);
}
```

同じことを構造体`modifier`に`condition`と`value`とをまとめて扱って実装してみます。
とはいえ、不思議はないでしょう。

```cpp
#include <print>

namespace kcv {

struct modifier final {
    bool condition;
    int value;
};

}  // namespace kcv

int main() {
    int x = 42;

    const auto mod = kcv::modifier{.condition = true, .value = 123};
    if (mod.condition) {
        x += mod.value;
    }

    std::println("{}", x);
}
```

次も同様に、`condition`と`value`を`x`に加算します。
ただし、条件･値を複数個にして計算します。
しかしながら、やはり問題ないと思います。

```cpp
#include <print>

int main() {
    int x = 42;

    {
        const bool condition = true;
        const int value      = 123;
        if (condition) {
            x += value;
        }
    }

    {
        const bool condition = false;
        const int value      = 10;
        if (condition) {
            x += value;
        }
    }

    {
        const bool condition = true;
        const int value      = 50;
        if (condition) {
            x += value;
        }
    }

    std::println("{}", x);
}
```

上の複数個の例について、構造体を使って実装してみます。
せっかく構造体に格納するので、さらに配列にまとめます。

`for文`を使いたくなるところをぐっとこらえてナウでヤングな`<ranges>`を使います(`for文`を使うと`continue`まみれになります)。
`condition`を満たすものに限定し、`value`を取り出します。
ここでは`value`の集計のために`std::ranges::fold_left`を使います。

```cpp
#include <algorithm>
#include <array>
#include <functional>
#include <print>
#include <ranges>

namespace kcv {

struct modifier final {
    bool condition;
    int value;
};

}  // namespace kcv

int main() {
    int x = 42;

    const auto modifiers = std::to_array<kcv::modifier>({
        {.condition = true,  .value = 123},
        {.condition = false, .value = 10},
        {.condition = true,  .value = 50},
    });

    x = std::ranges::fold_left(
        modifiers
            | std::ranges::views::filter([](const auto& e) static -> bool { return e.condition; })
            | std::ranges::views::transform([](const auto& e) static -> int { return e.value; }),
        x, std::plus<int>{}
    );

    std::println("{}", x);
}
```

条件･値が増えても、`if文`を高く高く積み重ねることなく、配列に追加するのみで済みます。
条件･値は構造体であり、すなわちJSONで表現できます。

### 実装例

ある艦娘が、`ids`または`types`である装備を搭載していて、かつ、この艦娘が`ship_id`または`ship_type`であるならば、装備ボーナスとして`bonus`の和を得ます。
ただし、実際の運用において、`ids`と`types`の存在性はXORであり、`ship_id`と`ship_type`の存在性もまたXORです。
例えば、(JSON側で)`bonuses`のある要素に、駆逐艦「電」の艦船IDを`ship_id`に指定しておきながら、駆逐艦でない艦種IDを`ship_type`に指定するなんてないですからね。
もし入力されることを想定するならば、匙か例外でも投げてください。

```json: ./fit_bonuses.json
[
  {
    "ids": [1, 3, 5],
    "bonuses": [
      {
        "ship_id": [1, 2, 3],
        "bonus": {
          "houg": 10
        }
      }
    ]
  },
  {
    "types": [30, 60, 90],
    "bonuses": [
      {
        "ship_id": [1],
        "bonus": {
          "rais": 10
        }
      },
      {
        "ship_id": [2, 3],
        "bonus": {
          "tais": 30
        }
      },
      {
        "ship_type": [300],
        "bonus": {
          "houg": 5
        }
      }
    ]
  }
]
```

```cpp
// std
#include <algorithm>
#include <cstddef>
#include <filesystem>
#include <fstream>
#include <optional>
#include <print>
#include <ranges>
#include <utility>
#include <vector>

// glz
#include <glaze/glaze.hpp>

namespace kcv {

struct equipment final {
    int id;
    int type;
};

struct ship final {
    int id;
    int type;
    std::vector<equipment> equipments;
};

struct bonus_value final {
    int houg;  // 火力
    int rais;  // 雷装
    int tais;  // 対潜
};

struct bonus_data final {
    std::optional<std::vector<int>> ship_id;
    std::optional<std::vector<int>> ship_type;
    bonus_value bonus;
};

struct equipment_cond final {
    std::optional<std::vector<int>> ids;
    std::optional<std::vector<int>> types;
    std::vector<bonus_data> bonuses;
};

bool has_fit_equipment(const equipment_cond& fit_bonus, const ship& ship) {
    const auto has_fit_equipment_impl = [&ship](const std::vector<int>& cond, const int equipment::*proj) {
        auto view = ship.equipments  //
                  | std::ranges::views::filter([&cond, &proj](const auto& e) -> bool {
                        return std::ranges::contains(cond, e.*proj);
                    });
        // not .empty(); の代用. std::ranges::input_rangeのため.
        return view.begin() != view.end();
    };

    const auto& [ids, types, bonuses] = fit_bonus;
    if (ids.has_value() xor types.has_value()) [[likely]] {
        if (ids.has_value()) {
            return has_fit_equipment_impl(*ids, &equipment::id);
        }
        if (types.has_value()) {
            return has_fit_equipment_impl(*types, &equipment::type);
        }

        std::unreachable();
    } else {
        // 運用上ここには到達しない
        return false;
    }
}

bool matches_ship(const bonus_data& data, const ship& ship) {
    if (const auto& e = data.ship_id; e.has_value() and not std::ranges::contains(*e, ship.id)) {
        return false;
    }
    if (const auto& e = data.ship_type; e.has_value() and not std::ranges::contains(*e, ship.type)) {
        return false;
    }
    return true;
}

auto extract_bonuses(const std::vector<equipment_cond>& fit_bonuses, const ship& ship) -> std::vector<bonus_value> {
    return fit_bonuses
         | std::ranges::views::filter([&ship](const auto& e) -> bool { return has_fit_equipment(e, ship); })
         | std::ranges::views::transform([](const auto& e) -> const std::vector<bonus_data>& { return e.bonuses; })
         | std::ranges::views::join
         | std::ranges::views::filter([&ship](const auto& e) -> bool { return matches_ship(e, ship); })
         | std::ranges::views::transform([](const auto& e) -> const bonus_value& { return e.bonus; })
         | std::ranges::to<std::vector>();
}

void read_json(auto& dst, const std::string& buffer) {
    if (const auto error = glz::read_json(dst, buffer); error) {
        std::println("{}", glz::format_error(error, buffer));
    }
}

void read_json(auto& dst, const std::filesystem::path& fname) {
    auto buffer = std::string{};
    buffer.resize_and_overwrite(
        std::filesystem::file_size(fname),
        [&fname](char* data, std::size_t size) -> std::size_t {
            std::ifstream{fname}.read(data, size);
            return size;
        }
    );
    kcv::read_json(dst, buffer);
}

}  // namespace kcv

int main() {
    // これらの艦娘がどのような装備ボーナスを得るか, これを計算する.
    const auto ships = std::vector<kcv::ship>{
        kcv::ship{
            .id = 1,
            .type = 100,
            .equipments = std::vector{
                kcv::equipment{.id = 1, .type = 10},
                kcv::equipment{.id = 2, .type = 20},
                kcv::equipment{.id = 3, .type = 30},
            }
        },
        kcv::ship{
            .id = 2,
            .type = 200,
            .equipments = std::vector{
                kcv::equipment{.id = 4, .type = 40},
                kcv::equipment{.id = 5, .type = 50},
                kcv::equipment{.id = 6, .type = 60},
            }
        },
        kcv::ship{
            .id = 3,
            .type = 300,
            .equipments = std::vector{
                kcv::equipment{.id = 7, .type = 70},
                kcv::equipment{.id = 8, .type = 80},
                kcv::equipment{.id = 9, .type = 90},
            }
        },
    };

    // ファイル読み込み
    const auto fit_bonuses = []() -> std::vector<kcv::equipment_cond> {
        auto temp = std::vector<kcv::equipment_cond>{};
        kcv::read_json(temp, std::filesystem::path{"./fit_bonuses.json"});
        return temp;
    }();

    // 装備ボーナスの計算と標準出力への出力
    std::ranges::for_each(ships, [&fit_bonuses](const auto& ship) {
        const auto bonuses = kcv::extract_bonuses(fit_bonuses, ship);
        const auto bonus   = std::ranges::fold_left(bonuses, kcv::bonus_value{}, [](auto acc, const auto& e) {
            acc.houg += e.houg;
            acc.rais += e.rais;
            acc.tais += e.tais;
            return acc;
        });
        std::println("ship_id: {}, houg: {:2}, rais: {:2}, tais: {:2}", ship.id, bonus.houg, bonus.rais, bonus.tais);
    });
}
```

```: 実行例
ship_id: 1, houg: 10, rais: 10, tais:  0
ship_id: 2, houg: 10, rais:  0, tais: 30
ship_id: 3, houg:  5, rais:  0, tais: 30
```

### 実装例の感想

C++コードが160行くらいあってちょっと長いですが、コピペで動作すると思うので、気になったら動作確認して遊んでみてください。

実際の装備ボーナスの条件はもっと複雑で、装備の個数条件、装備のレベル条件などがありますが、長くなりすぎるため省きました。

述語関数と`<ranges>`を主軸に書けたので気持ちがいい。`std::ranges::views::cache_last`待ってる。

## 結論

- glaze JSONライブラリ便利!
  - [https://github.com/stephenberry/glaze](https://github.com/stephenberry/glaze)
- JSON便利!
- std::json待ってる!!
- 艦これしよう!!!
  - [https://www.dmm.com/netgame/feature/kancolle.html](https://www.dmm.com/netgame/feature/kancolle.html)