C++の前方宣言とは、クラスや構造体、関数、列挙型などについて、「この名前の型や関数が存在します」と先にコンパイラへ知らせるための宣言です。
前方宣言では、クラスの中身やメンバ変数、メンバ関数の内容までは定義しません。
あくまで、名前の存在だけを知らせます。
たとえば、あるクラスを使いたいけれど、そのクラスの詳しい中身まではまだ必要ない場合に、前方宣言を使うことがあります。
C++では、基本的に何かを使う前に、その名前が宣言されている必要があります。
そのため、あるクラスを別のクラスの中で参照したい場合などに、前方宣言が役立ちます。
前方宣言の基本的な考え方
前方宣言は「名前だけ」を知らせる仕組み
前方宣言で分かるのは、あくまで「その名前の型が存在する」ということだけです。
そのため、コンパイラは前方宣言を見ても、次のような情報は分かりません。
- そのクラスにどんなメンバ変数があるのか
- そのクラスにどんなメンバ関数があるのか
- そのクラスのサイズが何バイトなのか
- そのクラスがどのクラスを継承しているのか
- そのクラスのコンストラクタやデストラクタがどうなっているのか
つまり、前方宣言された型は、まだ中身が分からない不完全型として扱われます。
完全な定義が必要になる場面もある
前方宣言だけで済む場面もありますが、クラスの中身やサイズが必要になる場面では、完全な定義が必要です。
たとえば、あるクラスのオブジェクトを実際に作る場合、そのクラスのサイズが分かっていなければなりません。
サイズが分からないと、メモリ上にどれだけの領域を確保すればよいか判断できないからです。
また、メンバ関数を呼び出す場合も、そのクラスに本当にその関数が存在するかをコンパイラが知っている必要があります。
そのため、前方宣言は万能ではなく、使える場面と使えない場面を理解しておくことが重要です。
前方宣言でできること
ポインタとして扱う
前方宣言された型は、ポインタとして扱うことができます。
ポインタは、対象のオブジェクトそのものを持つわけではなく、そのオブジェクトの場所を指し示すものです。
そのため、ポインタ変数を宣言するだけであれば、対象クラスの中身までは分からなくても問題ありません。
ポインタ自体のサイズは、指しているクラスの中身に関係なく決まっています。
したがって、前方宣言だけでもポインタ型として使うことができます。
参照として扱う
前方宣言された型は、参照として扱うこともできます。
参照も、オブジェクトそのものをその場に持つわけではありません。
そのため、関数の引数や戻り値として参照を使うだけであれば、前方宣言で足りる場合があります。
ただし、参照先のメンバ変数にアクセスしたり、メンバ関数を呼び出したりする場合には、完全な定義が必要です。
関数の引数や戻り値に使う
前方宣言された型は、関数の引数や戻り値の型として使える場合があります。
特に、ポインタや参照として使う場合は、前方宣言だけで問題ないことが多いです。
また、値渡しや値返しの場合でも、関数の宣言だけであれば前方宣言で済むことがあります。
ただし、関数の中身を定義する場所や、実際にオブジェクトを生成・コピー・破棄する場所では、完全な定義が必要になります。
そのため、初心者のうちは「ポインタや参照なら前方宣言で済むことが多い」「値として扱うなら完全な定義が必要になりやすい」と覚えておくと分かりやすいです。
メンバ関数の宣言に使う
クラスのメンバ関数の引数や戻り値に、前方宣言された型を使うこともできます。
ヘッダーファイルでは関数の宣言だけを書き、実際の処理はソースファイル側に書く、という設計では前方宣言がよく使われます。
この場合、ヘッダーファイルでは相手のクラスの詳細を知らなくてもよく、ソースファイル側で必要になったときに完全な定義を読み込めばよいからです。
前方宣言だけではできないこと
オブジェクトを値として持つことはできない
前方宣言だけでは、あるクラスのオブジェクトを値として持つことはできません。
たとえば、あるクラスの中に別のクラスの実体をメンバ変数として持たせたい場合、そのメンバ変数のサイズが必要です。
クラス全体のサイズを計算するためには、各メンバ変数のサイズが分かっていなければなりません。
前方宣言だけではその型のサイズが分からないため、値としてメンバに持つことはできません。
このような場合は、対象クラスの完全な定義を読み込む必要があります。
オブジェクトを直接生成することはできない
前方宣言だけでは、そのクラスのオブジェクトを直接生成することもできません。
オブジェクトを作るには、コンストラクタやサイズなどの情報が必要です。
しかし、前方宣言ではクラスの中身が分からないため、コンパイラはオブジェクトを生成できません。
ポインタや参照として扱うだけなら前方宣言で済むことがありますが、実体を作る場合には完全な定義が必要です。
メンバ変数やメンバ関数にアクセスできない
前方宣言だけでは、そのクラスにどのようなメンバ変数やメンバ関数があるか分かりません。
そのため、前方宣言された型のポインタや参照を持っていたとしても、メンバ変数にアクセスしたり、メンバ関数を呼び出したりすることはできません。
メンバにアクセスするには、そのクラスの完全な定義が必要です。
継承元として使うことはできない
前方宣言だけのクラスを、基底クラスとして継承することはできません。
派生クラスを定義するには、基底クラスのサイズやメンバ構造などを知る必要があります。
前方宣言だけでは、基底クラスの中身が分からないため、継承関係を正しく構築できません。
そのため、あるクラスを継承する場合は、基底クラスの完全な定義が必要です。
サイズを調べることはできない
前方宣言された型はサイズが分からないため、その型自体のサイズを調べることはできません。
サイズを調べるには、クラスの中にどのようなメンバがあるかをコンパイラが知っている必要があります。
前方宣言だけではその情報がないため、サイズの計算はできません。
ただし、その型へのポインタのサイズであれば分かります。
ポインタのサイズは、指している型の中身に関係なく決まっているためです。
前方宣言を使う主な理由
ヘッダーファイルの依存関係を減らすため
前方宣言を使う大きな理由のひとつは、ヘッダーファイル同士の依存関係を減らすことです。
C++では、クラス定義をヘッダーファイルに書くことが多くあります。
そして、あるヘッダーファイルが別のヘッダーファイルを読み込むと、その依存関係がプロジェクト全体に広がっていきます。
必要のないヘッダーファイルまで読み込んでしまうと、コードの依存関係が複雑になり、変更の影響範囲も広くなります。
前方宣言を使えば、相手のクラスの中身が不要な場面では、ヘッダーファイルを直接読み込まずに済みます。
コンパイル時間を短縮するため
C++のヘッダーファイルは、読み込まれるたびにコンパイラによって処理されます。
特に、大きなヘッダーファイルや、多くの別ヘッダーを読み込んでいるヘッダーファイルをあちこちで読み込むと、コンパイル時間が長くなりやすくなります。
前方宣言を使って不要な読み込みを減らすことで、コンパイル時間の短縮につながる場合があります。
大規模なプロジェクトでは、ヘッダーの依存関係を整理することが、ビルド時間の改善に大きく関係します。
循環依存を避けるため
前方宣言は、クラス同士が互いに参照し合うような場合にも役立ちます。
たとえば、あるクラスが別のクラスを参照し、その別のクラスも最初のクラスを参照したい場合、そのままお互いのヘッダーファイルを読み込み合うと、循環依存が発生しやすくなります。
循環依存が起きると、コンパイルエラーの原因になったり、ヘッダー構成が分かりにくくなったりします。
このような場合に前方宣言を使うと、クラス同士の関係を保ちながら、ヘッダーファイル同士の直接的な依存を減らせます。
実装の詳細を隠すため
前方宣言は、クラスの内部実装を隠したい場合にも使われます。
外部に公開するヘッダーファイルでは詳細な実装を見せず、実装ファイル側に中身を隠すことで、利用者側に余計な情報を見せずに済みます。
この考え方は、Pimplイディオムと呼ばれる設計手法でも使われます。
Pimplでは、実装部分を別のクラスに分離し、そのクラスを前方宣言してポインタ経由で扱います。
これにより、ヘッダーの依存関係を減らし、内部実装の変更が外部に与える影響を小さくできます。
前方宣言とincludeの違い
前方宣言は名前だけを知らせる
前方宣言は、対象の型や関数の名前だけをコンパイラに知らせるものです。
そのため、型の中身やサイズ、メンバ関数の情報までは分かりません。
名前だけ分かればよい場面では、前方宣言で十分です。
includeは定義全体を読み込む
includeは、別のファイルに書かれている内容を読み込む仕組みです。
対象クラスの完全な定義を読み込むため、そのクラスのサイズやメンバ情報が必要な場面で使います。
たとえば、値としてメンバ変数に持つ場合、オブジェクトを生成する場合、メンバ関数を呼び出す場合、継承する場合などは、完全な定義が必要になります。
使い分けの基本
基本的には、ヘッダーファイルでは前方宣言で済むなら前方宣言を使い、実装ファイル側で必要なヘッダーを読み込む、という考え方がよく使われます。
ただし、無理に前方宣言を使いすぎる必要はありません。
小さくて依存の少ないヘッダーファイルであれば、素直にincludeしたほうが読みやすい場合もあります。
重要なのは、「前方宣言を使うこと」そのものではなく、型の完全な定義が必要かどうかを判断することです。
前方宣言で注意すべきポイント
不完全型という考え方を理解する
前方宣言だけされた型は、不完全型です。
不完全型とは、名前は分かっているものの、中身がまだ分かっていない型のことです。
不完全型は、ポインタや参照として扱うことはできますが、サイズが必要な操作やメンバにアクセスする操作には使えません。
前方宣言を理解するうえで、この「不完全型」という考え方は非常に重要です。
deleteする場所では完全な定義が必要
前方宣言された型のポインタを削除する場合には注意が必要です。
オブジェクトを削除するときには、そのクラスのデストラクタを呼ぶ必要があります。
デストラクタを正しく呼ぶには、クラスの完全な定義が見えていなければなりません。
そのため、不完全型のまま削除処理を行うのは避けるべきです。
ポインタを削除する場所では、対象クラスの完全な定義を読み込むようにします。
スマートポインタと組み合わせる場合も注意する
前方宣言は、スマートポインタと組み合わせて使われることもよくあります。
特に、独占所有を表すスマートポインタでは、前方宣言された型をメンバに持つことがあります。
ただし、スマートポインタが管理しているオブジェクトを破棄するタイミングでは、対象クラスの完全な定義が必要です。そのため、デストラクタの定義場所に注意する必要があります。
実務では、スマートポインタで前方宣言された型を持つ場合、デストラクタをヘッダー内ではなく実装ファイル側に書くことがあります。
標準コンテナに値として入れる場合は注意する
前方宣言された型を、標準コンテナに値として格納する場合にも注意が必要です。
たとえば、ある型を配列やリストの要素として直接格納する場合、その型のサイズやコンストラクタ、デストラクタなどが必要になります。
そのため、標準コンテナに値として入れる場合は、基本的には完全な定義を読み込むと考えたほうが安全です。
一方で、ポインタを格納するだけであれば、前方宣言で済む場合があります。
ただし、生ポインタを使う場合は、そのオブジェクトを誰が所有し、誰が破棄するのかを明確にする必要があります。
enumの前方宣言には条件がある
列挙型も前方宣言できる場合があります。
スコープ付き列挙型は前方宣言しやすいです。
一方、通常の列挙型では、基底型を明示する必要があります。
そのため、列挙型の前方宣言を使う場合は、どの形式なら前方宣言できるのかを確認しておく必要があります。
初心者のうちは、列挙型の前方宣言は少し特殊な扱いだと考えておくとよいでしょう。
前方宣言が役立つ具体的な場面
クラス同士が互いに参照し合う場合
前方宣言が特に役立つのは、2つ以上のクラスが互いに関係を持つ場合です。
たとえば、プレイヤーとチーム、親オブジェクトと子オブジェクト、画面とUI部品のように、互いに相手を参照する設計はよくあります。
このような場合、すべてのヘッダーを直接読み込み合うと、循環依存が発生しやすくなります。
片方または両方を前方宣言にすることで、依存関係を整理しやすくなります。
ヘッダーを軽くしたい場合
多くのファイルから読み込まれるヘッダーは、できるだけ軽くしておくと保守しやすくなります。
特に、ライブラリや大規模プロジェクトでは、ひとつのヘッダーの変更が大量のファイルの再コンパイルにつながることがあります。
前方宣言を使えば、必要以上にヘッダーを読み込まずに済むため、依存関係を小さくできます。
実装を隠したい場合
クラスの内部実装を利用者に見せたくない場合にも、前方宣言が使われます。
利用者にとって必要なのは公開インターフェースだけであり、内部でどのようなデータ構造を使っているかまでは知る必要がないことがあります。
このような場合、前方宣言を使って内部実装を隠すことで、クラスの設計をより柔軟にできます。
前方宣言を使いすぎるデメリット
コードの見通しが悪くなることがある
前方宣言を使うと、ヘッダーファイルを読み込まずに済む反面、その型がどこで定義されているのか分かりにくくなることがあります。
特に、プロジェクトが小さい場合や、対象のヘッダーが軽い場合には、前方宣言を使うメリットがあまり大きくないこともあります。
includeしたほうが分かりやすい場合もある
前方宣言を使えば依存関係を減らせますが、常に最善とは限りません。
対象の型を頻繁に使う場合や、そのヘッダーが非常に小さい場合は、素直にincludeしたほうが読みやすく、保守しやすいこともあります。
前方宣言は、依存を減らすための手段であり、目的ではありません。
開発環境によっては補完や解析に影響することがある
現代の開発環境では、コード補完や静的解析が非常に発達しています。
前方宣言を多用すると、ヘッダーだけを見たときに型の詳細が分からず、読み手にとって把握しづらくなることがあります。
もちろん、多くのIDEは定義ジャンプなどで補ってくれますが、読みやすさとのバランスは重要です。
実務での判断基準
ポインタや参照だけなら前方宣言を検討する
ある型をポインタや参照として持つだけであれば、前方宣言で済むことが多いです。
特に、ヘッダーファイル内で相手のクラスの中身に触れないのであれば、前方宣言を使う候補になります。
値として持つなら完全な定義が必要
メンバ変数として別のクラスの実体を持つ場合は、完全な定義が必要です。
この場合、コンパイラはそのメンバのサイズを知る必要があります。
前方宣言だけではサイズが分からないため、includeが必要になります。
メンバにアクセスするなら完全な定義が必要
相手のクラスのメンバ変数やメンバ関数にアクセスする場合は、完全な定義が必要です。
ポインタや参照として受け取るだけなら前方宣言で済むことがありますが、その先の中身を使うならincludeが必要です。
継承するなら完全な定義が必要
あるクラスを継承する場合は、基底クラスの完全な定義が必要です。
継承関係では、基底クラスの構造やサイズが派生クラスの定義に影響するため、前方宣言だけでは不十分です。
ヘッダーでは前方宣言、実装ファイルではincludeを使う
実務でよく使われる考え方は、ヘッダーファイルでは前方宣言で依存を減らし、実装ファイルで必要なヘッダーを読み込むというものです。
ヘッダーには必要最小限の情報だけを書き、実際にメンバ関数を呼び出す処理などは実装ファイル側に書くことで、依存関係を整理しやすくなります。
前方宣言の覚え方
「住所だけなら前方宣言でよい」と考える
前方宣言を直感的に理解するには、「実物を置くのか、住所だけ持つのか」で考えると分かりやすいです。
ポインタや参照は、ある意味でオブジェクトの場所を扱うものです。
つまり、実物の中身をその場に置くわけではありません。
そのため、前方宣言だけで扱えることがあります。
「実物を置くなら完全な定義が必要」と考える
一方で、値としてオブジェクトを持つ場合は、実物をその場に置くことになります。
実物を置くには、そのサイズや構造が分かっていなければなりません。
そのため、値として持つ場合やオブジェクトを生成する場合は、完全な定義が必要です。
「中身を使うなら完全な定義が必要」と考える
メンバ変数やメンバ関数にアクセスする場合は、そのクラスの中身を使うことになります。
前方宣言では中身が分からないため、メンバにアクセスすることはできません。
中身を使うなら完全な定義が必要、と覚えると判断しやすくなります。
まとめ
C++の前方宣言は、型や関数の名前だけを先にコンパイラへ知らせる仕組みです。
前方宣言を使うと、ヘッダーファイルの依存関係を減らしたり、コンパイル時間を短縮したり、循環依存を避けたりできます。
一方で、前方宣言だけでは型の中身やサイズは分かりません。
そのため、オブジェクトを値として持つ場合、オブジェクトを生成する場合、メンバにアクセスする場合、継承する場合などには、完全な定義が必要です。
基本的には、次のように考えると分かりやすいです。
ポインタや参照として使うだけなら、前方宣言で済むことが多いです。
値として持つ場合や、中身を使う場合は、完全な定義が必要です。
実務では、ヘッダーファイルでは前方宣言を使って依存を減らし、実装ファイル側で必要なヘッダーを読み込む設計がよく使われます。
前方宣言は、C++のヘッダーファイル設計を理解するうえで非常に重要な考え方です。
依存関係を整理し、保守しやすいコードを書くためにも、どの場面で前方宣言を使えるのか、どの場面で完全な定義が必要なのかを押さえておきましょう。
以上、C++の前方宣言についてでした。
最後までお読みいただき、ありがとうございました。
