今回は、前回に引き続き数学編ということで 「数学クラス(行列)」についての記事を書いていきます。
前回のベクトル編ではC++20/23の機能を使ったリファクタリングを紹介しましたが、行列ではベクトルにあったような「Swizzle(スウィズル)機能」は不要です。 では、一体どこを重点的にリファクタリングしたのか、というところをメインに紹介していきます。
前回:
ゲームフレームワークをツクリタイ : 数学編 - ReU’s diary
データの持ち方
まず、今回の行列クラスにおける内部データの持ち方は以下のようになっています。
union
{
TVector<Type, Column> vec[Row];
Type value[Row * Column]{};
};
Row(行)と Column(列)をテンプレート引数で受け取り、行ごとのベクトル配列(vec)としても、フラットな1次元配列(value)としてもアクセスできるように union を組んでいます。
1. パラメータパック展開
可変長引数を受け取って内部配列に詰め込むパラメータパックの展開処理ですが、基本的には前回のベクトルクラスと全く同じアプローチを取っています。そのため、今回の記事では詳しい説明は割愛します。
2. アクセサ関数の共通化
以前の実装では、行列の各要素へのアクセサ関数を、以下のように手動でひたすらコピペして量産していました。
Type _11() { return vec[0][0]; } Type _12() { return vec[0][1]; }
※上記はコードを端折っていますが、実際には static_assert を使用して、_12() などのアクセスが本来の行列のサイズ(Row/Column)に対して安全かどうかをチェックしていました。3x3行列なのに _44() にアクセスされたら困りますからね。
当時は「まぁいいでしょ……」と思って耐えていましたが、今見るとタイポの温床が大量にある極めて危険な状態です。
しかも、グラフィックスの描画バグが起きた際にこの自作行列を使用していたら、原因究明時に以下のような疑念が次々と湧いてきます。
- アクセサのタイポによる数値のバグではないか?(
_34の中で誤って別の要素を返していないか?など) - 計算式(乗算など)の間違いか?
- 定数バッファ(Constant Buffer)へのデータ転送ミスか?
このように、デバッグ時にチェックしなければいけない容疑者が多すぎて、自作の数学クラスに対してまず怪しむところから入るという
文字通り「何をやってるんだ状態」になっていました。
結果として、作りはしたものの、結局は絶対に信頼できる DirectXMath をラッピングした方を実戦投入し、自作クラスはただの「勉強用の置き物」と化していました(笑)。
この「タイポの恐怖」と「サイズ不一致によるデバッグ地獄」を完全に解消するために今回作成したのが、requires とマクロを融合させたアクセサ自動生成機構です。
requires × マクロ
新実装では、以下のようなアクセサ自動生成マクロを定義しました。
// 個別要素アクセス用マクロ(_11, _12 など) #define MATRIX_ACCESSOR(r, c) \ inline Type& _##r##c() requires (Row >= r && Column >= c) { \ return vec[r-1][c-1]; \ } \ // 行ベクトルアクセス用マクロ(_1, _2 など) #define MATRIX_ROW_ACCESSOR(r) \ inline auto& _##r() requires (Row >= r) { \ return vec[r-1]; \ } \
これらをクラス内で並べるだけで、アクセサの定義が完了します。
// _〇() MATRIX_ROW_ACCESSOR(1); MATRIX_ROW_ACCESSOR(2); MATRIX_ROW_ACCESSOR(3); MATRIX_ROW_ACCESSOR(4); // _1〇() MATRIX_ACCESSOR(1, 1); MATRIX_ACCESSOR(1, 2); MATRIX_ACCESSOR(1, 3); MATRIX_ACCESSOR(1, 4); // ...(2行目、3行目、4行目のマクロも同様に並べる)
これだけで済みます。昔あんなに泥臭くコピペしていた大量の関数が、一瞬で、しかもタイポの余地なく完璧な精度で自動生成されます。
これによって、
1. マクロ化によりタイポが物理的に不可能になり、
2. requires によりサイズ違いの誤用もコンパイル時に100%防げる、
という、自作フレームワークとして絶対の信頼を置ける最強の行列クラスへ生まれ変わりました。これならデバッグ時に行列のバグを疑う必要は一切ありません!
おわりに
C++20のテンプレートと requires、そしてレトロなマクロの力を少しだけ借りることで、コピペコードを一切排除した美しく安全な行列クラスを構築することができました。
……え?「DirectXMath が内部で行っている、SIMD最適化による爆速の行列計算と比べて速度はどうなんだ」って?
(SIMD..速度..うっ頭が..)
き、ききき綺麗に書くのが目的だから....速度は気になった時に最適化すればいいから!!(決していいわけではありませんよ?いいですね)
次回はどうしよう。 このペースで書いていったら今ある分のストックがなくなっちゃう;;
