|
CPP02 1.0
|
EX00では、オブジェクトの基本的な形( Orthodx Canonical Form )でクラス設計(Fixed)の基礎を学びます。
EX01では、Fixedクラスに整数や浮動小数点の値を扱うコンストラクタを追加していきます。さらに、 固定小数点 として扱えるようにして、人間が理解しやすい形に出力できる様にします。(演算子のオーバーロード)
EX02では、Fixedオブジェクト同士の演算を実装します。具体的には演算子のオーバーロード( 比較 :>, <, >=, <=, ==, !=, 四則 :+, -, *, /, インクリメント・デクリメント :++, --)の設計。
EX03では、 BSP (Binary Space Partitioning)という幾何学的アルゴリズム(点が三角形の内側にあるかどうか判定する関数bsp)を実装します。判定するためにベクトル演算(クロス積??)の概念を利用する。
まとめると、Fixedクラスを基本的なオブジェクトの構造から始まり、具体的な数値の扱い、他のオブジェクトの関係性、実用的な問題解決への応用に繋がっていきます。Fixedクラスが進化していく過程を体験します。
Doxygenで作成されたソースコードドキュメントです。
クラスの連携図やソースコードの説明に関する情報がまとめられています。
浮動小数点の課題(速度、精度、決定性)を克服する手法として、固定小数点の仕組みが利用されることがあります。
ちなみに、Cub3dの課題でもレンダリングの処理速度を改善するために固定小数点の仕組みを使うこともあります。(実際に実装しました)
また、C++の標準ライブラリには組み込み用固定小数点の型がないため実装する必要があります。
固定小数点の実装を通じて、固定小数点の利点や欠点を理解し、適切な数値型を選択する判断力を養えるだけでなく、クラス設計の演算子オーバーロードによる直感的な操作ができるオブジェクト設計スキルが得られます。
int型の数値をオブジェクトで扱う。
「標準的な形式」基本的なオブジェクトの形を学びます。
構成
- デフォルトコンストラクタ
- コピーコンストラクタ
- コピー代入演算子
- デストラクタ
オブジェクトのライフサイクルを管理する基盤を築く。
- オブジェクトの誕生→コピー→終了まで
* 固定小数点の概念、表現、計算、2進数、ビットシフト
- コンピューターで固定小数点で値を扱うと、整数・小数・負の値を2進すうで表現できる。
- 数値のビットパターンと、その半分の値(1/2)のビットパターンが同じ。
- 2進数小数点(バイナリポイント)の位置が違うだけ。
より実用的な固定小数点クラスへ改良する
オブジェクトに機能を追加していき、数値表現と操作するためのツールとして進化していく課題。
追加する機能
- 整数や浮動小数点をFixed型として扱う。
- 内部の固定小数点の表現を、人間が理解できる表現に戻す。(整数、浮動小数点など)
- 画面に出力する機能(<< の代入演算子を使って出力)
}int型のコンストラクター :
raw << fractionalBits例:
raw = 10の場合(0d)
10 << 8= (0d)2560(0b)
1010 << 8= (0b)1010 0000 0000Fixed::Fixed(const float raw) : fixedPointValue(static_cast<int>(roundf(raw * (1 << fractionalBits)))) {}float型のコンストラクター :
static_cast<int>(roundf(raw * (1 << fractionalBits)))例:
raw = 42.42fの場合
42.42f * (1 << 8)=42.42f * 256f=10859.52f
roundf(10859.52f)=10860(0d)
10860== (0b)10 1010 0110 1100
roundf():浮動小数点の小数部を丸める。(四捨五入)
toInt(), toFloat()
raw = 10の場合:
fixPointValue(10 << 8)-> (0b)1010 0000 0000== (0d)2560
- **
toInt()**
int toInt(void) constConverts the fixed-point value to an integer value (truncates the fractional part).Definition Fixed.cpp:101
toInt()->2560 >> 8-> (0b)1010== (0d)10- **
toFloat()**
float Fixed::toFloat(void) const { return static_cast<float>(this->fixedPointValue) / (1 << fractionalBits); }float toFloat(void) constConverts the fixed-point value to a floating-point value.Definition Fixed.cpp:92
toFloat()->2560.0f / 256.0f=10.0fを返します。分数
256.0fは(1 << franctionalBits)==1 << 8==256が暗黙的にfloat型にキャストされます。
<<のオーバーロード
o << fixed.toFloat();return o;}std::ostream & operator<<(std::ostream &o, const Fixed &fixed)Overloads the output stream operator to insert the float representation of the fixed-point number int...Definition Fixed.cpp:113
- 出力ストリームの参照 :
std::ostream &o- Fixedオブジェクトの定数参照 :
const Fixed &fixed- 出力ストリームにオブジェクトを出力 :
o << fixed.toFloat();
>>の命令が直接用意されており、コンパイラはその命令をそのまま利用することがあります。ただし、CPUによっては、算術右シフト命令がなく、複数の命令を組み合わせて同等の動作を実現することが必要となり、その方法が異なることがありました。 std::floor() or ゼロ方向へ丸める std::trunc() std::floor()と同等)
std::floor()関数と固定小数点数の動作イメージ 機能:小数部を切り捨てる。(その数よりも小さい整数値を返します。)
-> cppreference.com
* 負の数の例:
std::floor(42.42f) = 42.0f
fixedPointValue(42.42f) -> 10860 -> toInt() -> 42
std::floor(-5.2f) = -6.0f
fixedPointValue(-5.2f) -> -1331 -> toInt() -> -6
固定小数点数の最小分解能を考慮した数値比較
++// 固定小数点数の最小分解能を考慮した許容範囲void fixedEquals(float expected, float actual) {int k = 1;float epsilon = (1.0f / (1 << 8)) * k; // fractionalBits は 8 と仮定ASSERT_NEAR(expected, actual, epsilon);}分母0による除算の挙動
++void checkDivisionByZero(Fixed &a) {// std::cerr の出力をキャプチャするためのstringstreamstd::stringstream ss_cerr;std::streambuf *old_cerr = std::cerr.rdbuf();// std::cerr の内容を元に戻すstd::cerr.rdbuf(old_cerr);// エラーメッセージのチェックstd::string expected_error = "Division by zero is undefined. Returning maximum float.";ASSERT_NE(ss_cerr.str().find(expected_error), std::string::npos)// 返り値が最大 float 値に近いかのチェックfloat max_float = std::numeric_limits<float>::max();// NG// ASSERT_FLOAT_EQ(result.toFloat(), max_float);// より緩やかな比較が必要な場合は ASSERT_NEAR を使用// float epsilon = 1e-6; // 適宜調整// ASSERT_NEAR(result.toFloat(), max_float, epsilon);}T max(T... args)T rdbuf(T... args)T str(T... args)
<<出力の動作確認++TEST(FixedOutputTest, OutputStreamOperator) {Fixed a(123.45f);ss << a;// 123.449 ~= 123.45f ... neary equal// Bad Test// ASSERT_EQ(ss.str(), "123.453125"); // Due to fixed-point precision}T stof(T... args)
間違い
課題要件として、表現可能な最小値(
the smallest representable ε, such that 1 + ε > 1)でインクリメント・デクリメントすることが求められていたのに、わざわざ整数値1相当の値(fixedPoint += (1 << franctionalBits))で計算していたことが判明。正解
fixedPointValue++の実装が正しいです。
間違い
++Fixed Fixed::operator*(const Fixed &other) const { return Fixed(this->toFloat() * other.toFloat()); }Fixed operator*(const Fixed &other) constOverloads the multiplication operator.Definition Fixed.cpp:134NGな理由:固定小数点の利便性を消失している💦
float型の値同士を計算している(時間がかかる+FPUの機能がないコンピュータだとそもそも動作しない)
また、仮数部23bitで扱える範囲を超えると、計算結果に誤差が生じる。
正解
++Fixed result;long long raw_result = (long long)this->fixedPointValue * other.fixedPointValue;result.setRawBits((int)(raw_result >> fractionalBits));return result;}解説
一時的に64bitのデータに計算結果を入れて、固定小数点の重み256をビットシフトで丸めている。
本来の目的(floatを使わず高速な計算を実現化するクラス)を満たしている。
**
fractionalBits = 8の数値が変わった時の影響:**この課題の仕様(
value++方式)を採用した場合、ビット数によって++の「重み」が変わります。
fractionalBits = 8:++を実行すると 1/256 増える。
fractionalBits = 16:++を実行すると 1/65536 増える。影響:精度を高く設定するほど、
++による値の変化はより細かく(小さく)なります。汎用性:数学的な「
+1」を期待するコード(例:forループのカウンタなど)にこのクラスを使うと、意図通りに動かなくなるため注意が必要です。処理速度の違い:
this->fixedPointValue++は、CPUの1命令(INC命令など)で処理されるため、これ以上ないほど高速です。
Files :
* コピー代入演算子の注意点:constのメンバーを持つクラスは、コピー代入演算子をカプセル化するのを避けるか、慎重に検討する方がいい。
![]()
placement newを使うなどあるが、直感的な挙動とは異なる可能性がある。この演習ではコピー代入演算子を使う用途がないため、ダミー用の実装をしています。 (代入せず、*this自分自身を返すだけ)
直接メンバー変数を使って数値の取り出しと代入をしていましたが、setRawBits(), getRawBits()を使うことでより安全かつ可読性の高いコードができる。という意見がありました。
セッター関数およびコピー代入演算子に関する考え。
結論:前者のコードに落ち着きました。
例:
2560.0f/256.0f=10.0f元の浮動小数点数は固定小数点の表記にするためにスケーリング係数を乗じた値が保持される。
スケーリング係数で割ることで、この逆の処理ができる。(スケーリング反転)
スケーリング係数:
1 << fractionalBits==256
浮動小数点数にビットシフトをすると、符号・指数部・仮数部のスケール反転とは異なる操作となり、大きく異なる値になることがある。
基本的には、
x << nは整数に2のn乗を掛ける操作にあたり、浮動小数点数の構造上適していません。
main: 安定版
feature/ex00: 各課題の機能開発を行う。
docs: ここにpushすると、GitHub Actions workflowが動作してドキュメントが公開されます。
C++ Moduleのプロジェクト毎にDoxygenを使ったドキュメントをまとめています。
doxygen用のcssテーマは、Doxygen Awesomeを使用。