目的
- ビルド時間短縮、循環依存の回避、ヘッダの安定化
- 「ヘッダでは“存在だけ”を伝える」「実体は .cpp で知る」が基本方針
まず結論:できる/できない 早見表
| パターン | 前方宣言だけでOK | 補足 |
|---|---|---|
T* / T& をメンバに持つ | ✅ | サイズ不要。class T; で十分 |
const T& を関数引数 | ✅ | 参照の型情報だけで可 |
T を関数戻り値が参照/ポインタ | ✅ | T* / T& の場合 |
std::shared_ptr<T> をメンバに持つ | ✅ | 削除は型非依存(参照カウント) |
std::unique_ptr<T> をメンバに持つ | ❌ 基本NG | デフォルトデリータが delete T を生成→完全型必須(対処は後述) |
T を値メンバに持つ(T obj;) | ❌ | サイズ/レイアウトが必要 |
継承(class D : public T) | ❌ | 基底の完全定義が必要 |
テンプレートで sizeof(T) 等を使う | ❌ | 完全型必須 |
enum class E : int; | ✅ | 基本型付きで宣言可(定義は .cpp で) |
よくあるケース別レシピ
1) unique_ptr<T> の“前方宣言にしたい”問題(C4150 回避)
推奨:ヘッダで #include "T.h" して完全型にする(最もシンプルで安全)
応用:カスタムデリータで逃がす(ヘッダ軽量化が必要な時だけ)
// .h
class T;
struct TDeleter { void operator()(T*) const noexcept; };
std::unique_ptr<T, TDeleter> ptr;
// .cpp
#include "T.h"
void TDeleter::operator()(T* p) const noexcept { delete p; }
VS の 警告 C4150(“delete呼ぶのに定義がない”)はこのケースで出ます。
unique_ptrのデフォルトデリータを使うなら 必ず完全型に。
2) 循環依存の解消
NG
// A.h
#include "B.h"
struct A { B b; };
// B.h
#include "A.h"
struct B { A a; };
OK(前方宣言+ポインタ/参照)
// A.h
class B;
struct A { B* b; };
// B.h
class A;
struct B { A* a; };
3) PImpl(実装隠蔽)パターン(ビルド安定化の王道)
// Widget.h
class Widget {
public:
Widget();
~Widget(); // dtor は .cpp で定義(完全型が見える場所)
Widget(Widget&&) noexcept;
Widget& operator=(Widget&&) noexcept;
private:
struct Impl; // 前方宣言
std::unique_ptr<Impl> p; // OK:Impl はこの翻訳単位の中で完全型になる
};
// Widget.cpp
#include "Widget.h"
struct Widget::Impl { /* 実装詳細 */ };
Widget::~Widget() = default;
4) 実装例
// SceneManager.h
#include "ISceneChangedListener.h"
#include "IBaseScene.h" // ★ unique_ptrで保持するので必須
#include <memory>
// 前方宣言(軽量化)
namespace mm2hack::apps::resources::parameters { class Parameters; }
namespace mm2hack::apps::scenes {
class SceneChangeMediator;
enum class SceneID : int;
}
namespace mm2hack::apps::scenes {
class SceneManager final : public ISceneChangedListener {
using Parameters = resources::parameters::Parameters;
// ...
private:
SceneChangeMediator* _mediator = nullptr; // 前方宣言でOK
std::unique_ptr<IBaseScene> _currentScene; // 完全型が必要⇒include済み
};
}
// SceneManager.cpp
#include "SceneManager.h"
#include "SceneChangeMediator.h"
#include "SceneID.h"
#include "apps/resources/parameters/Parameters.h"
// 実装…
依存最小のための指針
- ヘッダには“宣言だけ”:
class T;、enum class E : int; - .cpp で“定義を知る”:必要な
#includeは .cpp に集中 - 参照/ポインタ/
shared_ptrを使う:値メンバは極力避ける unique_ptrは注意:完全型 or カスタムデリータ- 継承は include:基底は必ず完全型が必要
便利テンプレ(コピペ用)
クラス前方宣言
namespace ns { class Foo; struct Bar; }
enum 前方宣言(C++11+)
namespace ns { enum class Mode : int; }
関数宣言(参照渡しで完全型不要)
namespace ns {
class Foo;
void UseFoo(const Foo&); // Foo の定義不要
}
メンバでの使い分け
class Foo; // 前方宣言
struct Holder {
Foo* p; // OK
// Foo v; // NG(完全型必要)
std::shared_ptr<Foo> sp; // OK
// std::unique_ptr<Foo> up; // NG(デフォルトデリータなら完全型必要)
};
VS 固有のハマりどころ
- C4150: 「delete呼ぶのに定義がない」
→unique_ptr<T>の T が不完全。T を include するか、カスタムデリータを使う。 - C2027: 「不完全型を使った」
→ 値メンバ/継承/sizeof 等で完全型が必要な場面。設計か include を見直す。
チェックリスト(PR前のセルフレビュー)
- [ ] ヘッダに余計な
#includeはないか(参照/ポインタで置き換えられないか) - [ ]
unique_ptr<T>を持つヘッダは T を include しているか - [ ] 循環依存を前方宣言+ポインタに直せるか
- [ ] enum は
enum class E : int;で宣言できないか - [ ] PImpl で重い依存を .cpp に逃がせないか
よろしければご活用ください。

コメント