いつフォワード宣言を使用できますか?


602

別のクラスのヘッダーファイルでクラスの前方宣言をいつ許可するかの定義を探しています。

基本クラス、メンバーとして保持されているクラス、参照によってメンバー関数に渡されたクラスなどでそれを行うことはできますか?


14
私は必死にこれを「いつすべきか」という名前に変更し、回答を適切に更新したいと思っています...
deworde

12
@deworde「すべき」と言うとき、あなたは意見を求めています。
AturSams 2016年

@dewordeビルド時間を改善し、循環参照を回避するために、できる限り前方宣言を使用することを理解しています。私が考えることができる唯一の例外は、インクルードファイルにtypedefが含まれている場合です。
Ohad Schneider 2016

@OhadSchneider実用的な観点から、私はヘッダーの大ファンではありません。÷
deworde

基本的には、常に使用するために別のヘッダーを含める必要があります(コンストラクターパラメーターの前方declが大きな原因です)
deworde

回答:


962

コンパイラーの立場に身を置く:型を転送宣言すると、コンパイラーはすべて、この型が存在することを認識します。サイズ、メンバー、メソッドについては何も知りません。これが不完全型と呼ばれる理由です。したがって、コンパイラーは型のレイアウトを知っている必要があるため、型を使用してメンバーまたは基本クラスを宣言することはできません。

次の前方宣言を想定しています。

class X;

できることとできないことは次のとおりです。

不完全なタイプでできること:

  • メンバーをポインターまたは不完全な型への参照として宣言します。

    class Foo {
        X *p;
        X &r;
    };
  • 不完全な型を受け入れる/返す関数またはメソッドを宣言します。

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

    void f3(X*, X&) {}
    X&   f4()       {}
    X*   f5()       {}

不完全なタイプではできないこと:

  • 基本クラスとして使用する

    class Foo : X {} // compiler error!
  • これを使用してメンバーを宣言します。

    class Foo {
        X m; // compiler error!
    };
  • このタイプを使用して関数またはメソッドを定義します

    void f1(X x) {} // compiler error!
    X    f2()    {} // compiler error!
  • そのメソッドまたはフィールドを使用して、実際には不完全な型の変数を逆参照しようとしている

    class Foo {
        X *m;            
        void method()            
        {
            m->someMethod();      // compiler error!
            int i = m->someField; // compiler error!
        }
    };

テンプレートに関しては、絶対的な規則はありません。不完全な型をテンプレートパラメータとして使用できるかどうかは、型がテンプレートで使用される方法に依存します。

たとえばstd::vector<T>、パラメーターは完全な型である必要がありますが、boost::container::vector<T>そうではありません。特定のメンバー関数を使用する場合にのみ、完全な型が必要になる場合があります。これは、std::unique_ptr<T>たとえばの場合です

十分に文書化されたテンプレートは、完全な型である必要があるかどうかを含め、そのパラメーターのすべての要件を文書で示す必要があります。


4
すばらしい答えですが、私が同意しないエンジニアリングポイントについては、以下の私の記事を参照してください。つまり、受け入れるまたは返す不完全な型のヘッダーを含めない場合は、ヘッダーのコンシューマーに目に見えない依存関係を強制し、必要な他のヘッダーを知る必要があります。
Andy Dent 2013

2
@AndyDent:True、ただしヘッダーのコンシューマーは実際に使用する依存関係のみを含める必要があるため、C ++の「使用した分だけ支払う」という原則に従います。しかし、実際には、ヘッダーがスタンドアロンであることを期待するユーザーにとっては不便です。
Luc Touraille 2013

8
この一連のルールでは、1つの非常に重要なケースが無視されます。標準ライブラリでほとんどのテンプレートをインスタンス化するには、完全なタイプが必要です。ルールに違反すると動作が未定義になり、コンパイラエラーが発生しない可能性があるため、特に注意する必要があります。
James Kanze 2013

12
「コンパイラーの立場に身を置く」ための+1。「コンパイラー・ビーイング」が口ひげを持っていることを想像します。
PascalVKooten 2013

3
@JesusChrist:正確:オブジェクトを値で渡す場合、適切なスタック操作を行うために、コンパイラーはそのサイズを知る必要があります。ポインターまたは参照を渡す場合、コンパイラーはオブジェクトのサイズまたはレイアウトを必要としません。アドレスのサイズ(つまり、ポインターのサイズ)のみを必要とします。これは、指し示される型に依存しません。
Luc Touraille 2014年

45

主なルールは、メモリレイアウト(およびメンバー関数とデータメンバー)が、それを前方宣言するファイルで認識されている必要がないクラスのみを前方宣言できることです。

これは、基本クラスと、参照およびポインタを介して使用されるクラス以外のものを除外します。


6
ほとんど。また、関数プロトタイプでは、「プレーン」(つまり、非ポインター/参照)の不完全な型をパラメーターまたは戻り型として参照することもできます。
j_random_hacker 2009

ヘッダーファイルで定義するクラスのメンバーとして使用するクラスはどうなりますか?それらを転送宣言できますか?
Igor Oks

1
はい。ただし、その場合は、前方宣言されたクラスへの参照またはポインタのみを使用できます。しかし、それでもあなたはメンバーを持つことができます。
Reunanen、2009

32

ラコスはクラスの使用法を区別します

  1. in-name-only(前方宣言で十分)および
  2. サイズ(クラス定義が必要な場合)。

私はそれがもっと簡潔に発音されるのを見たことがない:)


2
名前のみの意味は何ですか?
2014

4
@Boon:あえて言って...?クラスの名前のみを使用する場合?
Marc Mutz-mmutz 2015年

1
マルク、ラコスの1つ
mlvljr

28

不完全な型へのポインタと参照だけでなく、不完全な型であるパラメータや戻り値を指定する関数プロトタイプを宣言することもできます。ただし、ポインターまたは参照でない限り、不完全なパラメーターまたは戻り値の型を持つ関数を定義することはできません。

例:

struct X;              // Forward declaration of X

void f1(X* px) {}      // Legal: can always use a pointer
void f2(X&  x) {}      // Legal: can always use a reference
X f3(int);             // Legal: return value in function prototype
void f4(X);            // Legal: parameter in function prototype
void f5(X) {}          // ILLEGAL: *definitions* require complete types

19

これまでの回答では、クラステンプレートの前方宣言をいつ使用できるかについては説明されていません。だから、ここに行きます。

クラステンプレートは、次のように宣言して転送できます。

template <typename> struct X;

受け入れられた答えの構造に従って、

できることとできないことは次のとおりです。

不完全なタイプでできること:

  • メンバーをポインターまたは別のクラステンプレートの不完全な型への参照として宣言します。

    template <typename T>
    class Foo {
        X<T>* ptr;
        X<T>& ref;
    };
  • メンバーをその不完全なインスタンス化の1つへのポインターまたは参照として宣言します。

    class Foo {
        X<int>* ptr;
        X<int>& ref;
    };
  • 不完全な型を受け入れる/返す関数テンプレートまたはメンバー関数テンプレートを宣言します。

    template <typename T>
       void      f1(X<T>);
    template <typename T>
       X<T>    f2();
  • 不完全なインスタンス化の1つを受け入れる/返す関数またはメンバー関数を宣言します。

    void      f1(X<int>);
    X<int>    f2();
  • 不完全な型へのポインタ/参照を受け入れる/返す関数テンプレートまたはメンバー関数テンプレートを定義します(ただし、そのメンバーを使用しません)。

    template <typename T>
       void      f3(X<T>*, X<T>&) {}
    template <typename T>
       X<T>&   f4(X<T>& in) { return in; }
    template <typename T>
       X<T>*   f5(X<T>* in) { return in; }
  • 不完全なインスタンス化の1つへのポインタ/参照を受け入れる/返す関数またはメソッドを定義します(ただし、そのメンバーを使用しません)。

    void      f3(X<int>*, X<int>&) {}
    X<int>&   f4(X<int>& in) { return in; }
    X<int>*   f5(X<int>* in) { return in; }
  • 別のテンプレートクラスの基本クラスとして使用する

    template <typename T>
    class Foo : X<T> {} // OK as long as X is defined before
                        // Foo is instantiated.
    
    Foo<int> a1; // Compiler error.
    
    template <typename T> struct X {};
    Foo<int> a2; // OK since X is now defined.
  • これを使用して、別のクラステンプレートのメンバーを宣言します。

    template <typename T>
    class Foo {
        X<T> m; // OK as long as X is defined before
                // Foo is instantiated. 
    };
    
    Foo<int> a1; // Compiler error.
    
    template <typename T> struct X {};
    Foo<int> a2; // OK since X is now defined.
  • このタイプを使用して関数テンプレートまたはメソッドを定義します

    template <typename T>
      void    f1(X<T> x) {}    // OK if X is defined before calling f1
    template <typename T>
      X<T>    f2(){return X<T>(); }  // OK if X is defined before calling f2
    
    void test1()
    {
       f1(X<int>());  // Compiler error
       f2<int>();     // Compiler error
    }
    
    template <typename T> struct X {};
    
    void test2()
    {
       f1(X<int>());  // OK since X is defined now
       f2<int>();     // OK since X is defined now
    }

不完全なタイプではできないこと:

  • そのインスタンス化の1つを基本クラスとして使用する

    class Foo : X<int> {} // compiler error!
  • そのインスタンス化の1つを使用して、メンバーを宣言します。

    class Foo {
        X<int> m; // compiler error!
    };
  • インスタンス化の1つを使用して関数またはメソッドを定義する

    void      f1(X<int> x) {}            // compiler error!
    X<int>    f2() {return X<int>(); }   // compiler error!
  • そのインスタンス化の1つのメソッドまたはフィールドを使用して、実際には不完全な型の変数を逆参照しようとしている

    class Foo {
        X<int>* m;            
        void method()            
        {
            m->someMethod();      // compiler error!
            int i = m->someField; // compiler error!
        }
    };
  • クラステンプレートの明示的なインスタンス化を作成する

    template struct X<int>;

2
「これまでのところ、クラステンプレートの前方宣言がいつできるかについての回答はありません。」ことは、単にのセマンティクスためではありませんXし、X<int>あなたの答えの額のすべてが、1行だけのリュックのを服用すると、任意の実質的な方法で正確に同じであり、唯一、前方宣言の構文が異なるとs/X/X<int>/g?それは本当に必要ですか?それとも私は違う小さな細部を見逃しましたか?可能ですが、視覚的に比較したところ、何も表示されません...
underscore_d

ありがとうございました!この編集により、貴重な情報が大量に追加されます。私はそれを完全に理解するために何回か読む必要があります...または、実際のコードでひどく混乱するまで待って、ここに戻ってくるというより良い方法を使用するかもしれません!これを使用して、さまざまな場所での依存関係を減らすことができると思います。
underscore_d

4

クラスへのポインターまたは参照のみを使用するファイル内で、それらのポインター/参照を考慮してメンバー/メンバー関数を呼び出さないでください。

class Foo;//前方宣言

Foo *またはFoo&タイプのデータメンバーを宣言できます。

Foo型の引数や戻り値を持つ関数を宣言できます(ただし、定義はできません)。

Foo型の静的データメンバーを宣言できます。これは、静的データメンバーがクラス定義の外部で定義されているためです。


4

私はこれを単なるコメントではなく別の回答として書いています。なぜなら、合法性の理由ではなく、堅牢なソフトウェアと誤解の危険性があるため、Luc Tourailleの回答に同意しないからです。

具体的には、インターフェイスのユーザーが知っておくべきことの暗黙の契約に問題があります。

参照型を返すか受け入れる場合は、それらが前方宣言を通じてのみ既知である可能性があるポインタまたは参照を通過できることを示しているだけです。

不完全な型を返すX f2();場合は、呼び出し元がXの完全な型指定を持っている必要があることを意味します。呼び出しサイトでLHSまたは一時オブジェクトを作成するために、Xが必要です。

同様に、不完全な型を受け入れる場合、呼び出し元はパラメーターであるオブジェクトを構築している必要があります。そのオブジェクトが関数から別の不完全な型として返された場合でも、呼び出しサイトには完全な宣言が必要です。つまり:

class X;  // forward for two legal declarations 
X returnsX();
void XAcceptor(X);

XAcepptor( returnsX() );  // X declaration needs to be known here

ヘッダーには、他のヘッダーを必要としない依存関係なしに、ヘッダーを使用するのに十分な情報を提供するという重要な原則があると思います。つまり、ヘッダーは、宣言する関数を使用するときにコンパイラエラーを発生させることなく、コンパイルユニットに含めることができる必要があります。

以外

  1. この外部依存関係が望ましい動作である場合。条件付きコンパイルを使用する代わりに、Xを宣言する独自のヘッダーを提供するように文書化された要件を持つことができます。これは、#ifdefsを使用する代わりに使用でき、モックやその他のバリアントを導入するのに便利な方法です。

  2. 重要な違いは、それらをインスタンス化することが明示的に期待されていないいくつかのテンプレートテクニックであり、誰かが私にだまされないように述べられています。


「他のヘッダーを必要としない依存関係なしに、ヘッダーがそれを使用するのに十分な情報を提供するという重要な原則があると思います。」-別の問題は、Naveenの回答に関するAdrian McCarthyのコメントで言及されています。これは、現在テンプレート化されていないタイプであっても、「使用するのに十分な情報を提供する必要がある」という原則に従わない正当な理由を提供します。
Tony Delroy、2014年

3
いつ前方宣言を使用すべき(または使用すべきでないか)について話している。しかし、それは完全にこの質問のポイントではありません。これは、(たとえば)循環依存の問題を解決したいときに、技術的な可能性を知ることです。
JonnyJD 2014

1
I disagree with Luc Touraille's answerそのため、長さが必要な場合は、ブログ投稿へのリンクを含めてコメントを書いてください。これは尋ねられた質問に答えません。Xがどのように機能するかについての質問を正当化した人全員が、Xを使用することに同意しない、またはXを使用する自由を制限する必要がある制限について議論することで答えを正当化します。
underscore_d

3

私が従う一般的な規則は、必要がない限りヘッダーファイルを含めないことです。したがって、クラスのオブジェクトをクラスのメンバー変数として格納している場合を除いて、そのオブジェクトは含めません。フォワード宣言を使用します。


2
これはカプセル化を壊し、コードをもろくします。これを行うには、タイプがtypedefであるか、デフォルトのテンプレートパラメータを持つクラステンプレートのクラスであるかを知る必要があります。実装が変更された場合は、フォワード宣言を使用した場所を更新する必要があります。
エイドリアンマッカーシー

@AdrianMcCarthyは正しいです。適切な解決策は、コンテンツが転送を宣言するヘッダーに含まれる転送宣言ヘッダーを用意することです。これは、そのヘッダーを所有する人も所有、保守、出荷する必要があります。例:iofwd標準ライブラリヘッダー。iostreamコンテンツの前方宣言が含まれています。
Tony Delroy

3

定義が必要ない限り(ポインターと参照を考える)、前方宣言で問題を回避できます。これが主にヘッダーに表示される理由ですが、実装ファイルは通常、適切な定義のヘッダーをプルします。


0

他の型(クラス)をクラスのメンバーとして使用する場合は、通常、クラスヘッダーファイルで前方宣言を使用します。C ++はその時点ではまだそのクラスの定義を認識していないため、ヘッダーファイルで前方宣言されたクラスメソッドを使用することはできません。これは.cpp-filesに移動する必要があるロジックですが、テンプレート関数を使用している場合は、テンプレートを使用する部分のみに減らし、その関数をヘッダーに移動する必要があります。


これは意味がありません。不完全な型のメンバーを持つことはできません。クラスの宣言は、すべてのユーザーがそのサイズとレイアウトについて知る必要があるすべてを提供する必要があります。そのサイズには、非静的メンバーのすべてのサイズが含まれます。メンバーを前方宣言すると、ユーザーのサイズがわかりません。
underscore_d

0

前方宣言がコードをコンパイルする(objが作成される)ことを理解してください。ただし、定義が見つからない場合、リンク(exeの作成)は成功しません。


2
なぜこれまでに2人がこれに賛成票を投じたのですか?あなたは質問が何について話しているかについて話しているのではありません。関数の前方宣言ではなく、通常の意味です。問題は、クラスの前方宣言についてです。「フォワード宣言はコードをコンパイルするようになる」とおっしゃっていましたが、私に好意を示します:compile class A; class B { A a; }; int main(){}、そしてそれがどうなるかを私に知らせてください。もちろん、コンパイルされません。ここでの適切な答えはすべて、前方宣言有効である理由と、限定された正確なコンテキストを説明しています。代わりに、まったく異なる何かについてこれを書いた。
underscore_d

0

Luc Tourailleの回答に記載されていない転送クラスで実行できる重要なことを1つだけ追加したいと思います。

不完全なタイプでできること:

不完全な型へのポインタ/参照を受け入れる/返す関数またはメソッドを定義し、そのポインタ/参照を別の関数に転送します。

void  f6(X*)       {}
void  f7(X&)       {}
void  f8(X* x_ptr, X& x_ref) { f6(x_ptr); f7(x_ref); }

モジュールは、前方宣言されたクラスのオブジェクトを別のモジュールに渡すことができます。


「転送されたクラス」と「転送された宣言されたクラス」は、2つの非常に異なるものを指すと誤解される場合があります。あなたが書いたものは、Lucの回答に含まれている概念から直接従うので、明白な説明を加えて良いコメントをしたでしょうが、それが回答を正当化するかどうかはわかりません。
underscore_d

0

すでに、Luc Tourailleは、クラスの前方宣言を使用する場合と使用しない場合で、非常によく説明しています。

私はそれを使用する必要がある理由をそれに追加します。

不要な依存性注入を回避するために、可能な限りForward宣言を使用する必要があります。

#includeヘッダファイルは、したがって、複数のファイルに追加され、我々は別のヘッダファイルにヘッダを追加する場合、それは追加することによって回避することができるソースコードの様々な部分に不要な依存性注入を追加する#includeにヘッダを.cppファイル可能な限りではなく、別のヘッダファイルに追加することとヘッダー.hファイルで可能な限りクラス転送宣言を使用します。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.