C++ ムーブと右辺値参照について誤解していたこと

本文

変数はとにかくconstを付けてできる限り不変にしたほうがよい。 したがって例えば以下のようなコードを書いていた:

#include <utility>
#include <vector>

namespace kcv {

class foo final {
   public:
    foo(const std::vector<int>& vec)
        : vec_{vec} {}

    foo(std::vector<int>&& vec) noexcept
        : vec_{std::move(vec)} {}

   private:
    std::vector<int> vec_;
};

}  // namespace kcv

int main() {
    const auto vec = std::vector<int>{1, 2, 3};

    const auto foo = kcv::foo{std::move(vec)};
}

std::move()がいい感じに右辺値参照型std::vector<int>&&にキャストして、ムーブコンストラクfoo(std::vector<int>&&)を呼び出していると思っていた。

しかし実際にはconst std::vector<int>&&にキャストされ、コピーコンストラクfoo(const std::vector<int>&)が呼び出される。

いや、考えてみれば当たり前だった。 constなのだからオブジェクト内部のポインタを挿げ替えることなんてできやしない。 しかし、std::move()がうまいことキャストしているものだと思っていた。

これからはconst_castを使おう! auto vec = std::vector<int>{1, 2, 3};としてstd::move()しよう!

艦これ 装備ボーナスオブジェクトのフォーマットと求値アルゴリズム

装備ボーナスオブジェクトのフォーマットと求値アルゴリズム

艦これの装備ボーナスはmain.jsに記述されておりユーザが調べるまでもなく決定的です。 もちろん、その実際の効果は検証によって明らかにされることではありますが、装備ボーナスはUIの一部として通信を行わずしてクライアント側で観測できる要素です。 したがって「何かの艦娘に何かの装備を載せたところ、これこれというステータス上昇が得られた」などという情報は「柔らかなソーシャル」においてはもっともですがシステムとして本質ではありません。

さて、装備ボーナスは難読化されたうえでmain.jsに記載されています。 われわれ一般提督が扱いやすいように有志提督がJSONに変換したもの、それがgithubなどで公開されています。 本稿では、それらJSONで記述された「装備ボーナスオブジェクト」のフォーマットおよび装備ボーナスの求値アルゴリズムに説明の付与を試みます。

最小構成の例

step1

いきなり装備ボーナスについて説明すると複雑度が大きいため、構成要素を絞ります。 「ある条件を満たすときある値を加算する」これをコードで表現すると以下のように表せます。

なお、コードはプログラミング言語C++をもとに書いています。 細かな言語仕様はともかく、雰囲気は伝わると思っています。

int main() {
    int total = 0;
    
    // ある条件を満たすときある値を加算する
    if (cond1) {
        total += value1;
    }

    if (cond2) {
        total += value2;
    }

    if (cond3) {
        total += value3;
    }
}

step2

step1はとても説明的に表せていて良いように見えます。 しかし、たとえば次の二点において嬉しくないこともあります。

  1. 条件と値のペアが増えると何行でもコードが肥大化する
  2. 処理言語が変わるとif+=などの文法が異なることもあるため移植性に乏しい

これらの嬉しくない点については容易に解決できます。 たとえば、値と条件のペア、これを配列で表すとともにforを用いることでコードが肥大化せず処理できます。 加えて、この配列をJSONで表現できれば移植が容易になります。 以下の例では、JSONの解析は本稿の範囲外とし、単なる配列として条件と値のペアの配列を与えています。

// 条件と値をまとめて扱うための型を用意
struct datum_type {
    bool cond;
    int value;
};

// 条件と値のペアの配列を定義 (JSONを解析して得られる装備ボーナスオブジェクトに相当)
const datum_type data[] = {
    datum_type{ .cond = true,  .value = 1 },
    datum_type{ .cond = false, .value = 2 },
    datum_type{ .cond = true,  .value = 3 },
};

int main() {
    int total = 0;
    
    // dataの各要素の条件と値とを処理
    for (const auto& [cond, value] : data) {
        // ある条件を満たすときある値を加算する
        if (cond) {
            total += value;
        }
    }
}

step3

装備ボーナスに進む前にもう少し複雑化してステップを踏んでおきます。

条件がより複雑でそれが型condition_typeで表され、値もより複雑でそれが型value_typeで表されるとします。

// 条件を表す型
struct condition_type {
    // ...
};

// 値を表す型
struct value_type {
    // ...
};

// 条件と値をまとめて扱うための型
struct datum_type {
    condition_type cond;
    value_type value;
};

condition_type cond;ifにアダプトするために述語関数(boolを返す関数)を定義します。

// 述語関数
bool matches_condition(const condition_type& cond) {
    // 実装については後述
}

値が複雑化した場合は+=をカスタマイズするなどをします。 C++ではオーバーロードによって+=をカスタマイズできます。 ご利用の言語をご確認ください。

// C++における+=のオーバーロード (カスタマイズ)
auto operator+=(value_type& lhs, const value_type& rhs) -> value_type& {
    // メンバごとに+=する
    lhs.rais += rhs.rais;
    lhs.tais += rhs.tais;
    return lhs;
}

これらを用意すると以下のように書けます。 全体の複雑度は増していますが、主たる処理に大きな変更はありません。

// 条件と値のペアの配列を定義 (JSONを解析して得られる装備ボーナスオブジェクトに相当)
const datum_type data[] = { /* 略 */ };

int main() {
    // ゼロ初期化
    auto total = value_type{};

    // dataの各要素の条件と値とを処理
    for (const auto& [cond, value] : data) {
        // ある条件を満たすときある値を加算する
        if (matches_condition(cond)) {
            total += value;
        }
    }
}

condition_typeおよび述語関数bool matches_condition(const condition_type& cond);について、 メンバ同士はAND、メンバが配列の場合その各要素はORとして記述できます。

たとえば、"装備IDid{1, 2, 3}のいずれか" かつ "装備改修値level > 6"の場合は次のように書けます。 具体的な値はJSONによって与えられるためここでは指定しません。

// 艦娘が搭載している装備一つを表す型
struct equipment_type {
    int id;    // 装備ID
    int level; // 装備改修値
};

// 条件を表す型
struct condition_type {
    std::vector<int> ids; // 装備IDの可変長配列
    int level;            // 装備改修値
};

// json = "[{ "ids": [1, 2, 3], "level": 6 }]"

// 述語関数
bool matches_condition(const equipment_type& equipment, const condition_type& cond) {
    // "装備ID`id`が`{1, 2, 3}`のいずれか" かつ "装備改修値`level > 6`"
    return std::ranges::contains(cond.ids, equipment.id)
       and equipment.level > cond.level;
}

フォーマット

公開されているフォーマット(JSONデータ)は、各々の開発者が独自に使い良いように定義しているためフォーマットは統一されていません。 代表的なものとして以下があります。

  • 74式EN
  • KC3改
  • 制空権シミュレータ
    • JSONというよりもtsコードの一部として定義
  • (fleethub)

いずれのフォーマットも似通っているため、ここでは代表として74式ENのフォーマットを取り扱います。

  • FitBonusPerEquipment

    • 装備ボーナスを集計する条件その1を表します。艦娘がidsで指定される装備をN個搭載しているか、 typesで指定される装備をN個搭載している場合にbonusesを処理します(N > 0)。
    • idstypesとが同時に指定されることは無いはずです。どちらも指定されないといったことも無いはずです。つまりidstypesの存在性はXORにはるはずです。

      {
          ids?: number[]           // 必要装備ID
          types?: number[]         // 必要装備カテゴリID
          bonuses: FitBonusData[]  // ids XOR types で指定される装備をベースとする装備ボーナス、その追加条件と値
      }
      
  • FitBonusData

    • 装備ボーナスを集計する条件その2を表します。この条件を満たす場合にFitBonusValueを計上します。
    • idsXORtypesで指定される装備を搭載していて、艦娘に対する条件を満たし、requires*によって指定される装備を搭載し、idsXORtypesで指定される装備のうち改修値がlevel以上の装備をnum個以上搭載している場合に計上します。

      {
          // 艦娘に対する条件
          shipS?: number[]            // 未改造ID
          shipClass?: number[]        // 艦型ID
          shipNationality?: number[]  // 国籍
          shipType?: number[]         // 艦種ID
          shipX?: number[]            // 艦船ID
      
          // いわゆるシナジー条件 その1
          requires?: number[]         // 装備ID 
          requiresLevel?: number      // 装備IDの最小改修値
          requiresNum?: number        // 装備IDの必要個数 (未使用?)
      
          // いわゆるシナジー条件 その2
          requiresType?: number[]     // 装備カテゴリID
          requiresNumType?: number    // 装備カテゴリIDの必要個数
      
          // ids XOR types で指定される装備に対する条件
          level?: number              // 以上の条件を全てANDで満たすとともに、(ids XOR types)で指定された装備のうち改修値がlevel以上の装備について、
          num?: number                // その個数がnum個以上であるならば一限で計上する
          bonus?: FitBonusValue       // 個数指定が無ければ搭載数ぶんだけ計上する
      
          // 電探条件
          bonusAR?: FitBonusValue     // numとlevelを除く以上の条件を全てANDで満たすとともに、対空電探を搭載している場合に計上する
          bonusSR?: FitBonusValue     // numとlevelを除く以上の条件を全てANDで満たすとともに、水上電探を搭載している場合に計上する
          bonusAccR?: FitBonusValue   // numとlevelを除く以上の条件を全てANDで満たすとともに、命中電探を搭載している場合に計上する
      }
      
  • FitBonusValue

    • 装備ボーナスの値を表します。この値を集計したものが最終的な装備ボーナス値となります。

      {
          houg?: number  // 火力
          tyku?: number  // 対空
          kaih?: number  // 回避
          souk?: number  // 装甲
          houm?: number  // 命中
          tais?: number  // 対潜
          raig?: number  // 雷撃
          saku?: number  // 索敵
          leng?: number  // 射程
          baku?: number  // 爆装
      }
      

求値アルゴリズム

大まかには以下のような雰囲気で処理できます。 ifの条件式を/* コメント */に替えているものは、実装としては単なるnullチェックに相当します。

auto total_equipment_bonus(const ship_t& ship, const std::vector<FitBonusPerEquipment>& fit_bonuses) -> FitBonusValue {
    auto total = FitBonusValue{};

    for (const auto& [ids, types, bonuses] : fit_bonuses) {
        // (ids XOR types)で指定される装備を艦娘のスロットから抽出する
        // 実際に必要となるのは、装備の改修値と個数
        const auto fit_equipments = extract_fit_equipments(ship, ids, types);
        if (fit_equipments.size() == 0) continue;
    
        for (const auto& data : bonuses) {
            if (not matches_data(ship, data)) continue;

            // 少しだけややこしいが少しだけ考えるとよく分かる部分
            if (/* data.bonusが有効な値を保持している */) {
                const int num = /* data.levelによって有効な改修値の最小値を設定されている */
                              ? /* fit_equipmentsのうち、その改修値がdata.level以上のもの、その個数 */ 
                              : /* fit_equipmentsの個数*/;
                if (/* data.numによって一限の制限を設定されている */) {
                    if (num >= data.num) {
                        total += data.bonus;
                    }
                } else {
                    // data.bonusをnum個ぶんだけ計上する
                    total += data.bonus * num; 
                }
            }

            // それぞれの電探を搭載しているならば計上する
            if (has_anti_air_radar and /* bonusARが有効な値を保持している */) {
                total += bonusAR;
            }
            // total += bonusSR;
            // total += bonusAccR;
        }
    }

    return total;
}

C++ 標準例外にスタックトレースを付けたいメモ

C++ 標準例外にスタックトレースを付けたいメモ

  • std::stacktrace
  • std::source_location
  • std::nested_exception

実装例

#include <cstdio>
#include <exception>
#include <format>
#include <print>
#include <source_location>
#include <stacktrace>
#include <string>

namespace kcv {

class exception : public std::exception {
   public:
    explicit exception(
        std::string msg          = "kcv::exception",
        std::source_location loc = std::source_location::current(),
        std::stacktrace trace    = std::stacktrace::current(1)
    )
        : what_{std::move(msg)}
        , loc_{loc}
        , trace_{trace} {}

    exception(const exception&)            = default;
    exception& operator=(const exception&) = default;
    exception(exception&&)                 = default;
    exception& operator=(exception&&)      = default;
    ~exception() noexcept override         = default;

    auto what() const noexcept -> const char* override final {
        return what_.c_str();
    }

    auto stacktrace() const noexcept -> const std::stacktrace& {
        return trace_;
    }

    auto source_location() const noexcept -> const std::source_location& {
        return loc_;
    }

   private:
    std::string what_;
    std::source_location loc_;
    std::stacktrace trace_;
};

inline auto make_exception_with_context(
    std::string msg          = "kcv::exception",
    std::source_location loc = std::source_location::current(),
    std::stacktrace trace    = std::stacktrace::current(1)
) -> kcv::exception {
    return kcv::exception{std::move(msg), loc, trace};
}

void print_exception(std::FILE* stream, const std::exception& e) {
    if (const auto kcv_exception = dynamic_cast<const kcv::exception*>(&e)) {
        const auto& loc = kcv_exception->source_location();
        std::print(
            stream,
            "{}:{}:{}: error: {}\n"
            "{}",
            loc.file_name(), loc.line(), loc.function_name(), kcv_exception->what(), kcv_exception->stacktrace()
        );
    } else {
        std::print(stream, "{}\n", e.what());
    }

    try {
        std::rethrow_if_nested(e);
    } catch (const std::exception& nested) {
        std::print(stream, "caused by: ");
        print_exception(stream, nested);
    } catch (...) {
        std::print(stream, "caused by: <non-standard exception>\n");
    }
}

}  // namespace kcv

void layer3() {
    throw std::exception{};
}

void layer2() {
    try {
        layer3();
    } catch (const std::exception& e) {
        std::throw_with_nested(kcv::make_exception_with_context());
    }
}

void layer1() {
    try {
        layer2();
    } catch (const std::exception& e) {
        std::throw_with_nested(kcv::make_exception_with_context());
    }
}

int main() try {
    layer1();  //
} catch (const std::exception& e) {
    kcv::print_exception(stderr, e);
}

実行例

source/main.cpp:100:void layer1(): error: kcv::exception layer1
   0# main at source/main.cpp:112
   1# __libc_start_call_main at ../sysdeps/nptl/libc_start_call_main.h:58
   2# __libc_start_main_impl at ../csu/libc-start.c:360
   3# _start at :0
   4# 
caused by: source/main.cpp:92:void layer2(): error: kcv::exception layer2
   0# layer1() at source/main.cpp:98
   1# main at source/main.cpp:112
   2# __libc_start_call_main at ../sysdeps/nptl/libc_start_call_main.h:58
   3# __libc_start_main_impl at ../csu/libc-start.c:360
   4# _start at :0
   5# 
caused by: std::exception

C++ 自作の例外送出ラッパー関数とstd::stacktraceの使用例メモ

本文

実装例

#include <concepts>
#include <cstdio>
#include <cstdlib>
#include <exception>
#include <format>
#include <print>
#include <ranges>
#include <source_location>
#include <stacktrace>
#include <stdexcept>
#include <string_view>

namespace kcv {

template <typename E>
    requires std::derived_from<E, std::exception> and std::constructible_from<E, const std::string&>
[[noreturn]] void throw_exception(std::string_view msg, std::source_location loc = std::source_location::current()) {
    const auto what = std::format(
        "{}:{}:{}: error: {}\n"                          //
        "{}",                                            //
        loc.file_name(), loc.line(), loc.column(), msg,  //
        std::stacktrace::current(1)                      //
    );
    throw E{what};
}

}  // namespace kcv

void g() {
    constexpr int arr[] = {1, 2, 3};
    constexpr auto size = std::ranges::size(arr);

    const auto i = 5uz;
    if (i >= size) {
        const auto messgae = std::format("out of range. [size: {}, index: {}]", size, i);
        kcv::throw_exception<std::out_of_range>(messgae);
    } else {
        std::println("{}", arr[i]);
    }
}

void f() {
    g();
}

void dummy() {
    // nop
}

int main() try {
    dummy();
    f();
} catch (const std::exception& e) {
    std::println(stderr, "{}", e.what());
    return EXIT_FAILURE;
}

実行例

GCCでは-lstdc++expが必要。 たぶんclangも同じライブラリを使うなら必要かも。 最適化レベルとかのほうじゃなくてライブラリ実装の指定かしら。

# 付与例
add_executable(main ./source/main.cpp)
target_link_libraries(main -lstdc++exp)
/home/hedgehog/cpp/test/source/main.cpp:36:9: error: out of range. [size: 3, index: 5]
   0#  g() at /home/hedgehog/cpp/test/source/main.cpp:36
   1#  f() at /home/hedgehog/cpp/test/source/main.cpp:43
   2# main at /home/hedgehog/cpp/test/source/main.cpp:52
   3# __libc_start_call_main at ../sysdeps/nptl/libc_start_call_main.h:58
   4# __libc_start_main_impl at ../csu/libc-start.c:360
   5# _start at :0
   6# 

感想

  • std::source_locationのフォーマットを覚えられないのでこれを兼ねてメモ
  • 例外型Eのクラス名を(静的リフレクションで)取得できれば幸せになれるかも

C++ enumにメンバ関数を定義したかった

C++ enumメンバ関数を定義したい

世間には既にこの要求が溢れていて、様々な提案がされている。 C++標準には提案されていない。

CRTPとDeducing thisについて学習をしていてこれを達成できないかと気になったので考えた。 結論としては達成できなかった。

enumは典型的にはswitchでの分岐かビットフラグとして使うと思う。 ここではswitchで使う方を想定する。

switchといえばstd::variantは型レベルの"switch"ができる。 型であればCRTPでインターフェースを要求してそれぞれの型にメンバ関数を要求できる。

#include <cstdio>
#include <print>
#include <string_view>
#include <variant>

namespace kcv {

// enum class color { red, green, blue }; をもとに、それぞれのメンバを型で表して、メンバ関数を実装

// CRTP
template <typename D>
class color_base {
   public:
    constexpr auto to_string() const noexcept -> std::string_view {
        return this->derived().to_string();
    }

   private:
    constexpr auto derived() noexcept -> D& {
        return static_cast<D&>(*this);
    }

    constexpr auto derived() const noexcept -> const D& {
        return static_cast<const D&>(*this);
    }
};

// color::red
struct red_impl final : public color_base<red_impl> {
    constexpr auto to_string() const noexcept -> std::string_view {
        return "red";
    }
} constexpr red = {};

// color::green
struct green_impl final : public color_base<green_impl> {
    constexpr auto to_string() const noexcept -> std::string_view {
        return "green";
    }
} constexpr green = {};

// color::blue
struct blue_impl final : public color_base<blue_impl> {
    constexpr auto to_string() const noexcept -> std::string_view {
        return "blue";
    }
} constexpr blue = {};

// enum class color
using color = std::variant<red_impl, green_impl, blue_impl>;

// オーバーロードパターン
template <typename... Ts>
struct overload : public Ts... {
    using Ts::operator()...;
};

template <typename... Ts>
overload(Ts...) -> overload<Ts...>;

}  // namespace kcv

template <typename D>
void print(kcv::color_base<D> color) {
    std::println("{}", color.to_string());
}

void print(kcv::color color) {
    std::visit(
        []<typename D>(const kcv::color_base<D>& color) static -> void { 
            std::println("{}", color.to_string());
        },
        color
    );
}

int main() {
    const auto red = kcv::red;
    print(red);

    const kcv::color green = kcv::green;
    print(green);

    const auto blue = kcv::blue;
    std::println("{}", blue.to_string());
}

実際に使えるとは言っていない

C++23 std::exitする直前にstacktraceとかを書き出す自分用メモ

自分用exit_with_error関数

#include <cstdlib>
#include <print>
#include <source_location>
#include <stacktrace>
#include <string>

namespace kcv {

[[noreturn]]
void exit_with_error(const std::string& error, std::source_location loc = std::source_location::current()) {
    std::println(
        "{}: In function `{}`\n"
        "{}:{}:{}: error: {}\n"
        "{}",
        loc.file_name(), loc.function_name(), loc.file_name(),
        loc.line(), loc.column(), error,
        std::stacktrace::current(1)
    );
    std::exit(EXIT_FAILURE);
}

}  // namespace kcv

void g() {
    kcv::exit_with_error("test");
}

void f() {
    g();
}

int main() {
    f();
}

実行例:

(略)test/source/main.cpp: In function `void g()`
(略)test/source/main.cpp:26:25: error: test
   0#  g() at :0
   1#  f() at :0
   2# main at :0
   3# __libc_start_call_main at ../sysdeps/nptl/libc_start_call_main.h:58
   4# __libc_start_main_impl at ../csu/libc-start.c:360
   5# _start at :0
   6# 

C++ 床関数とその逆関数

床関数に逆関数は定義できないが、区間で扱えばそれっぽいものができる。

#include <boost/numeric/interval.hpp>
#include <cmath>
#include <concepts>
#include <print>
#include <random>

namespace kcv {

using interval = boost::numeric::interval<double>;

template <typename T>
constexpr auto floor(const T& x) -> T {
    if constexpr (std::same_as<T, interval>) {
        return interval{std::floor(x.lower()), std::floor(x.upper())};
    } else if constexpr (std::floating_point<T> or std::integral<T>) {
        return std::floor(x);
    } else {
        static_assert(false);
    }
}

template <typename T>
auto floor_inverse(const T& x) -> interval {
    if constexpr (std::same_as<T, interval>) {
        return interval{kcv::floor_inverse(x.lower()).lower(), kcv::floor_inverse(x.upper()).upper()};
    } else if constexpr (std::floating_point<T> or std::integral<T>) {
        return interval{x, x + 1};
    } else {
        static_assert(false);
    }
}

template <typename T>
constexpr bool in(const T& lhs, const interval& rhs) {
    if constexpr (std::same_as<T, interval>) {
        return rhs.lower() <= lhs.lower() and lhs.upper() <= rhs.upper();
    } else if constexpr (std::floating_point<T> or std::integral<T>) {
        return rhs.lower() <= lhs and lhs <= rhs.upper();
    } else {
        static_assert(false);
    }
}

}  // namespace kcv

int main() {
    auto seed_gen = std::random_device{};
    auto engin    = std::default_random_engine{seed_gen()};
    auto dist     = std::uniform_real_distribution<>{-100.0, 100.0};

    for (bool in = true; in;) {
        const auto temp = dist(engin);
        const auto x    = kcv::interval{temp, temp + std::fabs(dist(engin))};
        const auto y    = kcv::floor_inverse(kcv::floor(kcv::floor_inverse(kcv::floor(x))));
        in              = kcv::in(x, y);
        // std::println("{}: {:.16f} ∊ [{:.16f}, {:.16f}]", in, x, y.lower(), y.upper());
        std::println("{}: [{:.16f}, {:.16f}] ∊ [{:.16f}, {:.16f}]", in, x.lower(), x.upper(), y.lower(), y.upper());
    }
}

実行例

true: [-26.2509252080405702, -5.1628427056888171] ∊ [-27.0000000000000000, -4.0000000000000000]
true: [46.8973364524507019, 57.6188605671290475] ∊ [46.0000000000000000, 59.0000000000000000]
true: [6.9922102230268735, 84.5419802956055122] ∊ [6.0000000000000000, 86.0000000000000000]
true: [9.8322779595293923, 49.0346651840906205] ∊ [9.0000000000000000, 51.0000000000000000]
true: [-94.2167368544990467, -74.9632873223832661] ∊ [-95.0000000000000000, -73.0000000000000000]
true: [-64.9015763947171109, -7.9979151599408880] ∊ [-65.0000000000000000, -6.0000000000000000]
true: [68.9245064825205986, 144.4859684699335389] ∊ [68.0000000000000000, 146.0000000000000000]
true: [18.3754696395526480, 63.9333296240576132] ∊ [18.0000000000000000, 65.0000000000000000]
true: [-41.8410633106878080, -1.4849703370931806] ∊ [-42.0000000000000000, 0.0000000000000000]
true: [9.0674299667539628, 81.1420643845776652] ∊ [9.0000000000000000, 83.0000000000000000]
true: [-93.7908175615415445, -31.5383955667387710] ∊ [-94.0000000000000000, -30.0000000000000000]
true: [-11.9471865927050089, 71.9437196031107646] ∊ [-12.0000000000000000, 73.0000000000000000]
true: [4.9858119638782910, 86.2839493544630898] ∊ [4.0000000000000000, 88.0000000000000000]
true: [-7.5972642458043964, 86.7705268152596574] ∊ [-8.0000000000000000, 88.0000000000000000]
true: [76.6138325876984823, 98.3996081561534339] ∊ [76.0000000000000000, 100.0000000000000000]