本文
これはC++ Advent Calendar 2024の18日目の記事です。
前日は@Hourierさんのリテラルの型エイリアスは止めろ、マジで止めろでした。
ここで示したコードは、GCC14.2 -std=c++23で動作確認しました。
glaze JSONライブラリの紹介
glaze JSONライブラリは、やや癖があるものの慣れると便利なJSONライブラリです。(筆者所感)
特徴
- ヘッダオンリー
- リフレクション
- 構造体 <-> JSON文字列の相互変換
- 名前空間は
glz
- その他より詳しい情報はここ
インストール
私の環境ではいつものおまじないでインストールできました。
N
は適当に書き換えてください。
mkdir build && cd build && cmake .. && sudo make install -j N
使用例
./data.json
を構造体kcv::data_type
に読み、kcv::data_type
をJSON文字列に変換し、これを標準出力に出力します。エラーが発生すればその旨を標準出力に出力します。
{
"key": "value",
"vector": [1, 2, 3],
"tuple": ["C++", 23]
}
// 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>
へ読み込むことができません。
// 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 },
^
特殊化を定義・追加すると期待通りに動作します:
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万人以上・公式発表)は11周年を迎え、毎年4月23日には周年をインクリメントします。ごく最近のニュースとしては、20のゲームサーバが俺達の課金で強化されました。おっと、震電改も配布されましたよ。
そんな艦これでは、攻略を有利に進めるべく、マスクされた内部データを明かさんとユーザ有志による検証が盛んに行われており、日々、日本語のみならず英語・中国語などで検証報告が行われています。検証項目として「ダメージ式」、「命中率」、「航空戦St.2 AACI撃墜」などが最たる例として挙げられます。その他にも「攻防順序の偏り」、「羅針盤経路探索」など枚挙に暇がありません。
その中でも私はダメージ式の検証を行っています...がこの話は長くなるのでここまで。
艦これには「ある艦娘にある装備を搭載している場合、あるボーナスを付与する」装備ボーナスと呼ばれるシステムがあります。
これを実装したいという要求が当然発生するわけですが、どのように実装しましょう?
愚直にif文
を積み重ねても実装できますが、ユーザ有志が作成する便利ツールは我らがC++
だけでなくRust
やJavaScript
でも書かれていますから、移植性があると嬉しいです。
そこで、JSON
の出番となります。
配列の各要素はOR
、構造体の各要素はAND
としてド・モルガン的論理演算を考えることで、なんかいい感じにうまいこと、こう、できるらしいです。
C++erとしては実行速度重視となるため、実行時にJSONファイルを読むなんてことはしたくないかもしれませんが、最近私が知って興奮して書きたくなったので紹介します。
この技法の名前、一般的に何と呼ばれているのでしょうかね?
前置き2
(公開前追記)
いきなり160行もの実装例を示しても難しいと思うので、簡略化した例でフィルタリングを説明します。
ある整数x
に対して、条件condition
を満たしたとき値value
をx
に加算し、x
の値を確認します。
素直に書けば次のようになると思います。
#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
とをまとめて扱って実装してみます。
とはいえ、不思議はないでしょう。
#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
に加算します。
ただし、条件・値を複数個にして計算します。
しかしながら、やはり問題ないと思います。
#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
を使います。
#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
に指定するなんてないですからね。
もし入力されることを想定するならば、匙か例外でも投げてください。
[
{
"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
}
}
]
}
]
// 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ライブラリ便利!
- JSON便利!
- std::json待ってる!!
- 艦これしよう!!!