C++20 些末事 標準入力で入力した数値のうち、最大値と最小値を標準出力に出力する

コード

// 0個以上の数値を入力して、入力された最大値と最小値を表示する。

#include <cmath>
#include <iostream>
#include <limits>
#include <ranges>

auto main() -> int {
    using value_type = int;

    auto min = std::numeric_limits<value_type>::max();
    auto max = std::numeric_limits<value_type>::min();

    // 数値以外を入力すると反復が終了する。
    for (auto n : std::ranges::views::istream<value_type>(std::cin)) {
        min = std::min(n, min);
        max = std::max(n, max);
    }

    std::cout << "min: " << min << std::endl;
    std::cout << "max: " << max << std::endl;
}

実行例

1
2
3
-1
10
-3
q
min: -3
max: 10
3 1 4 1 5 q
min: 1
max: 5

C++20 enumで日本語を使いたい | consteval関数でassertを使う

はじめに

プログラミング言語C++ではenumが使えます。 しかしenumは日本語文字列が使えません。

enum class hoge { "青", "黄", "赤" };  // NG

日本語で書くにはどうするか。 ということで、以下のようにしてみました。

#include <algorithm>
#include <array>
#include <cassert>
#include <cstddef>
#include <string_view>
#include <utility>

class master {
    using value_type = std::pair<std::string_view, int>;

    // 装備名とIDのマスタデータ
    // このマスタデータは、実行時に持ち込まない
    static constexpr auto mst = std::array<value_type, 6>{{
        {"10.5cm連装砲", 160},
        {"10cm連装高角砲", 3},
        {"10cm連装高角砲(砲架)", 71},
        {"10cm連装高角砲+高射装置", 122},
        {"10cm連装高角砲改+増設機銃", 275},
        {"10cm連装高角砲群 集中配備", 464},
        // ...
    }};

    // マスタデータは二分探索できるようにソート済み
    static_assert(std::ranges::is_sorted(mst, {}, &value_type::first));

   public:

    // 装備名をキーにIDを得る
    static consteval int id(std::string_view name) {
        const auto itr = std::ranges::lower_bound(mst, name, {}, &value_type::first);
        
        // これらの表明は、constevalの実行時すなわちコンパイル時に行われる
        assert(itr != mst.end());
        assert(itr->first == name);
        
        // 引数の装備名がマスタに存在する場合にのみIDを返す
        // 存在しなければ上記assertによってコンパイルエラー
        return itr->second;
    }
};

namespace literals {

consteval int operator""_id(const char* str, std::size_t size) {
    return master::id(std::string_view{str, size});
}

}  // namespace literals

int main() {  //
    static_assert(master::id("10.5cm連装砲") == 160);

    {
        using literals::operator""_id;
        static_assert("10cm連装高角砲群 集中配備"_id == 464);
    }

    {
        using literals::operator""_id;
        // static_assert("100km連装砲"_id == 100);
    }
}

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;
}

C++14以上 変数テンプレート

変数をテンプレートで書ける。 ありがちな使用例では、数値をそれぞれの型に、単純なキャストで変換するものが挙げられている。

template <class T>
constexpr T pi = static_cast<T>(3.14159265358979323846);

単純なキャスト以外で変数テンプレートを使うにはどうすればよいか。 lambda式とconstexpr if文の組み合わせ以外思いつかない。

#include <concepts>
#include <string>
#include <cassert>

template <typename T>
T v = []() {
    if constexpr (std::is_arithmetic_v<T>) {
        return 42;
    } else if constexpr(std::same_as<T, std::string>) {
        return "42";
    }
}();

int main() {
    auto i = v<int>;
    assert(i == 42);
    
    auto s = v<std::string>;
    assert(s == "42");
}

うーん、関数でよくない?

C++ JSONライブラリ「nlohmann/json」の使い方

本稿のコード例はC++20で示しています。

概要

nlohmann/json

N・ローマンさんのJSONライブラリ(C++11)

single headerが提供されています。"single"と言っても前方宣言用のヘッダファイルjson_fwd.hppもあります。

特徴

  • UIが優れている
  • 速度とメモリはあまりこだわっていない
    • 悪いわけではない

すっごい便利です。 あまりにも便利すぎるので神クラスを作りかねない

コンパイル/ビルド

ディレクトリ例:

workspace/
  |--include/
  |   |--nlohmann/
  |        |--json_fwd.hpp
  |        |--json.hpp
  |
  |--source/
       |--main.cpp

main.cpp:

#include <nlohmann/json_fwd.hpp>  // なくても動く
#include <nlohmann/json.hpp>

int main(){}

コンパイル(GCC)
-I Path/To/Fileでヘッダファイルへのパスを指定する。

{workspace}$ g++ source/main.cpp -o main -I ./include

データ格納

#include <nlohmann/json.hpp>

#include <fstream>

using json = nlohmann::json;  // 推奨されているエイリアス

int main() {
    const json j1 = {"key", "value"};

    const json j2 = {"array", {3, 1, 4}};

    const json j3 = R"({"key":"value"})"_json;

    const json j4 = json::parse(R"({"key":"value"})");

    const json j5 = json::parse(std::ifstream("example.json"));

    const json j6 = 42;

    const json j7 = "string";
}

文字列からの構築はママだが、オブジェクトからの構築は少しややこしい。

次の2つを念頭に置くと理解しやすいかも。

const std::vector<int> a(3);  // [0, 0, 0]
const std::vector<int> b{3};  // [3]
// C++11 初期化子リスト
// https://cpprefjp.github.io/lang/cpp11/initializer_lists.html
const std::vector<json> v1 = {{json{1}, json{2}, json{3}}};

// C++14 ネストする集成体初期化における波カッコ省略を許可
// https://cpprefjp.github.io/lang/cpp14/brace_elision_in_array_temporary_initialization.html
const std::vector<json> v2 = {json{1}, json{2}, json{3}};

// C++17 クラステンプレートのテンプレート引数推論
// https://cpprefjp.github.io/lang/cpp17/type_deduction_for_class_templates.html
const std::vector v3 = {json{1}, json{2}, json{3}};

// 暗黙の型変換 int -> json
const std::vector<json> v4 = {1, 2, 3};

以上を踏まえてこうなる:

#include <nlohmann/json.hpp>

#include <iostream>

using json = nlohmann::json;

int main() {
    
    // ["key", 1]
    const json j1 = {"key", 1};
    std::cout << j1 << std::endl;

    // ["key", 2]
    const json j2{"key", 2};
    std::cout << j2 << std::endl;

    // {"key":3}
    const json j3 = {{"key", 3}};
    std::cout << j3 << std::endl;

    // ["key",4]
    const json j4 = std::pair<const char*, int>{"key", 4};
    std::cout << j4 << std::endl;

    // {"key":5}
    const json j5{std::pair<const char*, int>{"key", 5}};
    std::cout << j5 << std::endl;

    // {"key1":6,"key2":6}
    const json j6 = {{"key1", 6}, {"key2", 6}};
    std::cout << j6 << std::endl;

    // [{"key1":7,"key2":7}]
    const json j7 = {{{"key1", 7}, {"key2", 7}}};
    std::cout << j7 << std::endl;

    return 0;
}
#include <nlohmann/json.hpp>

#include <iostream>

using json = nlohmann::json;

void show(const json &x, int i = -1) { std::cout << x.dump(i) << std::endl; }

int main() {
    
    const json message = R"(
        {
            "id": 42,
            "text": "Hello"
        }
    )"_json;
    

    // [
    //   "message",
    //   {
    //     "id": 42,
    //     "text": "Hello"
    //   }
    // ]
    const json j1 = {"message", message};
    show(j1, 2);

    // {
    //   "message": {
    //     "id": 42,
    //     "text": "Hello"
    //   }
    // }
    const json j2 = {{"message", message}};
    show(j2, 2);

    // {
    //   "messages": [
    //     {
    //       "id": 42,
    //       "text": "Hello"
    //     },
    //     {
    //       "id": 42,
    //       "text": "Hello"
    //     }
    //   ]
    // }
    const json j3 = {{"messages", {message, message}}};
    show(j3, 2);

    // [
    //   "messages",
    //   [
    //     {
    //       "id": 42,
    //       "text": "Hello"
    //     },
    //     {
    //       "id": 42,
    //       "text": "Hello"
    //     }
    //   ]
    // ]
    const json j4 = {"messages", {message, message}};
    show(j4, 2);

    return 0;
}

データ取得

#include <iostream>
#include <nlohmann/json.hpp>

using json = nlohmann::json;

int main() {
    json j = {
        {"key", "value"},
        {"array", {3, 1, 4}},
    };

    // キー"key"の存在確認をせずに書き換え
    // {"key" : "value"} => {"key" : 42}
    j["key"] = 42;

    // キー"array"の存在確認
    // .contains("array") -> bool でもOK
    if (auto itr = j.find("array"); itr != j.end()) {
        // 型チェック
        if (itr->is_array()) {
            // キー"array"の値を参照
            // [3, 1, 4]
            const json &array = *itr;

            // (範囲チェックせずに)要素を参照
            // [3, @1, 4]
            const json &v1 = array.at(1);

            // (型チェックせずに)値を取得
            // 1
            const int num = v1.get<int>();

            // 内部表現を参照
            // [3, 1, 4]
            std::vector<json> &vec = itr->get_ref<json::array_t &>();

            // => [3, 1, 4, 1]
            vec.push_back(num);
        }
    }

    // インデント幅4でシリアライズ
    const std::string serialized = j.dump(4);
    std::cout << serialized << std::endl;
}

任意の構造体との相互変換

Tがデフォルト構築可能ならば、表明static_assert(std::default_initializable<T>);を満足する。 すなわち、T x;が妥当である。 たとえば、int x;は妥当であるが、const int x;は妥当ではない。

デフォルト構築可能な場合

ここにデフォルト構築可能なstruct messageがある。これをjsonと相互変換したい。

struct message {
    int id;
    std::string text;
};
static_assert(std::default_initializable<message>);

リファレンスによると、デフォルト構築可能な型T<=>nlohmann::jsonの変換に必要なものはこの二つらしい:

  • void to_json(nlohmann::json &dst, const T &src);
  • void from_json(const nlohmann::json &src, T &dst);

この二つはclassのinterfaceではないから、Hidden Friendsで書くといいと思う(私の浅知恵)。

このようにする:

struct message {
    int id;
    std::string text;

   private:
    using json = ::nlohmann::json;

    friend void to_json(json &dst, const message &src) noexcept {
        dst["id"]   = src.id;
        dst["text"] = src.text;
    }

    // 変換できない場合の例外をcatchできるようにしておく
    friend void from_json(const json &src, message &dst) {
        src.at("id").get_to(dst.id);
        src.at("text").get_to(dst.text);
    }
};
static_assert(std::default_initializable<message>);

このように動く:

{
    const message origin = { .id = 42, .text = "Hello" }; 

    const json    j = origin;  // to_json()
    const message m = j;       // from_json()
}

{
    const message origin = { .id = 42, .text = "Hello" }; 

    const json    j  = static_cast<json>   (origin);
    const message m1 = static_cast<message>(j);
    const message m2 = j.get      <message>();

    message m3;
    j.get_to(m3);
}

デフォルト構築不可能な場合

ここにデフォルト構築不可能なstruct messageがある。これをjsonと相互変換したい。

class message {
    int id_;
    std::string text_;

   public:
    message() = delete;

    template <typename... Args>
        requires std::constructible_from<std::string, Args...>
    message(int id, Args... args)
        : id_{id}
        , text_{args...} {}

    auto &id() const noexcept { return this->id_; }
    auto &text() const noexcept { return this->text_; }
};
static_assert(not std::default_initializable<message>);

リファレンスによると、デフォルト構築不可能な型T<=>nlohmann::jsonの変換に必要なものはこの二つらしい:

namespace ::nlohmann {

template <>
struct adl_serializer<T> {
    void to_json(nlohmann::json &dst, const T &src);
    void from_json(const nlohmann::json &src);
};

} // ::nlohmann

もしto_json()のみが必要であれば、Hidden Friendsでの実装でも動く。 しかし、to_json()Hidden Friendsで実装し、from_json()を特殊化で実装すると動かない。

このようにする:

template <>
struct nlohmann::adl_serializer<message> {
    static void to_json(json &dst, const message &src) noexcept {
        dst["id"]   = src.id();
        dst["text"] = src.text();
    }

    // 変換できない場合の例外をcatchできるようにしておく
    static message from_json(const json &src) {
        return {
            src.at("id").get<int>(),
            src.at("text").get<std::string>(),
        };
    }
};

このように動く:

{
    const message origin = {42, "Hello"}; 

    const json    j = origin;  // to_json()
    const message m = j;       // from_json()
}

{
    const message origin = {42, "Hello"}; 

    const json    j  = static_cast<json>   (origin);
    const message m1 = static_cast<message>(j);
    const message m2 = j.get      <message>();

    // デフォルト構築不可能
    // message m3;
    // j.get_to(m3);
}

ソート

nlohmann::jsonはランダムアクセスイテレーターを満たしていないらしいので、std::ranges::sort()を利用できない。 しかし、内部表現の配列std::vector<nlohmann::json>を参照することで、std::ranges::sort()を利用できる。 ただし、暗黙に型変換を行うので、比較関数の扱いが厄介だ。

#include <iostream>
#include <nlohmann/json.hpp>

using json = nlohmann::json;

int main() {
    json j = R"(  {"array" : [3, 1, 4, 1, 5]}  )"_json;

    {
        std::sort(j.at("array").begin(), j.at("array").end());
        
        // {"array":[1,1,3,4,5]}
        std::cout << j.dump() << std::endl;
    }

    {
        // 内部表現を参照
        std::vector<json> &array = j.at("array").get_ref<json::array_t &>();

        // int型として降順ソート
        std::ranges::sort(array, std::greater<int>{});
     
        // {"array":[5,4,3,1,1]}
        std::cout << j.dump() << std::endl;
    }

    return 0;
}

リアライゼーション

プログラム中のデータを、ファイルやネットワークへ書き出したり読みだしたりするときにシリアライゼーションをする。 シリアライズせずにメモリに展開された生のbit列をそのまま送出すると、バイトオーダー(エンディアン)が異なる場合に、解釈に齟齬が発生する可能性がある。 バイナリを書き出す場合は、std::cout.write(bin.data(), bin.size()) << std::flush;のようにすると、ヌル文字を超えられる。 const std::uint8_t *では書き出せないので、reinterpret_cast<const char *>する。

文字列

文字列へのシリアライズには.dump(int) -> std::stringを使う。 文字列からのデシリアライズにはstatic json::parse(str/file) -> jsonを使う。 よくある.jsonファイルとして扱える。

MessagePack

#include <nlohmann/json.hpp>

#include "message.hpp"

using json = nlohmann::json;

int main() {
    const message msg1 = {.id = 1, .text = "aaa"};
    const message msg2 = {.id = 2, .text = "bb"};
    const message msg3 = {.id = 3, .text = "c"};

    const json messages = {{"messages", {msg1, msg2, msg3}}};

    const std::vector<std::uint8_t> msgpack = json::to_msgpack(messages);
    const json j                            = json::from_msgpack(msgpack);
    const std::vector<message> vec          = j.at("messages");

    return 0;
}

CBOR

#include <nlohmann/json.hpp>

#include "message.hpp"

using json = nlohmann::json;

int main() {
    const message msg1 = {.id = 1, .text = "aaa"};
    const message msg2 = {.id = 2, .text = "bb"};
    const message msg3 = {.id = 3, .text = "c"};

    const json messages = {{"messages", {msg1, msg2, msg3}}};

    const std::vector<std::uint8_t> cbor = json::to_cbor(messages);
    const json j                         = json::from_cbor(cbor);
    const std::vector<message> vec       = j.at("messages");

    return 0;
}

バイナリ型に符号化方式を埋め込む

nlohmann/jsonシリアライズは、std::stringstd::vector<std::uint8_t>を返す。 すなわち、良くも悪くも、MessagePackCBORが同じ型で表現されることを意味する。 ゆえに、以下のコードはコンパイルエラーとならない。

#include <iostream>
#include <nlohmann/json.hpp>

#include "message.hpp"

using json = ::nlohmann::json;

int main() {
    const message msg1 = {.id = 1, .text = "aaa"};
    const message msg2 = {.id = 2, .text = "bb"};
    const message msg3 = {.id = 3, .text = "c"};

    const json messages = {{"messages", {msg1, msg2, msg3}}};

    const std::vector<std::uint8_t> msgpack = json::to_msgpack(messages);
    const json j                            = json::from_cbor(msgpack);  // XXX
    const std::vector<message> vec          = j.at("messages");

    return 0;
}

ということで、オレオレラップする。

使用例:

const binary_data b = binary_data::from_json(j);
const json        j = binary_data::to_json(b);

basic_binary_data.hpp

#include <nlohmann/json.hpp>
#include <ranges>
#include <vector>

using json = ::nlohmann::json;

/// @tparam Encording msgpack_encording | cbor_encording | json_stringify | etc... in encording.hpp
/// @tparam CharT char | unsgined char | std::size_t | std::uint8_t | etc...
template <typename Encording, typename CharT = char>
    requires (sizeof(CharT) == 1)
class basic_binary_data {
   public:
    using encording = Encording;

    using value_type      = CharT;
    using pointer         = value_type *;
    using const_pointer   = const value_type *;
    using reference       = value_type &;
    using const_reference = const value_type &;
    using iterator        = value_type *;
    using const_iterator  = const value_type *;
    using size_type       = std::size_t;

    static json to_json(const basic_binary_data &x) { return encording::to_json(x.begin(), x.end()); }

    static basic_binary_data from_json(const json &x) {
        // vecの型はstd::vector<T>のみならず、std::stringでも可
        const auto &vec = encording::from_json(x);
        // vecのvalue_typeがbasic_binary_dataのvalue_typeと一致しない場合への対処
        constexpr auto adapter = std::views::transform([](auto a) { return static_cast<value_type>(a); });
        return basic_binary_data{vec | adapter};
    }

    basic_binary_data() noexcept
        : binary_data_{} {}

    basic_binary_data(std::size_t n) noexcept
        : binary_data_(n) {}

    auto begin() noexcept { return this->binary_data_.begin(); }
    auto begin() const noexcept { return this->binary_data_.begin(); }

    auto end() noexcept { return this->binary_data_.end(); }
    auto end() const noexcept { return this->binary_data_.end(); }

    auto size() const noexcept { return this->binary_data_.size(); }
    auto data() noexcept { return this->binary_data_.data(); }
    auto data() const noexcept { return this->binary_data_.data(); }

    void resize(std::size_t n) noexcept { this->binary_data_.resize(n); }

    // value_typeにかかわらず、確実にconst char*を返す
    // ただし、null終端を保証しない
    // sizeとの併用を推奨する
    auto c_str() noexcept { return reinterpret_cast<char *>(this->binary_data_.data()); }
    auto c_str() const noexcept { return reinterpret_cast<const char *>(this->binary_data_.data()); }

   private:
    basic_binary_data(const std::ranges::range auto &rng) noexcept
        : binary_data_{rng.begin(), rng.end()} {}

   private:
    std::vector<value_type> binary_data_;
};

encording.hpp

#include <nlohmann/json.hpp>

using json = ::nlohmann::json;

struct msgpack_encording {
    template <typename Iterator>
    static json to_json(Iterator first, Iterator last) {
        return json::from_msgpack(first, last);
    }

    static std::vector<std::uint8_t> from_json(const json &x) { return json::to_msgpack(x); }
};

struct cbor_encording {
    template <typename Iterator>
    static json to_json(Iterator first, Iterator last) {
        return json::from_cbor(first, last);
    }

    static std::vector<std::uint8_t> from_json(const json &x) { return json::to_cbor(x); }
};

struct json_stringify {
    template <typename Iterator>
    static json to_json(Iterator first, Iterator last) {
        return json::parse(first, last);
    }

    static std::string from_json(const json &x) { return x.dump(); }
};

// struct your_encording {};

binary_data.hpp

#include "basic_binary_data.hpp"
#include "encording.hpp"

// いずれか一つ、お好みの符号化方式を使用する

// JSON
using binary_data = basic_binary_data<json_stringify>;

// MessagePack
// using binary_data = basic_binary_data<msgpack_encording>;

// CBOR
// using binary_data = basic_binary_data<cbor_encording>;

使い方:

main.cpp

#include <filesystem>
#include <fstream>
#include <iostream>
#include <nlohmann/json.hpp>

#include "binary_data.hpp"
#include "message.hpp"

using json = ::nlohmann::json;

int main() {
    const message msg1 = {.id = 1, .text = "aaa"};
    const message msg2 = {.id = 2, .text = "bb"};
    const message msg3 = {.id = 3, .text = "c"};

    const json messages = {{"messages", {msg1, msg2, msg3}}};

    const std::filesystem::path path = [](std::string &&fname) {
        fname += ".";
        fname += binary_data::encording::extension;
        return std::move(fname);
    }("a");

    // シリアライズしてファイルに書き出す
    {
        const binary_data b = binary_data::from_json(messages);

        std::ofstream ofs(path);
        ofs.write(b.c_str(), b.size());
        ofs.flush();
    }

    // ファイルから読み込む
    {
        const std::uintmax_t size = std::filesystem::file_size(path);
        binary_data b(size);

        std::ifstream ifs(path);
        ifs.read(b.c_str(), b.size());

        const json j = binary_data::to_json(b);
    }

    return 0;
}

パッチ

#include <iostream>
#include <nlohmann/json.hpp>

using json = nlohmann::json;

int main() {
    const json origin = {
        {"key", "value"},
    };

    std::cout << origin.dump() << std::endl;

    const json patch = {
        {
            {"op", "replace"},
            {"path", "/key"},
            {"value", 42},
        },
    };

    // originを変更する場合は、.patch_inplace(patch)を使う
    const json patched = origin.patch(patch);

    std::cout << patched.dump() << std::endl;

    return 0;
}
{"key":"value"}
{"key":42}

まとめ

ここが一番よくまとまっています。

C++20 比較関数と任意の型のソート

C++20 比較関数と任意の型のソート

はじめに

想定読者は過去の私である。 なぜこう教えてくれなかったのだ。 次なる私よ、あなたのために。

主題のとおり、C++20を前提に書いている。 もしC++20コンパイルできない古いgccを使用しているならば、 新しいgccsudo apt install g++-13だとか12だとか11だとか10をして、installできると思う。 詳細は己のにくわしく聞くといい。

キーワード:

  • 比較関数
    • operator
    • 三方比較演算子 / 一貫比較 / <=>
    • std::less
    • std::greater
  • ソート / sort
    • std::vector
    • std::list
    • std::set

比較関数

ここにint型オブジェクトabがあって、いくつかの二項演算ができる:

int a{}, b{};

bool less          = a <  b;  // OK
bool greater       = a >  b;  // OK
bool equal_to      = a == b;  // OK
bool not_equal_to  = a != b;  // OK
bool less_equal    = a <= b;  // OK
bool greater_equal = a >= b;  // OK

ここに任意のT型オブジェクトabがあって、いくつかの二項演算ができるかもしれない:

T a{}, b{};

bool less          = a <  b;  // ?
bool greater       = a >  b;  // ?
bool equal_to      = a == b;  // ?
bool not_equal_to  = a != b;  // ?
bool less_equal    = a <= b;  // ?
bool greater_equal = a >= b;  // ?

これらの二項演算を可能とするには、それぞれの演算の定義が必要:

struct T {
    value_type value;

    bool operator< (const T& rhs) const { return this->value <  rhs.value; }
    bool operator> (const T& rhs) const { return this->value >  rhs.value; }
    bool operator==(const T& rhs) const { return this->value == rhs.value; }
    bool operator!=(const T& rhs) const { return this->value != rhs.value; }
    bool operator<=(const T& rhs) const { return this->value <= rhs.value; }
    bool operator>=(const T& rhs) const { return this->value >= rhs.value; }
};


T a{}, b{};

bool less          = a <  b;  // OK
bool greater       = a >  b;  // OK
bool equal_to      = a == b;  // OK
bool not_equal_to  = a != b;  // OK
bool less_equal    = a <= b;  // OK
bool greater_equal = a >= b;  // OK

これらの二項演算は#include <compare>してoperator<=>を利用することで一挙に定義できる。 また、friend指定することでグローバルに利用できる:

#include <compare>  // 必要

struct T {
    value_type value;

    // これが一番かんたんに実装できる
    // しかし、非静的メンバ変数が(大量に)ある場合、そのメンバの数だけ(大量に)比較関数を導出するので注意
    friend auto operator<=>(const T&, const T&) = default;
};


T a{}, b{};

bool less          = a <  b;  // OK
bool greater       = a >  b;  // OK
bool equal_to      = a == b;  // OK
bool not_equal_to  = a != b;  // OK
bool less_equal    = a <= b;  // OK
bool greater_equal = a >= b;  // OK

個別に指定できる:

#include <compare>  // 必要

struct T {
    value_type value;

    value_type value1;
    value_type value2;
    value_type array[1000000];

    bool operator==(const T& rhs) const { return this->value == rhs.value; } // おまじない
    auto operator<=>(const T& rhs) const { return this->value <=> rhs.value; }
};

他にも何通りかの実装方法がある:

#include <compare>  // 必要

struct T {
    value_type value;

    auto operator<=>(const T& rhs) const = default;
};
#include <compare>  // 必要

struct T {
    value_type value;

    friend bool operator==(const T& lhs, const T& rhs) { return lhs.value == rhs.value; } // おまじない
    friend auto operator<=>(const T& lhs, const T& rhs) { return lhs.value <=> rhs.value; }
};

ところで、operator()を定義すると、そのインスタンスは関数を呼び出すような振る舞いができる:

struct F {
    void operator()(/* 引数リスト */) const {
        return;
    }
};


F f{};

f();   // invokable

F{}(); // インスタンス F{} に対する関数呼び出し ()

二つの引数を受け取って大小比較するoperator()も定義できる:

struct T {
    value_type value;

    friend auto operator<=>(const T&, const T&) = default;
};

struct Less {
    bool operator()(const T& lhs, const T& rhs) const {
        return lhs < rhs; // T型のまま比較
    }
};

struct Greater {
    bool operator()(const T& lhs, const T& rhs) const {
        return lhs > rhs; // T型のまま比較
    }
};


T a{}, b{};

bool less    = Less   {}(a, b);  // a < b
bool greater = Greater{}(a, b);  // a > b

任意の型Tのinner classとしても定義できる。 inner classなのでprivateなメンバであっても参照・比較できる:

struct T {
    // 説明のため未定義
    // friend auto operator<=>(const T&, const T&) = default;
    
    // 下に定義したT::Less、T::Graterをもとに比較する
    // operator<=>で定義してしまえばinner classのアクセス制限の緩和の恩恵なんてないけれども

    struct Less {
        bool operator()(const T& lhs, const T& rhs) const {
            return lhs.value < rhs.value;
        }
    };

    struct Greater {
        bool operator()(const T& lhs, const T& rhs) const {
            return lhs.value > rhs.value;
        }
    };

   private:
    value_type value;
};


T a{}, b{};

// bool less    = a < b; operator<は未定義
// bool greater = a > b; operator>は未定義

bool less    = T::Less   {}(a, b);
bool greater = T::Greater{}(a, b);

上記のstruct Lessstruct Greaterは、よりイケてる形で標準ライブラリにある:

#include <functional>

struct T {
    value_type value;

    friend auto operator<=>(const T&, const T&) = default;
};


T a{}, b{};

bool less          = std::less         {}(a, b);  // a <  b
bool greater       = std::greater      {}(a, b);  // a >  b
bool equal_to      = std::equal_to     {}(a, b);  // a == b
bool not_equal_to  = std::not_equal_to {}(a, b);  // a != b
bool less_equal    = std::less_equal   {}(a, b);  // a <= b
bool greater_equal = std::greater_equal{}(a, b);  // a >= b

任意の型のソート

ソーティングの結果には2通りある:

  • 昇順
  • 降順

比較によりこの結果が決定づけられる:

  • a < bのとき昇順
  • a > bのとき降順

関数sort()に引数として<>を与えることは出来ない:

sort(data, <);  // NG
sort(data, >);  // NG

sort(data, '<');  // OK?
sort(data, '>');  // OK?

インスタンスであれば、関数sort()に引数を与えられる:

std::less less{};
sort(data, less);  // OK

sort(data, std::greater{});  // OK

以上より、任意の型Tのソートはこのようにする:

#include <compare>

struct T {
    using value_type = int;  // お好みの型
    value_type value;

    friend auto operator<=>(const T&, const T&) = default;
};

#include <vector>
#include <list>
#include <set>

#include <algorithm>
#include <functional>

int main() {
    // 比較関数の指定を省略すれば、operator<で比較する
    // すなわち、デフォルトで昇順ソート

    // std::array<T, N>でも同様
    std::vector<T> vec = {/* 略 */};
    std::sort(vec.begin(), vec.end());                                                                    // 昇順
    std::sort(vec.begin(), vec.end(), std::less{});                                                       // 昇順
    std::sort(vec.begin(), vec.end(), std::greater{});                                                    // 降順
    std::sort(vec.begin(), vec.end(), [](const T& lhs, const T& rhs) { return lhs.value < rhs.value; });  // 昇順
    std::sort(vec.begin(), vec.end(), [](const T& lhs, const T& rhs) { return lhs.value > rhs.value; });  // 降順
    std::ranges::sort(vec);                                                                               // 昇順
    std::ranges::sort(vec, std::less{});                                                                  // 昇順
    std::ranges::sort(vec, std::greater{});                                                               // 降順
    std::ranges::sort(vec, [](const T& lhs, const T& rhs) { return lhs.value < rhs.value; });             // 昇順
    std::ranges::sort(vec, [](const T& lhs, const T& rhs) { return lhs.value > rhs.value; });             // 降順
    std::ranges::sort(vec, {}, &T::value);                                                                // 昇順
    std::ranges::sort(vec, std::less{}, &T::value);                                                       // 昇順
    std::ranges::sort(vec, std::greater{}, &T::value);                                                    // 降順

    // std::listはランダムアクセスイテレーターを満たさないため、std::ranges::sort()を使えない
    // メンバ関数を使う
    std::list<T> list = {/* 略 */};
    list.sort();                                                                  // 昇順
    list.sort(std::less{});                                                       // 昇順
    list.sort(std::greater{});                                                    // 降順
    list.sort([](const T& lhs, const T& rhs) { return lhs.value < rhs.value; });  // 昇順
    list.sort([](const T& lhs, const T& rhs) { return lhs.value > rhs.value; });  // 降順

    // std::setは常にstd::is_sorted()を満たすように、型に比較関数を指定する
    std::set<T>                  set1 = {/* 略 */};  // 昇順
    std::set<T, std::less<T>>    set2 = {/* 略 */};  // 昇順; ここは"<T>"が必要らしい
    std::set<T, std::greater<T>> set3 = {/* 略 */};  // 降順; ここは"<T>"が必要らしい
}

おまけ黒魔術:

struct T {
    using value_type = int;
    value_type value;

    operator int() const { return this->value; }
};


T a{}, b{};

bool less = a < b;  // 暗黙の型変換でint型として比較

謝辞

この文書は、少なくとも以下を参照して書かれた。

GoogleSpreadsheetの計算精度について

Google spreadsheetの計算精度について

こんにち、一般に浮動小数点数IEEE754 binary64の形式に準拠している。 この規格では、十進小数を15.95桁の精度で表現する。 ゆえに、たとえば、0.1+0.20.3は等しくない

Google spreadsheetやMS-Excelもこの例に漏れていない。 ただし、これらの表計算ソフトは一部の計算において、その精度を十進小数15桁に落としている。 ゆえに、たとえば、=(1+1E-15) = 1=trueとなる。 したがって、"IEEE754 binary64に準拠した計算"が困難となる。

このことは、ダメージ検証に際して大きな障害であった。 ダメージ検証では、数学分野や金勘定システムとは異なり、IEEE754 binary64に準拠した十進小数15.95桁精度の計算が"正しい"。 特に、四則演算の順序や小数から整数への丸めにおいて、実際の挙動とは異なる計算結果を示していた。 よって、"正しい"計算を行うためにGoogle spreadsheetでIEEE754 binary64に準拠した計算を行うことが求められた。 (Google spreadsheetを利用すべきなのかという疑問に対する回答は省略する。)導入は以上だ。結論を示す。

Google spreadsheetでIEEE754 binary64に準拠した計算は可能だ(ほんまか?)。

例えば切り捨ての計算はこのように行う。まず、sign(・)で符号ビットを検証する。

ああ、いや、これは冗談だ。勘弁してくれ。いったい誰がこれを扱えるのか。私には無理だ。

私は以下のように行う。

  • a=bdelta(a,b)を使う。
  • a<>bnot(delta(a,b))を使う。
  • a<=bgestep(a,b)を使う。
  • a>bnot(gestep(a,b))を使う。
  • 四則演算+-*-はそのまま使える。
  • int(a)int(a)-not(gestep(a,int(a)))を使う。
  • ceiling(a)ceiling(x) + not(gestep(ceiling(x), x))を使う。
  • min(args)およびmax(args)はそのまま使える。
  • Google Apps Script (GAS) はそのまま使える。ただし、関数呼び出しが遅い。

int(a)-not(gestep(a,int(a)))による切り捨ての実例を示す。

表計算ソフトで金勘定をするべきではない。