第1章:演算子オーバーロードと固定小数点数
はじめに
CPP Module 02では、C++のAd-hocポリモーフィズム(演算子オーバーロード)と固定小数点数を学びます。これらはC++の型システムを拡張し、ユーザー定義型を組み込み型のように扱うための重要な機能です。
---
1. Orthodox Canonical Form
1.1 歴史的背景
C++の初期(1983-1985年)、Bjarne Stroustrupは「C with Classes」から完全なオブジェクト指向言語へと発展させる中で、オブジェクトのライフサイクル管理という問題に直面しました。
C言語では構造体のコピーは単純なビットコピーでしたが、C++ではリソース(メモリ、ファイルハンドルなど)を管理するオブジェクトが登場し、深いコピーと浅いコピーの区別が必要になりました。
この問題に対する解決策として、Orthodox Canonical Form(正準形式)が確立されました。
1.2 Orthodox Canonical Formとは
Orthodox Canonical Form(OCF)は、クラスが正しく動作するために必要な4つの特殊メンバ関数を定義する規約です:
class Sample {
public:
// 1. デフォルトコンストラクタ
Sample();
// 2. コピーコンストラクタ
Sample(const Sample& other);
// 3. コピー代入演算子
Sample& operator=(const Sample& other);
// 4. デストラクタ
~Sample();
};
1.3 なぜ4つ必要か
デフォルトコンストラクタ:
- オブジェクトを初期化する
- 配列の生成や一部のコンテナで必要
Sample::Sample() : _value(0) {
std::cout << "Default constructor called" << std::endl;
}
コピーコンストラクタ:
- 既存オブジェクトから新しいオブジェクトを作成
- 値渡しの引数、関数の戻り値で使用
Sample::Sample(const Sample& other) : _value(other._value) {
std::cout << "Copy constructor called" << std::endl;
}
コピー代入演算子:
- 既存オブジェクトに別のオブジェクトの値を代入
- 自己代入のチェックが重要
Sample& Sample::operator=(const Sample& other) {
std::cout << "Copy assignment operator called" << std::endl;
if (this != &other) {
_value = other._value;
}
return *this;
}
デストラクタ:
- オブジェクトの破棄時にリソースを解放
- RAII(Resource Acquisition Is Initialization)パターンの基盤
Sample::~Sample() {
std::cout << "Destructor called" << std::endl;
}
1.4 Rule of Three
C++03では「Rule of Three」として知られていました:
> コピーコンストラクタ、コピー代入演算子、デストラクタのいずれか1つを定義する場合、3つすべてを定義すべき。
これは、これらのいずれかをカスタマイズする必要がある場合、そのクラスはリソース管理を行っており、すべてのケースで正しく動作する必要があるためです。
1.5 深いコピーと浅いコピー
浅いコピー(Shallow Copy):
class Shallow {
int* ptr;
public:
Shallow() : ptr(new int(42)) {}
// デフォルトのコピー:ポインタの値だけコピー
// ptr は同じメモリを指す → 二重解放の危険
};
深いコピー(Deep Copy):
class Deep {
int* ptr;
public:
Deep() : ptr(new int(42)) {}
Deep(const Deep& other) : ptr(new int(*other.ptr)) {}
// 新しいメモリを確保し、値をコピー
~Deep() { delete ptr; }
};
---
2. 演算子オーバーロード
2.1 Ad-hocポリモーフィズム
Christopher Strachey(1967)による分類では、ポリモーフィズムは2種類に分けられます:
- Parametric polymorphism(パラメトリック多相性): テンプレート
- Ad-hoc polymorphism(アドホック多相性): オーバーロード
演算子オーバーロードはAd-hocポリモーフィズムの一形態であり、同じ演算子が異なる型に対して異なる動作をすることを可能にします。
2.2 演算子オーバーロードの歴史
ALGOL 68(1968)は最初に演算子オーバーロードを導入した言語の1つでした。C++はこの概念を取り入れ、ユーザー定義型を「第一級市民」として扱えるようにしました。
Bjarne Stroustrupの設計哲学: > 「ユーザー定義型は組み込み型と同等の権利を持つべきである」
2.3 オーバーロード可能な演算子
| カテゴリ | 演算子 |
|---------|--------|
| 算術 | + - / % |
| 比較 | == != < > <= >= |
| 論理 | && \|\| ! |
| ビット | & \| ^ ~ << >> |
| 代入 | = += -= = /= |
| インクリメント/デクリメント | ++ -- |
| その他 | [] () -> , |
オーバーロード不可能:
::(スコープ解決).(メンバアクセス).(メンバポインタアクセス)?:(三項演算子)sizeoftypeid
2.4 メンバ関数 vs 非メンバ関数
メンバ関数として:
class Fixed {
public:
Fixed operator+(const Fixed& rhs) const {
return Fixed(this->_value + rhs._value);
}
};
// 使用
Fixed a, b;
Fixed c = a + b; // a.operator+(b)
非メンバ関数として:
Fixed operator+(const Fixed& lhs, const Fixed& rhs) {
return Fixed(lhs.getValue() + rhs.getValue());
}
// 使用
Fixed c = a + b; // operator+(a, b)
使い分けの指針:
=,[],(),->: メンバ関数として- 対称的な演算子(
+,-など): 非メンバ関数が好ましい - 左オペランドが他のクラスの場合: 非メンバ関数必須
2.5 比較演算子
class Fixed {
public:
bool operator>(const Fixed& rhs) const {
return this->_value > rhs._value;
}
bool operator<(const Fixed& rhs) const {
return this->_value < rhs._value;
}
bool operator>=(const Fixed& rhs) const {
return !(*this < rhs);
}
bool operator<=(const Fixed& rhs) const {
return !(*this > rhs);
}
bool operator==(const Fixed& rhs) const {
return this->_value == rhs._value;
}
bool operator!=(const Fixed& rhs) const {
return !(*this == rhs);
}
};
2.6 インクリメント/デクリメント演算子
前置と後置を区別するため、後置にはダミー引数(int)を使用します:
class Fixed {
public:
// 前置インクリメント: ++a
Fixed& operator++() {
++this->_value;
return *this;
}
// 後置インクリメント: a++
Fixed operator++(int) {
Fixed temp(*this);
++(*this);
return temp;
}
};
注意: 後置は古い値を返すため、コピーが必要です。パフォーマンスを考慮すると、前置の方が効率的です。
2.7 出力ストリーム演算子
std::ostream& operator<<(std::ostream& os, const Fixed& fixed) {
os << fixed.toFloat();
return os;
}
この演算子は非メンバ関数として定義する必要があります(左オペランドがstd::ostreamのため)。
---
3. 固定小数点数
3.1 コンピュータにおける数値表現の歴史
初期のコンピュータ(1940-1950年代)では、すべての数値計算は整数または固定小数点数で行われていました。浮動小数点数(IEEE 754)が標準化されたのは1985年のことです。
固定小数点数は、浮動小数点演算ユニット(FPU)を持たないシステムや、精度が重要な金融計算で今でも使用されています。
3.2 固定小数点数とは
固定小数点数は、小数点の位置が固定された数値表現です。整数部と小数部のビット数が固定されています。
例:8.8固定小数点数(16ビット):
- 上位8ビット: 整数部
- 下位8ビット: 小数部
値: 3.5
整数部: 3 = 0000 0011
小数部: 0.5 = 1000 0000 (= 128/256)
表現: 0000 0011 1000 0000 = 0x0380 = 896
3.3 浮動小数点数との比較
| 観点 | 固定小数点数 | 浮動小数点数 | |------|-------------|-------------| | 精度 | 均一(全範囲で同じ) | 可変(値が大きいほど粗い) | | 範囲 | 狭い | 広い | | 演算速度 | 高速(整数演算) | 比較的遅い | | 丸め誤差 | 予測可能 | 複雑 | | 実装 | FPU不要 | FPU必要 |
3.4 固定小数点数の演算
加算/減算:
// 直接加算可能(小数点位置が同じ)
fixed_a + fixed_b
乗算:
// 結果の小数ビット数が2倍になるため、シフトで調整
(fixed_a * fixed_b) >> fractional_bits
除算:
// 被除数をシフトしてから除算
(fixed_a << fractional_bits) / fixed_b
3.5 42の課題での固定小数点数
42のCPP Module 02では、8ビットの小数部を持つ固定小数点数クラスを実装します:
class Fixed {
private:
int _rawBits;
static const int _fractionalBits = 8;
public:
Fixed();
Fixed(const int n);
Fixed(const float n);
Fixed(const Fixed& other);
Fixed& operator=(const Fixed& other);
~Fixed();
int getRawBits() const;
void setRawBits(int const raw);
float toFloat() const;
int toInt() const;
};
3.6 変換の実装
整数から固定小数点数:
Fixed::Fixed(const int n) : _rawBits(n << _fractionalBits) {
std::cout << "Int constructor called" << std::endl;
}
浮動小数点数から固定小数点数:
Fixed::Fixed(const float n) : _rawBits(roundf(n * (1 << _fractionalBits))) {
std::cout << "Float constructor called" << std::endl;
}
固定小数点数から浮動小数点数:
float Fixed::toFloat() const {
return static_cast<float>(_rawBits) / (1 << _fractionalBits);
}
固定小数点数から整数:
int Fixed::toInt() const {
return _rawBits >> _fractionalBits;
}
---
4. 静的メンバ関数
4.1 静的メンバ関数とは
静的メンバ関数はインスタンスなしで呼び出せ、thisポインタを持ちません。
class Fixed {
public:
static Fixed& min(Fixed& a, Fixed& b);
static const Fixed& min(const Fixed& a, const Fixed& b);
static Fixed& max(Fixed& a, Fixed& b);
static const Fixed& max(const Fixed& a, const Fixed& b);
};
4.2 const版と非const版
Fixed& Fixed::min(Fixed& a, Fixed& b) {
return (a < b) ? a : b;
}
const Fixed& Fixed::min(const Fixed& a, const Fixed& b) {
return (a < b) ? a : b;
}
オーバーロードにより、const引数とnon-const引数の両方に対応します。
---
5. Binary Space Partitioning(BSP)
5.1 BSPとは
Binary Space Partitioning(二分空間分割)は、空間を再帰的に2つの半空間に分割するデータ構造です。1969年にSchumackerらによって開発され、1980年代のゲーム(Doom等)で広く使用されました。
5.2 点が三角形内にあるかの判定
BSPの基本的な応用として、点が三角形内にあるかを判定するアルゴリズムがあります。
外積を使用した方法:
三角形ABCと点Pについて:
- ベクトルAB × APの符号を計算
- ベクトルBC × BPの符号を計算
- ベクトルCA × CPの符号を計算
- 3つの符号がすべて同じなら、点は三角形内
float sign(Point p1, Point p2, Point p3) {
return (p1.getX() - p3.getX()) * (p2.getY() - p3.getY())
- (p2.getX() - p3.getX()) * (p1.getY() - p3.getY());
}
bool bsp(Point const a, Point const b, Point const c, Point const point) {
float d1 = sign(point, a, b);
float d2 = sign(point, b, c);
float d3 = sign(point, c, a);
bool has_neg = (d1 < 0) || (d2 < 0) || (d3 < 0);
bool has_pos = (d1 > 0) || (d2 > 0) || (d3 > 0);
return !(has_neg && has_pos);
}
---
まとめ
本章で学んだこと:
次章では、各演習の詳細な実装を解説します。