C++20 CSV iteratorを実装する

はじめに

これはCSV Parserを実装するわけではありません。 vincentlaucsb/csv-parserに乗っかって、iteratorを実装します。

国民的人気ブラウザゲーム艦隊これくしょん-艦これ-では、ユーザ有志による検証が非常に盛んに行われています。 その中でも、ダメージ検証は特に活発であり、専用のデータ採取ツール(赤仮、74式EN)や、データ解析用計算ツール(ダメージ検証用スプレ)、異常ダメージ検知などの様々なツールが開発・保守・運用されています。

艦娘は、最大でconstexpr auto slot_size = 6;個のスロットがあります。

艦娘の6個のslot

各スロットには装備を載せることができ、各装備は、少なくとも以下の要素を持ちます。

  • 名前
  • 改修値
  • 熟練度
  • 搭載数
  • 戦闘後搭載数

これらの要素は、前述のデータ採取ツールによって、戦闘結果として単位攻防ごとにCSVデータの形式で得られます。 このCSVデータの列名は以下のようになっています。列数は150です。

No.,日付,海域,マス,出撃,ランク,敵艦隊,提督レベル,自陣形,敵陣形,自索敵,敵索敵,制空権,会敵,自触接,敵触接,自照明弾,敵照明弾,戦闘種別,艦名1,艦名2,艦名3,艦名4,艦名5,艦名6,自艦隊,巡目,攻撃艦,砲撃種別,砲撃回数,表示装備1,表示装備2,表示装備3,クリティカル,ダメージ,かばう,攻撃艦.編成順,攻撃艦.ID,攻撃艦.名前,攻撃艦.種別,攻撃艦.疲労,攻撃艦.戦闘後疲労,攻撃艦.残耐久,攻撃艦.最大耐久,攻撃艦.損傷,攻撃艦.残燃料,攻撃艦.戦闘後残燃料,攻撃艦.最大燃料,攻撃艦.残弾薬,攻撃艦.戦闘後残弾薬,攻撃艦.最大弾薬,攻撃艦.Lv,攻撃艦.速力,攻撃艦.火力,攻撃艦.雷装,攻撃艦.対空,攻撃艦.装甲,攻撃艦.回避,攻撃艦.対潜,攻撃艦.索敵,攻撃艦.運,攻撃艦.射程,攻撃艦.装備1.名前,攻撃艦.装備1.改修,攻撃艦.装備1.熟練度,攻撃艦.装備1.搭載数,攻撃艦.装備1.戦闘後搭載数,攻撃艦.装備2.名前,攻撃艦.装備2.改修,攻撃艦.装備2.熟練度,攻撃艦.装備2.搭載数,攻撃艦.装備2.戦闘後搭載数,攻撃艦.装備3.名前,攻撃艦.装備3.改修,攻撃艦.装備3.熟練度,攻撃艦.装備3.搭載数,攻撃艦.装備3.戦闘後搭載数,攻撃艦.装備4.名前,攻撃艦.装備4.改修,攻撃艦.装備4.熟練度,攻撃艦.装備4.搭載数,攻撃艦.装備4.戦闘後搭載数,攻撃艦.装備5.名前,攻撃艦.装備5.改修,攻撃艦.装備5.熟練度,攻撃艦.装備5.搭載数,攻撃艦.装備5.戦闘後搭載数,攻撃艦.装備6.名前,攻撃艦.装備6.改修,攻撃艦.装備6.熟練度,攻撃艦.装備6.搭載数,攻撃艦.装備6.戦闘後搭載数,防御艦.編成順,防御艦.ID,防御艦.名前,防御艦.種別,防御艦.疲労,防御艦.戦闘後疲労,防御艦.残耐久,防御艦.最大耐久,防御艦.損傷,防御艦.残燃料,防御艦.戦闘後残燃料,防御艦.最大燃料,防御艦.残弾薬,防御艦.戦闘後残弾薬,防御艦.最大弾薬,防御艦.Lv,防御艦.速力,防御艦.火力,防御艦.雷装,防御艦.対空,防御艦.装甲,防御艦.回避,防御艦.対潜,防御艦.索敵,防御艦.運,防御艦.射程,防御艦.装備1.名前,防御艦.装備1.改修,防御艦.装備1.熟練度,防御艦.装備1.搭載数,防御艦.装備1.戦闘後搭載数,防御艦.装備2.名前,防御艦.装備2.改修,防御艦.装備2.熟練度,防御艦.装備2.搭載数,防御艦.装備2.戦闘後搭載数,防御艦.装備3.名前,防御艦.装備3.改修,防御艦.装備3.熟練度,防御艦.装備3.搭載数,防御艦.装備3.戦闘後搭載数,防御艦.装備4.名前,防御艦.装備4.改修,防御艦.装備4.熟練度,防御艦.装備4.搭載数,防御艦.装備4.戦闘後搭載数,防御艦.装備5.名前,防御艦.装備5.改修,防御艦.装備5.熟練度,防御艦.装備5.搭載数,防御艦.装備5.戦闘後搭載数,防御艦.装備6.名前,防御艦.装備6.改修,防御艦.装備6.熟練度,防御艦.装備6.搭載数,防御艦.装備6.戦闘後搭載数,艦隊種類,敵艦隊種類

ここで本題です。 このCSVデータについて、攻撃艦の装備の名前1-6をイテレートしたいです。 おっと、防御艦の装備の改修値1-6もイテレートしたいです。

はて、どうしたものか。 まず、Data Oriented的な観点でいうと、装備の各要素

  • 名前
  • 改修値
  • 熟練度
  • 搭載数
  • 戦闘後搭載数

をメンバとする構造体の配列として扱うのは不適でしょう。 第1スロットの装備の名前 -> 第2スロットの装備の名前 -> ...とイテレートしたいのです。 AoSではなく、SoAライクな設計にします。

コード

今回利用するvincentlaucsb/csv-parserは、row[index]row[key]のようにフィールドにアクセスできます。 ただし、indexintで、keyconst std::string&です。

フィールドへのアクセスは、ここでは、可読性のために整数ではなく文字列を使うことにします。 keyとなる文字列は、直積(?) {"攻撃艦", "防御艦"} * {1, 2, 3, 4, 5, 6} * {"名前", "改修値", "熟練度", "搭載数", "戦闘後搭載数"}だけ必要です。 keyconst char*ではなく、const std::string&ですから、std::formatfmt::formatを利用して、静的実行時定数としてコンストラクトします。 くわえて、スロット1-6でイテレートできるように、std::arrayを使います。 すなわち、static const std::array<std::string, slot_size> keys;を定義します。

型にキーとなる文字列("攻撃艦"や"名前")を結び付ければ、templateを利用して、型ごとにkeysを上手に定義できます。 ということで、タグ型のようなものを定義します:

攻撃艦と防御艦のタグ型

#include <string_view>

namespace kcv {

struct attacker {
    static constexpr auto key = std::string_view{"攻撃艦"};
};

struct defender {
    static constexpr auto key = std::string_view{"防御艦"};
};

}  // namespace kcv

装備の各要素の型

#include <string_view>

namespace kcv {
namespace slotitem {

class name {
   public:
    static constexpr auto key = std::string_view{"名前"};

    // fmtライブラリのために
    friend constexpr auto format_as(const name& name) noexcept -> std::string_view { return name.value_; }

    constexpr name(std::string_view name) noexcept 
        : value_{name} {}

   private:
    std::string_view value_;
};

class improvement;
class proficiency;
// ...

}  // namespace slotitem
}  // namespace kcv

イテレータ

ここまで来れば、あとは典型的なiteratorの実装です。 とりあえずstd::input_iteratorを満たすようにします。 std::random_access_iteratorも満たすことができますが、記事が長くなるのでね。 ちなみに、std::filesystem::directory_iteratorを参考にしました。

#include <fmt/format.h>

#include <array>
#include <cstddef>
#include <string>
#include <string_view>
#include <utility>

#include "vincentlaucsb/csv.hpp"

namespace kcv {

inline constexpr auto slot_size = std::size_t{6};

template <typename S, typename T>
class slot_iterator final {
   public:
    using ship_type  = S;
    using value_type = T;

    using difference_type  = std::ptrdiff_t;
    using iterator_concept = std::input_iterator_tag;

    friend constexpr auto begin(const slot_iterator itr) noexcept -> slot_iterator {
        return itr;
    }

    friend constexpr auto end(const slot_iterator) noexcept -> slot_iterator {
        return slot_iterator{};
    }

    friend constexpr bool operator==(const slot_iterator &lhs, const slot_iterator &rhs) noexcept {
        return lhs.i_ == rhs.i_;
    }

    friend constexpr bool operator!=(const slot_iterator &lhs, const slot_iterator &rhs) noexcept {
        return lhs.i_ != rhs.i_;
    }

    friend constexpr auto operator++(slot_iterator &itr) noexcept -> slot_iterator & {
        ++itr.i_;
        return itr;
    }

    friend constexpr auto operator++(slot_iterator &itr, int) noexcept -> slot_iterator {
        auto self = itr;
        itr.i_++;
        return self;
    }

    friend auto operator*(const slot_iterator &itr) -> value_type {
        const auto &key   = keys[itr.i_];
        const auto &row   = *itr.row_;
        const auto &field = row[key];
        return value_type{field.get_sv()};
    }

    // end
    constexpr slot_iterator() noexcept
        : row_{nullptr}
        , i_{slot_size} {}

    // begin
    constexpr slot_iterator(const csv::CSVRow &row) noexcept
        : row_{std::addressof(row)}
        , i_{0} {}

   private:
    static inline const auto keys = []<std::size_t... i>(std::index_sequence<i...>) {
        return std::array<std::string, sizeof...(i)>{
            fmt::format("{}.装備{}.{}", ship_type::key, 1 + i, value_type::key)...
        };
    }(std::make_index_sequence<slot_size>{});

   private:
    const csv::CSVRow *row_;
    std::size_t i_;
};

}  // namespace kcv

使用例.cpp

サンプルデータ:

赤仮砲撃戦.csv

No.,日付,海域,マス,出撃,ランク,敵艦隊,提督レベル,自陣形,敵陣形,自索敵,敵索敵,制空権,会敵,自触接,敵触接,自照明弾,敵照明弾,戦闘種別,艦名1,艦名2,艦名3,艦名4,艦名5,艦名6,自艦隊,巡目,攻撃艦,砲撃種別,砲撃回数,表示装備1,表示装備2,表示装備3,クリティカル,ダメージ,かばう,攻撃艦.編成順,攻撃艦.ID,攻撃艦.名前,攻撃艦.種別,攻撃艦.疲労,攻撃艦.戦闘後疲労,攻撃艦.残耐久,攻撃艦.最大耐久,攻撃艦.損傷,攻撃艦.残燃料,攻撃艦.戦闘後残燃料,攻撃艦.最大燃料,攻撃艦.残弾薬,攻撃艦.戦闘後残弾薬,攻撃艦.最大弾薬,攻撃艦.Lv,攻撃艦.速力,攻撃艦.火力,攻撃艦.雷装,攻撃艦.対空,攻撃艦.装甲,攻撃艦.回避,攻撃艦.対潜,攻撃艦.索敵,攻撃艦.運,攻撃艦.射程,攻撃艦.装備1.名前,攻撃艦.装備1.改修,攻撃艦.装備1.熟練度,攻撃艦.装備1.搭載数,攻撃艦.装備1.戦闘後搭載数,攻撃艦.装備2.名前,攻撃艦.装備2.改修,攻撃艦.装備2.熟練度,攻撃艦.装備2.搭載数,攻撃艦.装備2.戦闘後搭載数,攻撃艦.装備3.名前,攻撃艦.装備3.改修,攻撃艦.装備3.熟練度,攻撃艦.装備3.搭載数,攻撃艦.装備3.戦闘後搭載数,攻撃艦.装備4.名前,攻撃艦.装備4.改修,攻撃艦.装備4.熟練度,攻撃艦.装備4.搭載数,攻撃艦.装備4.戦闘後搭載数,攻撃艦.装備5.名前,攻撃艦.装備5.改修,攻撃艦.装備5.熟練度,攻撃艦.装備5.搭載数,攻撃艦.装備5.戦闘後搭載数,攻撃艦.装備6.名前,攻撃艦.装備6.改修,攻撃艦.装備6.熟練度,攻撃艦.装備6.搭載数,攻撃艦.装備6.戦闘後搭載数,防御艦.編成順,防御艦.ID,防御艦.名前,防御艦.種別,防御艦.疲労,防御艦.戦闘後疲労,防御艦.残耐久,防御艦.最大耐久,防御艦.損傷,防御艦.残燃料,防御艦.戦闘後残燃料,防御艦.最大燃料,防御艦.残弾薬,防御艦.戦闘後残弾薬,防御艦.最大弾薬,防御艦.Lv,防御艦.速力,防御艦.火力,防御艦.雷装,防御艦.対空,防御艦.装甲,防御艦.回避,防御艦.対潜,防御艦.索敵,防御艦.運,防御艦.射程,防御艦.装備1.名前,防御艦.装備1.改修,防御艦.装備1.熟練度,防御艦.装備1.搭載数,防御艦.装備1.戦闘後搭載数,防御艦.装備2.名前,防御艦.装備2.改修,防御艦.装備2.熟練度,防御艦.装備2.搭載数,防御艦.装備2.戦闘後搭載数,防御艦.装備3.名前,防御艦.装備3.改修,防御艦.装備3.熟練度,防御艦.装備3.搭載数,防御艦.装備3.戦闘後搭載数,防御艦.装備4.名前,防御艦.装備4.改修,防御艦.装備4.熟練度,防御艦.装備4.搭載数,防御艦.装備4.戦闘後搭載数,防御艦.装備5.名前,防御艦.装備5.改修,防御艦.装備5.熟練度,防御艦.装備5.搭載数,防御艦.装備5.戦闘後搭載数,防御艦.装備6.名前,防御艦.装備6.改修,防御艦.装備6.熟練度,防御艦.装備6.搭載数,防御艦.装備6.戦闘後搭載数,艦隊種類,敵艦隊種類
1,2023/03/10 20:40:07,ブルネイ泊地沖,マップ:7-1 セル:4,出撃,勝利S,深海潜水艦隊 II群,120,単横陣,梯形陣,発見!(索敵機なし),発見!(索敵機なし),制空権確保,同航戦,,,,,砲撃戦,丹陽,Верный,夕張改二特,時雨改二,早潮改二,,通常艦隊,先制対潜,自軍,0,0,二式爆雷改二,,,2,237,0,4,145,時雨改二,駆逐艦,52,63,36,36,小破未満,15,14,15,20,20,20,137,高速,63,89,72,55,120,143,62,55,短,三式水中探信儀改,10,0,0,0,四式水中聴音機,6,0,0,0,二式爆雷改二,5,0,0,0,,,,,,,,,,,応急修理要員,0,0,,,3,1530,潜水カ級,潜水艦,,,19,19,小破未満,,,0,,,0,50,低速,0,46,0,7,1,0,6,1,短,21inch魚雷前期型,,,,,21inch魚雷前期型,,,,,,,,,,,,,,,,,,,,,,,,,通常艦隊,通常艦隊
2,2023/03/10 20:40:07,ブルネイ泊地沖,マップ:7-1 セル:4,出撃,勝利S,深海潜水艦隊 II群,120,単横陣,梯形陣,発見!(索敵機なし),発見!(索敵機なし),制空権確保,同航戦,,,,,砲撃戦,丹陽,Верный,夕張改二特,時雨改二,早潮改二,,通常艦隊,先制対潜,自軍,0,0,,,,1,62,0,3,623,夕張改二特,軽巡洋艦,49,50,44,47,小破未満,30,28,30,40,40,40,161,低速,57,130,87,67,112,109,75,35,短,甲標的 丙型,5,0,0,0,試製61cm六連装(酸素)魚雷,9,0,0,0,試製61cm六連装(酸素)魚雷,9,0,0,0,四式水中聴音機,6,0,0,0,2cm 四連装FlaK 38,8,0,0,0,応急修理要員,0,0,,,2,1530,潜水カ級,潜水艦,,,19,19,小破未満,,,0,,,0,50,低速,0,46,0,7,1,0,6,1,短,21inch魚雷前期型,,,,,21inch魚雷前期型,,,,,,,,,,,,,,,,,,,,,,,,,通常艦隊,通常艦隊
3,2023/03/10 20:40:07,ブルネイ泊地沖,マップ:7-1 セル:4,出撃,勝利S,深海潜水艦隊 II群,120,単横陣,梯形陣,発見!(索敵機なし),発見!(索敵機なし),制空権確保,同航戦,,,,,砲撃戦,丹陽,Верный,夕張改二特,時雨改二,早潮改二,,通常艦隊,先制対潜,自軍,0,0,Hedgehog(初期型),,,1,146,0,1,651,丹陽,駆逐艦,49,53,40,40,小破未満,15,14,15,20,20,20,139,高速,68,56,88,61,110,116,64,63,短,四式水中聴音機,6,0,0,0,四式水中聴音機,6,0,0,0,Hedgehog(初期型),2,0,0,0,,,,,,,,,,,応急修理要員,0,0,,,4,1530,潜水カ級,潜水艦,,,19,19,小破未満,,,0,,,0,50,低速,0,46,0,7,1,0,6,1,短,21inch魚雷前期型,,,,,21inch魚雷前期型,,,,,,,,,,,,,,,,,,,,,,,,,通常艦隊,通常艦隊
#include <fmt/core.h>

#include <concepts>

#include "vincentlaucsb/csv.hpp"

// #include "slot_iterator.hpp"
// #include "slotitem/name.hpp"

using slot_iterator = kcv::slot_iterator<kcv::attacker, kcv::slotitem::name>;
static_assert(std::input_iterator<slot_iterator>);
static_assert(std::ranges::range<slot_iterator>);

auto main() -> int {
    for (auto reader = csv::CSVReader{"赤仮砲撃戦.csv"}; const auto& row : reader) {
        for (const auto& e : slot_iterator{row}) {
            fmt::print("{}, ", e);
        }
        fmt::println("");
    }
    return 0;
}