web-dev-qa-db-ja.com

可能な限りインクルードの代わりに前方宣言を使用する必要がありますか?

クラス宣言がポインターとしてのみ別のクラスを使用するときはいつでも、循環依存関係の問題を先制的に回避するために、ヘッダーファイルを含める代わりにクラスフォワード宣言を使用することは意味がありますか?したがって、代わりに:

//file C.h
#include "A.h"
#include "B.h"

class C{
    A* a;
    B b;
    ...
};

代わりにこれを行います:

//file C.h
#include "B.h"

class A;

class C{
    A* a;
    B b;
    ...
};


//file C.cpp
#include "C.h"
#include "A.h"
...

可能な限りこれを行わない理由はありますか?

71
Mat

ほとんどの場合、前方宣言メソッドの方が優れています。 (前方宣言を使用できるファイルを含める方が良いという状況は考えられませんが、万が一に備えて常に良いとは言いません)。

前方宣言クラスにマイナス面はありませんが、ヘッダーを不必要に含めることにはいくつかのマイナス面が考えられます。

  • C.hを含むすべての翻訳単位にもA.hが含まれますが、必要ない場合もあるため、コンパイル時間が長くなります。

  • 間接的に不要な他のヘッダーを含む可能性があります

  • 不要な記号で翻訳単位を汚染する

  • 変更された場合、そのヘッダーを含むソースファイルを再コンパイルする必要があるかもしれません(@PeterWood)

56
Luchian Grigore

はい、前方宣言の使用は常に優れています。

彼らが提供する利点のいくつかは次のとおりです。

  • コンパイル時間の短縮。
  • 名前空間の汚染はありません。
  • (場合によっては)生成されたバイナリのサイズが小さくなる場合があります。
  • 再コンパイル時間を大幅に短縮できます。
  • プリプロセッサ名の潜在的な衝突を回避します。
  • PIMPL Idiomを実装することにより、インターフェースから実装を隠す手段を提供します。

ただし、クラスをForward宣言すると、その特定のクラスはIncompleteタイプになり、Incompleteタイプで実行できる操作が厳しく制限されます。
クラスのレイアウトを知るためにコンパイラを必要とする操作は実行できません。

不完全型では次のことができます。

  • メンバーを、不完全な型へのポインターまたは参照として宣言します。
  • 不完全な型を受け入れる/返す関数またはメソッドを宣言します。
  • 不完全な型へのポインター/参照を受け入れる/返す関数を定義します(ただし、そのメンバーは使用しません)。

不完全なタイプでは次のことができません。

  • 基本クラスとして使用します。
  • メンバーを宣言するために使用します。
  • このタイプを使用して関数またはメソッドを定義します。
36
Alok Save

可能な限りこれを行わない理由はありますか?

便利。

フェーズの前に、このヘッダーファイルのユーザーがAの定義も含める必要があることがわかっている場合(またはほとんどの場合)。その後、一度だけすべてを含めると便利です。

この経験則をあまりにもリベラルに使用すると、ほとんどコンパイルできないコードが生成されるため、これはかなり扱いにくいテーマです。 Boostはいくつかの密接な機能をまとめた特定の「便利な」ヘッダーを提供することにより、問題に異なるアプローチをすることに注意してください。

18
Matthieu M.

前方宣言をしたくない場合の1つは、それ自体が扱いにくい場合です。これは、次の例のように、クラスの一部がテンプレート化されている場合に発生する可能性があります。

// Forward declarations
template <typename A> class Frobnicator;
template <typename A, typename B, typename C = Frobnicator<A> > class Gibberer;

// Alternative: more clear to the reader; more stable code
#include "Gibberer.h"

// Declare a function that does something with a pointer
int do_stuff(Gibberer<int, float>*);

前方宣言はコードの複製と同じです。コードが大幅に変更される傾向がある場合は、毎回2箇所以上変更する必要があり、それは良くありません。

11
anatolyg

可能な限りインクルードの代わりに前方宣言を使用する必要がありますか?

いいえ、明示的な前方宣言を一般的なガイドラインと見なすべきではありません。基本的に、前方宣言はコピーアンドペースト、またはコードのスペルミスです。バグが見つかった場合は、前方宣言が使用されているすべての場所で修正する必要があります。これはエラーが発生しやすい可能性があります。

「前方」宣言とその定義の不一致を避けるには、宣言をヘッダーファイルに入れ、そのヘッダーファイルを定義および宣言を使用するソースファイルの両方に含めます。

ただし、不透明なクラスのみが前方宣言されているこの特別な場合、この前方宣言は使用しても構いませんが、一般的には、このスレッドのタイトルが示すように、「可能な場合は常にインクルードの代わりに前方宣言を使用する」ことができますかなり危険です。

前方宣言に関する「目に見えないリスク」の例を次に示します(目に見えないリスク=コンパイラまたはリンカーによって検出されない宣言の不一致)。

  • データを表すシンボルの明示的な前方宣言は、データ型のフットプリント(サイズ)の正しい知識を必要とする可能性があるため、安全ではない場合があります。

  • 関数を表すシンボルの明示的な前方宣言も、パラメーターの種類やパラメーターの数のように安全ではない場合があります。

以下の例はこれを示しています。例えば、データと関数の2つの危険な前方宣言です。

ファイルa.c:

_#include <iostream>
char data[128][1024];
extern "C" void function(short truncated, const char* forgotten) {
  std::cout << "truncated=" << std::hex << truncated
            << ", forgotten=\"" << forgotten << "\"\n";
}
_

ファイルb.c:

_#include <iostream>
extern char data[1280][1024];           // 1st dimension one decade too large
extern "C" void function(int tooLarge); // Wrong 1st type, omitted 2nd param

int main() {
  function(0x1234abcd);                         // In worst case: - No crash!
  std::cout << "accessing data[1270][1023]\n";
  return (int) data[1270][1023];                // In best case:  - Boom !!!!
}
_

G ++ 4.7.1を使用してプログラムをコンパイルします。

_> g++ -Wall -pedantic -ansi a.c b.c
_

注:g ++はコンパイラーまたはリンカーのエラー/警告を出さないため、目に見えない危険性があります
注:_extern "C"_を省略すると、c ++の名前のマングリングにより、function()のリンクエラーが発生します。

プログラムの実行:

_> ./a.out
truncated=abcd, forgotten="♀♥♂☺☻"
accessing data[1270][1023]
Segmentation fault
_
8
Blue Demon

おもしろい事実、 C++スタイルガイド で、Googleは#includeを使用することをお勧めしますが、循環依存関係を避けることをお勧めします。

8
Zouch

可能な限りこれを行わない理由はありますか?

絶対:クラスまたは関数のユーザーが実装の詳細を知り、複製することを要求することにより、カプセル化を破ります。これらの実装の詳細が変更された場合、ヘッダーに依存するコードは引き続き機能しますが、前方宣言するコードが破損する可能性があります。

関数の前方宣言:

  • 静的ファンクターオブジェクトや(gasp!)マクロのインスタンスではなく、関数として実装されていることを知っている必要があります。

  • デフォルトパラメータのデフォルト値を複製する必要があります。

  • 実際の名前と名前空間を知っている必要があります。これは、おそらくエイリアスの下で別の名前空間にそれを引っ張るusing宣言である可能性があるためです。

  • インライン最適化で失われる可能性があります。

消費するコードがヘッダーに依存している場合、これらの実装の詳細はすべて、コードを壊さずに関数プロバイダーによって変更できます。

クラスの前方宣言:

  • 派生クラスであるか、派生元の基本クラスであるかを知る必要があります。

  • typedefやクラステンプレートの特定のインスタンス化だけでなく、クラスであることを知る必要があります(または、クラステンプレートであることを知り、すべてのテンプレートパラメータとデフォルト値を正しく取得する)、

  • クラスの本当の名前と名前空間を知っている必要があります。それは、おそらくエイリアスの下で別の名前空間にそれを引っ張るusing宣言かもしれないからです。

  • 正しい属性を知っている必要があります(おそらく特別な配置要件があります)。

繰り返しますが、前方宣言はこれらの実装の詳細のカプセル化を破り、コードをより脆弱にします。

コンパイル時間を短縮するためにヘッダーの依存関係をカットする必要がある場合は、クラス/関数/ライブラリのプロバイダーに特別なforward宣言ヘッダーを提供してもらいます。標準ライブラリは<iosfwd>でこれを行います。このモデルは、実装の詳細のカプセル化を保持し、ライブラリのメンテナーがコンパイラの負荷を軽減しながら、コードを壊すことなくこれらの実装の詳細を変更できるようにします。

別のオプションは、実装の詳細をさらに隠し、わずかな実行時オーバーヘッドを犠牲にしてコンパイルを高速化する、pimplイディオムを使用することです。

5
Adrian McCarthy

可能な限りこれを行わない理由はありますか?

私が考える唯一の理由は、いくつかの入力を節約することです。

前方宣言がなければ、ヘッダーファイルを1回だけインクルードできますが、他の人から指摘されたデメリットのために、かなり大きなプロジェクトにインクルードすることはお勧めしません。

2
ks1322