スマートポインタ¶
一言で言うと¶
「deleteを人間が書かない」ためのRAIIラッパ。所有権の形ごとに3種類あります。
std::unique_ptr<T>: 所有者は常に1人。コピー不可・ムーブで譲渡。既定の選択肢std::shared_ptr<T>: 所有者が複数(参照カウント)。最後の1人が破棄で解放std::weak_ptr<T>: shared_ptrの中身を「所有せずに見る」。使う時に生存確認
解決したい問題¶
// C++20(問題例)— 生のnew/delete
Enemy* SpawnBoss() { return new Enemy(); }
void Battle() {
Enemy* boss = SpawnBoss();
if (PlayerRanAway()) return; // deleteし忘れ → リーク
delete boss;
}
// さらに: 誰がdeleteすべきか型から分からない / 二重delete / delete後アクセス(UAF)
生ポインタは「見ているだけ」なのか「解放責任があるのか」を型で区別できない——これが諸悪の根源です(→ 所有権)。
unique_ptr(既定)¶
// C++20
#include <memory>
auto boss = std::make_unique<Enemy>(); // 生成(newを直接書かない)
boss->TakeDamage(10); // 生ポインタと同じ使い心地(operator->)
// コピー不可 = 所有者が2人になる事故がコンパイルエラーになる
// auto copy = boss; // エラー
auto newOwner = std::move(boss); // ムーブ=所有権の譲渡(bossは空に)
std::vector<std::unique_ptr<Enemy>> enemies; // 多態コンテナの定石(スライシング回避)
enemies.push_back(std::make_unique<BossEnemy>());
- コストほぼゼロ: サイズは生ポインタと同じ(カスタムデリータなしの場合)、操作もほぼ同じ機械語。使わない理由がない
- スコープ終了・所有者の破棄で自動delete(RAII)
- 関数が
unique_ptr<T>を返す=「所有権をあげる」、受け取る=「所有権をもらう」——シグネチャが所有権のドキュメントになる
shared_ptr(共有が本当に必要なときだけ)¶
auto texture = std::make_shared<Texture>("boss.png");
auto a = texture; // 参照カウント2(コピーで共有)
a.reset(); // カウント1
// 最後の所有者が消えた瞬間に解放
- コストあり: 制御ブロック(カウンタ)の確保、カウント操作はスレッド安全のためアトミック(コピーのたびに同期コスト)、サイズは生ポインタの2倍
- 循環参照で解放されない: AがBを、BがAをshared_ptrで持つと、カウントが0にならず両方リーク。GCと違い循環を検出しない——これがC#経験者の最重要注意点
weak_ptr(循環切りと「消えたかもしれない相手」)¶
class Enemy {
std::weak_ptr<Player> target_; // 所有しない(Playerの寿命を延ばさない)
public:
void Update() {
if (auto p = target_.lock()) { // 生存確認+一時的なshared_ptr化
ChaseTowards(p->Position());
} else {
// プレイヤーは既に消えていた(dangling にならない!)
}
}
};
- 「相手が先に死んでいるかもしれない参照」を安全に表現(生ポインタのdangling問題への型レベルの回答)
- 親子の循環は「親→子: shared(またはunique)、子→親: weak(または生ポインタ)」で切る
使い分けフローチャート¶
所有する?
├ No → 生ポインタ or 参照(「見ているだけ」。相手の寿命が保証される文脈で)
│ 相手が先に消えうる → weak_ptr(相手がshared管理の場合)/ ハンドルID方式
└ Yes → 所有者は1人で足りる?
├ Yes → unique_ptr(まずこれ。9割はこれで足りる)
└ No(本当に複数?) → shared_ptr(+循環にはweak_ptr)
「迷ったらshared_ptr」は間違い。所有者が曖昧になり、「いつ解放されるか誰も分からない」設計になります。uniqueで設計を試み、破綻したときだけsharedへ(→ 判断ガイド)。
非所有の生ポインタは「悪」ではない¶
class Scene {
std::vector<std::unique_ptr<Enemy>> enemies_; // Sceneが所有
};
void UpdateAi(Enemy* target); // 引数の生ポインタ=「借りて見るだけ」の合図(現代C++の慣習)
所有をスマートポインタで表現しきれば、残った生ポインタは自動的に「非所有」を意味する——この規約が現代C++の secret です。
C#との違い¶
| C#(GC) | C++(スマートポインタ) | |
|---|---|---|
| 解放時期 | GC任せ(不定) | 決定的(最後の所有者の破棄の瞬間) |
| 循環参照 | GCが回収できる | shared_ptrは回収できない(weak_ptrで切る) |
| 「消えた相手」 | 参照があれば生き続ける(そもそも消えない) | weak_ptr/ハンドルで生存確認 |
| コスト | GCの一時停止・メモリ圧 | カウント操作(shared)、ほぼゼロ(unique) |
C#では「参照を持つ=生かし続ける」ですが、C++のunique_ptr世界では「所有者が破棄=即死」。「まだ使ってる人がいるのに消える」事故はC++側で起き、「使い終わったのに生き続ける」事故はC#側で起きる、と対で覚えると整理できます。
Unity開発者が誤解しやすい点¶
Destroy(gameObject)の感覚(マークして後で消える、参照はnull的になる)に一番近いのはweak_ptr+shared_ptrやハンドル方式。unique_ptrのdeleteは即時で、残った生ポインタはnullにならない(danglingになる)- 「とりあえずshared_ptrはGCの代わり」— 循環を回収しない点で決定的に違う
- UEには独自のスマートポインタ群(TUniquePtr/TSharedPtr)とUObject用のGCがあり、標準のshared_ptrをUObjectに使ってはいけない(→ 第10部)
ゲーム開発での使用例¶
- シーン→エンティティ:
vector<unique_ptr<Entity>>(所有の階層) - 共有アセット(テクスチャ、サウンドデータ):
shared_ptr(または参照カウント式アセットマネージャ) - ターゲット参照・親参照:
weak_ptrか 世代付きハンドル(→ Object Pool。大量オブジェクトではハンドルの方が高速)
使う場面 / 使わない場面¶
- unique_ptr: ヒープ所有の既定。まず値で持てないか考えてから(メンバに直接置ければポインタ自体不要)
- shared_ptr: 寿命が本当に複数の所有者に依存するものだけ(アセット等)
- 使わない場面: 毎フレーム大量に生成・破棄するもの(カウント操作が重い→ Object Pool)。C API境界。UObject(UE)
よくある誤解¶
- 「モダンC++=生ポインタ禁止」— 禁止されるのは所有する生ポインタ。非所有の生ポインタ・参照は現役
- 「make_uniqueはnewの飾り」— 例外安全と「newを検索して0件にできる」規約的価値がある
関連項目¶
理解度チェック¶
- unique_ptrのコピーがコンパイルエラーになるのは、何の事故を型で防いでいるからですか。
- shared_ptrの循環参照はなぜ解放されないのですか。C#との違いは?
- 「非所有の生ポインタ」が現代C++で許される条件は?
演習¶
samples/smart_pointers.cpp(検証済み)で、(a) unique_ptrのムーブによる所有権移転、(b) shared_ptrのカウント変化、(c) 循環参照でデストラクタが呼ばれないこと、(d) weak_ptrで循環を切ると解放されること、を出力で確認してください。
前: キャスト | カテゴリ目次 | 次: STLコンテナ・イテレータ・アルゴリズム・ranges