Skip to content

第11回 参照・ポインタ・寿命

前回: 第10回 音と演出 / 次回: 第12回 独自要素の追加

今回の授業内容

  • 完成に近づいたコードの所有関係を整理する
  • オブジェクトプールにおける「C++上の寿命」と「有効フラグによる論理的な寿命」の違いを理解する
  • 値渡しと参照渡しの違いを確認する
  • const参照を使う理由を説明する
  • 不要なポインタや不要なnullptrチェックを増やさない設計を確認する

今回の開始状態

第10回終了時点では、ゲームとして必要な主要機能がそろっています
今回の終了時点では、新機能を大きく増やさず、コードの受け渡しと寿命を点検します

この回は「完成版を読む」回ではありません
自分がここまで実装したコードを対象に、どのオブジェクトを誰が持っているかを整理する回です

変更するファイル

ファイル変更内容
Player.h / Bullet.h / Enemy.hDraw()などの引数が適切か確認する
Game.h所有しているメンバ変数を確認する
Game.cpp参照で渡すべき箇所、値で十分な箇所を確認する

所有しているものを確認する

Gameは、ゲーム全体で存在し続けるものを、ポインタではなくメンバ変数として直接所有します

cpp
Player player;
ImageManager images;
SoundManager sounds;
std::array<Bullet, maxBullets> bullets = {};
std::array<Enemy, maxEnemies> enemies = {};
std::array<Effect, maxEffects> effects = {};

PlayerBulletは、画像管理や音管理を所有しません
描画や再生に必要な間だけ、Gameから借りて使います
「誰が生成し、誰が保持し続け、誰が一時的に借りるだけか」を区別することが、この回のテーマです

オブジェクトプールの寿命を考え直す

第05回で、弾をオブジェクトプールとして管理する理由を「newdeleteを繰り返す負荷とリスクを避けるため」と説明しました
第11回では、この設計を寿命(オブジェクトが生成されてから破棄されるまでの期間)の観点から見直します

std::array<Bullet, maxBullets> bulletsGameのメンバ変数として宣言された時点で、48個のBulletはすべて一度に構築されます
これらのBulletは、Gameが破棄されるまで、C++のオブジェクトとして存在し続けます
「弾が発射される」「弾が画面外で消える」という現象は、実際には1つもBulletを生成・破棄していません
activeというbooltruefalseに切り替えているだけです

つまり、この授業でいう「弾の寿命」には2つの層があります

内容決まるタイミング
C++オブジェクトとしての寿命Bulletが生成されてから破棄されるまでGameが生成・破棄されるとき
論理的な寿命(プール上の使用期間)activetrueになってからfalseに戻るまでSpawn()から画面外判定まで

newdeleteを使う設計では、この2つの寿命が一致します
オブジェクトプールでは、C++の寿命は最初から最後まで変わらないまま、論理的な寿命だけが何度も繰り返されます
この違いを理解しておくと、「なぜBulletのコンストラクタではなくSpawn()で初期化するのか」「なぜデストラクタで後始末をする必要がないのか」を説明できるようになります

const参照で借りて使う

画像管理はコピーするものではありません
描画時に中身を書き換える必要もないため、const参照で受け取ります

cpp
void Player::Draw(const ImageManager& images) const;
void Bullet::Draw(const ImageManager& images) const;
void Enemy::Draw(const ImageManager& images) const;
void Player::Update(const Input& input);

この形にすると、呼び出された側はimagesinputを読み取り専用で使うことが明確になります
仮に値渡し(ImageManager imagesのように参照を外した形)にすると、呼び出しのたびにImageManagerが持つ画像ハンドルの配列がコピーされます
今回程度の要素数では実行速度への影響は小さいものの、「値をコピーして渡す」という書き方は、本来共有すべき1つのデータを複数持っているかのような誤解を生みます
Gameが唯一のImageManagerを持ち、他のクラスはそれを借りて読むだけ、という所有関係を、const参照という書き方でコードの上にも表しています

不要なポインタを増やさない

このプロジェクトでは、主要なオブジェクトはGameが直接所有しています
そのため、所有関係が明確なものを無理にポインタへ変える必要はありません

避けたい例は、理由なく次のようにすることです

cpp
Player* player = nullptr;

nullptrになり得る設計でないなら、ポインタにする必要はありません
不安だから確認するのではなく、誰が所有し、いつまで存在するかを設計で決めます

DXライブラリのハンドルのように、読み込み失敗時の値が-1と決まっているものは確認します
これは不要な防御ではなく、素材がない状態でも動かすための仕様です
「値がnullptr-1になり得るかどうか」を型やコメントではなく、確認の有無そのもので語れるようにしておくことが、読みやすいコードにつながります

実習

  1. Gameが所有しているメンバ変数を書き出す
  2. 各クラスが関数の引数として借りているものを書き出す
  3. Draw()の引数がconst参照になっているか確認する
  4. 弾・敵・エフェクトについて、C++オブジェクトとしての寿命と、activeによる論理的な寿命の違いを自分の言葉で説明する
  5. 値渡しに変更すると何が起きるかを説明する
  6. 不要なポインタや不要なnullptrチェックを追加していないか確認する

今回の完成条件

  • 所有と利用の違いを説明できる
  • オブジェクトプールにおける2種類の寿命の違いを説明できる
  • const参照を使う理由を説明できる
  • 不要なポインタを増やさない判断ができる
  • 自分のコードの所有関係を表で説明できる

まとめ

今回は、完成に近づいたコードを安全に扱うため、所有、参照、寿命を整理しました
次回は、この構造を崩さない範囲で独自要素を追加します