web-dev-qa-db-ja.com

いつ前方宣言を使用できますか?

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

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

569
Igor Oks

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

次のような前方宣言を仮定します。

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> の場合です。

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

912
Luc Touraille

主な規則は、あなたがそれを前方宣言するファイルの中でメモリレイアウト(そしてメンバー関数とデータメンバー)を知る必要がないクラスだけを前方宣言することができるということです。

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

43
Timo Geusch

Lakos はクラスの使い方を区別します

  1. in-name-only(前方宣言で十分です)
  2. in-size(クラス定義が必要).

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

32

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

例:

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
28
j_random_hacker

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

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

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>;
    
15
R Sahu

クラスまたはクラスへの参照または参照のみを使用するファイルでは、メンバー/メンバー関数はそれらのポインター/参照を考慮して呼び出すべきではありません。

class Foo; //前方宣言

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

Foo型の引数や戻り値を使って関数を宣言できます(定義はできません)。

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

5
yesraaj

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

具体的には、私はあなたがあなたのインターフェースのユーザーが知っていなければならないと期待するものの暗黙の契約に問題があります。

参照型を返すか受け入れる場合、それらはポインタまたは参照を通過できると言っているのですが、これらは順方向宣言を介してのみ認識される可能性があります。

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

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

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. 重要な違いは、それらをインスタンス化することが明示的には期待されていないテンプレート手法です。他の誰かが私を卑劣にしないために言及されています。

4
Andy Dent

私がしなければならない場合を除いて、私が従う一般的な規則はいかなるヘッダーファイルも含まないことです。そのため、クラスのオブジェクトを自分のクラスのメンバー変数として格納しているのでなければ、それを含めないでください。前方宣言を使用します。

3
Naveen

あなたが定義を必要としない限り(ポインタや参照を考える)、前方宣言をやめることができます。実装ファイルが通常適切な定義のためにヘッダーを引っ張る間、これが主にあなたがヘッダーの中にそれらを見るであろう理由です。

3
dirkgently

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

0

前方宣言はあなたのコードをコンパイルすることになるでしょう(objが作成されます)。しかし、リンクが定義されていない限り、リンクを作成することはできません。

0
Sesh

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

不完全型でできること:

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

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

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

0
Niceman