記憶域期間とスコープ・寿命の違い¶
一言で言うと¶
C++のオブジェクトには「いつ生まれ、いつ死ぬか」の分類=記憶域期間 (storage duration) が3+1種類あります。そしてスコープ(名前が見える範囲)と寿命(実体が生きている期間)は別物——この区別が曖昧だとdangling系のバグを理解できません。
4つの記憶域期間¶
| 期間 | 生成〜破棄 | 置き場所(典型) | 例 |
|---|---|---|---|
| 自動 (automatic) | スコープ開始〜終了 | スタック | ローカル変数、引数 |
| 静的 (static) | プログラム開始前後〜終了 | .data/.bss | グローバル変数、static変数 |
| 動的 (dynamic) | new〜delete(自分で決める) | ヒープ | new/make_uniqueしたもの |
| スレッド (thread_local) | スレッド開始〜終了 | スレッドごとの領域 | thread_local変数 |
// C++20
int g_frameCount = 0; // 静的: main前に0初期化、プログラム終了まで生きる
void Update() {
float dt = 0.016f; // 自動: この関数の間だけ
static int callCount = 0; // 静的: 初回呼び出しで初期化、以後ずっと生きる
auto e = std::make_unique<Enemy>(); // 動的: eの破棄と共にdelete(RAIIで自動に紐付け)
} // dt消滅、eのデストラクタ→Enemyもdelete
スコープと寿命は別物¶
Enemy* Make() {
Enemy local; // 寿命: この関数の間だけ
return &local; // スコープを超えてアドレスだけ持ち出す
} // localは死ぬ
Enemy* p = Make(); // pは死体を指している(dangling)。使えばUB
- スコープはコンパイル時の概念(名前がどこで見えるか)
- 寿命は実行時の概念(実体がいつまで有効か)
- ポインタ・参照・ラムダの参照キャプチャ・string_view・イテレータは、名前を介さずに寿命切れの実体へ到達する穴を作れる——これがC++のメモリバグの構図そのもの(→ メモリバグ図鑑)
寿命が延びる/延びない例¶
const std::string& r = GetName(); // 戻り値(一時オブジェクト)をconst参照で受けると
// 一時の寿命がrに合わせて延長される(特別規則)
std::string_view sv = GetName(); // 延長されない! 一時は文末で死に、svはdangling
寿命延長の規則は限定的で、「const参照に直接束縛したときだけ」。規則を暗記するより「一時オブジェクトを参照系の型で保存しない」を習慣にする方が安全です。
静的記憶域の罠: 初期化順問題 (static initialization order fiasco)¶
別々の翻訳単位にあるグローバル変数の初期化順は未規定です。
// audio.cpp
AudioSystem g_audio; // これと
// game.cpp
Game g_game{ g_audio.Volume() }; // これのどちらが先か、保証がない → 未初期化のg_audioを触るかも
対策:
- グローバル変数同士を依存させない(そもそもグローバルを減らす → Singletonの問題と同根)
- 関数内static(Meyers' Singleton)にする: 初回アクセス時初期化なので順序が使用順になる
- 明示的な
Startup()/Shutdown()をmainから呼ぶ(エンジンの定石)
破棄順も同様に危険(終了時クラッシュの一大原因): staticなオブジェクトのデストラクタが他のstaticを触ると事故ります。
C#との対応¶
- C#では「参照が残っていれば生きている」(寿命をGCが管理)ため、寿命切れの実体を指すという状況が原理的にない(danglingがない世界)。代わりに「いつ死ぬか分からない」(非決定的破棄)世界
- C#のstaticフィールドの初期化順は型の静的コンストラクタで規定されており、C++の翻訳単位間フィアスコに相当する問題は薄い
- スタックとヒープの区別: C#では型(struct/class)でほぼ決まるが、C++では同じ型をどこにでも置ける(→ 値と参照)
Unity開発者が誤解しやすい点¶
- 「ローカル変数を返しても大丈夫」— C#では常に安全(値はコピー、参照はGCが延命)。C++では参照・ポインタで返した瞬間にUB予備軍
- 「staticはシーンをまたいで残る便利な置き場」— C++のstaticは初期化・破棄順という別の危険を持つ
Awake/OnDestroyのようなエンジン管理の寿命と、C++の言語寿命は別レイヤー。UEではさらにGC寿命が重なる(→ 第10部)
ゲーム開発での使用例¶
- フレーム内だけ有効なデータ(当たり判定結果リスト)は自動記憶域 or フレームアロケータ(→ arena)
- 「シーンの寿命」「ゲーム全体の寿命」を型で表す(SceneContext、GameContextが所有 → 所有権)
- エンジンのサブシステム初期化・終了は明示的Startup/Shutdown方式が定石
理解度チェック¶
- 4つの記憶域期間と、それぞれの生成・破棄タイミングを言えますか。
- スコープと寿命の違いを、danglingの例で説明できますか。
- static initialization order fiascoとは何で、対策は?
演習¶
samples/lifetime_demo.cpp(検証済み)で、(a) デストラクタの実行順(自動変数は逆順)、(b) 関数内staticの初期化タイミング、を出力で確認してください。ローカル変数のアドレスを返す関数はコメントで「なぜ危険か」を読むだけにしてください(実行しない)。