【C++】前方宣言(Forward Declaration)チートシート(C++20 / VS対応)

The golden light dances across the kitchen C/C++
The golden light dances across the kitchen

目的

  • ビルド時間短縮循環依存の回避ヘッダの安定化
  • 「ヘッダでは“存在だけ”を伝える」「実体は .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 に逃がせないか

よろしければご活用ください。

コメント

タイトルとURLをコピーしました