第6部 演習 — C++の内部的な仕組み¶
問1. 概念確認¶
次のエラー・現象は、ビルドパイプラインのどの段階の問題ですか(旅の地図の①〜⑦で答え、理由を一言)。
error: 'Enemy' was not declared in this scopeundefined reference to 'Enemy::TakeDamage(int)'multiple definition of 'g_config'- 起動時に「MSVCP140.dll が見つかりません」
- main()の最初の行にブレークポイントを張ったのに、その前にクラッシュする
解答
1. ②コンパイル(宣言が翻訳単位に見えていない → includeか前方宣言不足)。2. ④リンク(定義を持つ.o/ライブラリが未リンク)。3. ④リンク(ODR違反 — ヘッダーで変数を定義した等)。4. ⑤ローダー(動的ライブラリの解決失敗)。5. ⑥静的初期化(グローバルオブジェクトのコンストラクタ内クラッシュ)。問2. コード読解¶
// widget.h
#pragma once
class Renderer; // (A)
class Widget {
public:
void Draw(Renderer& r); // (B)
private:
Renderer* renderer_; // (C)
// Renderer fallback_; // (D) ← コメントを外すとエラー
};
(A)の宣言だけで(B)(C)が成立し、(D)が失敗する理由を、コンパイラが必要とする情報の観点で説明してください。
解答
(A)は前方宣言で「Rendererという型が存在する」ことだけ伝える。(B)(C)はポインタ・参照であり、**サイズが8バイト(ポインタ)と分かれば足りる**——中身を知る必要がない。(D)は実体をメンバに置くため、Widgetのサイズ計算にRendererの**完全な定義(サイズ・レイアウト)**が必要 → includeが要る([翻訳単位とヘッダー](02_translation_units_and_headers.md))。問3. 問題のあるコードの改善¶
「utils.h に int Random(int max) { ... } と書いたら、2つ目の.cppで使った途端リンクエラーになった」。原因と修正方法を3通り挙げ、それぞれの使い分けを書いてください。
解答
原因: 非inline関数の定義がヘッダー経由で2翻訳単位に入り、[ODR](02_translation_units_and_headers.md)違反(多重定義)。修正: (1) `inline` を付ける — 小さくてヘッダーに置きたい関数。(2) 宣言だけ残し定義をutils.cppへ — 大きい関数・変更頻度が高い関数(再コンパイル範囲を狭める)。(3) `static`/無名namespace — 翻訳単位ごとに別実体でよい場合のみ(状態を持つと単位ごとに別になる罠あり)。問4. 設計比較¶
10万発の弾を毎フレーム更新します。(a) std::vector<Bullet*>(各弾はnew)、(b) std::vector<Bullet>、(c) SoA(座標配列と速度配列を分離)の3案を、キャッシュライン・プリフェッチ・SIMDの観点で比較してください。
解答の要点
(a) ポインタ追跡で毎要素キャッシュミスの可能性、プリフェッチ効かず、SIMD不可 — 最悪。(b) 連続配置でライン単位に載る+順走査プリフェッチ有効。Bulletが大きいと1ラインの個体数が減る。(c) 更新で使うデータだけが隙間なく並び、ライン効率最大+自動ベクトル化の条件が揃う — 大量処理の理想形。ただし(c)はコードの素直さを失うので、10万発という規模(計測)が正当化の根拠([キャッシュ](10_cache_branch_simd.md)、[AoS/SoA](../07_memory_and_runtime/05_cache_locality_aos_soa.md))。問5. デバッグ問題¶
「Releaseビルドでのみ、この関数の戻り値がおかしい」:
原因、Debugで動いてしまう理由、恒久対策(コード+ツール)を答えてください。
解答
原因: `total` 未初期化のまま加算(UB)。Debugでは(処理系によっては)スタックが0埋めされ偶然正しく見える。Releaseではレジスタのゴミ値から開始([UBと最適化](11_optimization_and_ub.md))。対策: `int total = 0;`(コード)、`-Wall -Wextra`(-Wuninitializedが警告)、UBSan/MSanの常用([UB](../05_cpp_language/18_undefined_behavior.md))。問6. 説明問題¶
「仮想関数のコストは間接呼び出しそのものより、インライン化の阻害だ」と言われる理由を、vtableと最適化の知識で3〜4文で説明してください。
解答例
間接call自体は数サイクルで、予測が当たれば安い。しかし呼び先が実行時まで不明だと、コンパイラは本体を展開(インライン化)できない。インライン化は呼び出し除去そのものより、展開後に起きる定数伝播・デッドコード除去・ベクトル化などの**連鎖最適化の入口**であり、それが全部止まる。だからホットループでは仮想呼び出し1個の数サイクルではなく、「最適化されなかったループ全体」の差として現れる。問7. 小規模実装問題¶
samples/tu_demo/ を出発点に、次を実施してください。
- 新しいクラス
Spawner(enemy.hに依存)をヘッダー+cppで追加し、3ファイル構成でビルドする nm(またはobjdump)で spawner.o の「提供シンボル」「未解決シンボル」を確認する- enemy.h のインクルードを前方宣言に置き換えられるか検討し、できる/できない理由をメンバの持ち方から説明する
評価基準
(1) ODR違反なくビルドが通る。(2) 未解決シンボル(U表示)がEnemyのメンバ関数であることを確認できる。(3) 「実体メンバなら定義が必要、ポインタ・参照なら前方宣言で足りる」を自分の構成で説明できる([翻訳単位](02_translation_units_and_headers.md)、[シンボル](03_symbols_and_linkage.md))。前: コンパイラ最適化とUB | カテゴリ目次 | 第7部: メモリと実行時へ