Kapan saya bisa menggunakan deklarasi maju?


602

Saya mencari definisi kapan saya diizinkan untuk melakukan penerusan deklarasi suatu kelas dalam file header kelas lain:

Apakah saya boleh melakukannya untuk kelas dasar, untuk kelas yang diadakan sebagai anggota, untuk kelas yang diteruskan ke fungsi anggota dengan referensi, dll.?


14
Saya sangat ingin ini dinamai "kapan saya harus ", dan jawaban diperbarui dengan tepat ...
deworde

12
@deworde Ketika Anda mengatakan kapan "harus" Anda meminta pendapat.
AturSams

@deworde adalah pemahaman saya bahwa Anda ingin menggunakan deklarasi maju kapan pun Anda bisa, untuk meningkatkan waktu pembuatan dan menghindari referensi melingkar. Satu-satunya pengecualian yang dapat saya pikirkan adalah ketika file include berisi typedef, dalam hal ini ada tradeoff antara mendefinisikan ulang typedef (dan mempertaruhkannya berubah) dan termasuk seluruh file (bersama dengan rekursifnya termasuk).
Ohad Schneider

@OhadSchneider Dari sudut pandang praktis, saya bukan penggemar header yang saya. ÷
deword

pada dasarnya selalu mengharuskan Anda untuk memasukkan header yang berbeda untuk menggunakannya (menyatakan deklarasi parameter konstruktor adalah penyebab besar di sini)
deword

Jawaban:


962

Tempatkan diri Anda pada posisi kompiler: ketika Anda meneruskan mendeklarasikan suatu tipe, yang diketahui kompiler adalah bahwa tipe ini ada; ia tidak tahu apa-apa tentang ukuran, anggota, atau metode. Inilah sebabnya mengapa ini disebut tipe tidak lengkap . Oleh karena itu, Anda tidak dapat menggunakan tipe untuk mendeklarasikan anggota, atau kelas dasar, karena kompiler perlu mengetahui tata letak tipe tersebut.

Dengan asumsi deklarasi maju berikut.

class X;

Inilah yang bisa dan tidak bisa Anda lakukan.

Apa yang dapat Anda lakukan dengan jenis yang tidak lengkap:

  • Nyatakan anggota sebagai penunjuk atau referensi ke tipe tidak lengkap:

    class Foo {
        X *p;
        X &r;
    };
    
  • Menyatakan fungsi atau metode yang menerima / mengembalikan tipe tidak lengkap:

    void f1(X);
    X    f2();
    
  • Tetapkan fungsi atau metode yang menerima / mengembalikan pointer / referensi ke tipe tidak lengkap (tetapi tanpa menggunakan anggotanya):

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

Apa yang tidak dapat Anda lakukan dengan jenis yang tidak lengkap:

  • Gunakan itu sebagai kelas dasar

    class Foo : X {} // compiler error!
  • Gunakan itu untuk mendeklarasikan anggota:

    class Foo {
        X m; // compiler error!
    };
    
  • Tentukan fungsi atau metode menggunakan tipe ini

    void f1(X x) {} // compiler error!
    X    f2()    {} // compiler error!
    
  • Gunakan metode atau bidangnya, sebenarnya mencoba untuk mengubah variabel dengan tipe tidak lengkap

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

Ketika datang ke template, tidak ada aturan absolut: apakah Anda dapat menggunakan tipe yang tidak lengkap karena parameter template tergantung pada cara tipe tersebut digunakan dalam template.

Misalnya, std::vector<T>mengharuskan parameternya menjadi tipe lengkap, sementara boost::container::vector<T>tidak. Terkadang, tipe yang lengkap diperlukan hanya jika Anda menggunakan fungsi anggota tertentu; ini adalah kasus untukstd::unique_ptr<T> , misalnya.

Templat yang terdokumentasi dengan baik harus menunjukkan dalam dokumentasinya semua persyaratan parameternya, termasuk apakah mereka perlu tipe yang lengkap atau tidak.


4
Jawaban yang bagus tapi tolong lihat milik saya di bawah ini untuk titik teknik yang saya tidak setuju. Singkatnya, jika Anda tidak menyertakan tajuk untuk jenis tidak lengkap yang Anda terima atau kembali, Anda memaksakan ketergantungan yang tidak terlihat pada konsumen tajuk Anda karena harus mengetahui yang lainnya yang mereka butuhkan.
Andy Dent

2
@AndyDent: Benar, tetapi pengguna header hanya perlu menyertakan dependensi yang sebenarnya ia gunakan, jadi ini mengikuti prinsip C ++ dari "Anda hanya membayar untuk apa yang Anda gunakan". Tapi memang, itu bisa merepotkan bagi pengguna yang berharap header menjadi mandiri.
Luc Touraille

8
Rangkaian aturan ini mengabaikan satu kasus yang sangat penting: Anda memerlukan tipe lengkap untuk membuat instantiate sebagian besar templat di pustaka standar. Perhatian khusus perlu diberikan pada ini, karena melanggar aturan menghasilkan perilaku yang tidak terdefinisi, dan mungkin tidak menyebabkan kesalahan kompiler.
James Kanze

12
+1 untuk "tempatkan diri Anda di posisi kompiler". Saya membayangkan "kompiler sedang" memiliki kumis.
PascalVKooten

3
@ JesusChrist: Tepat: ketika Anda melewati objek dengan nilai, kompiler perlu mengetahui ukurannya untuk membuat manipulasi tumpukan yang tepat; ketika melewati pointer atau referensi, kompiler tidak perlu ukuran atau tata letak objek, hanya ukuran alamat (yaitu ukuran pointer), yang tidak tergantung pada tipe yang ditunjukkan.
Luc Touraille

45

Aturan utamanya adalah bahwa Anda hanya dapat meneruskan kelas yang tata letak memorinya (dan dengan demikian fungsi anggota dan anggota data) tidak perlu diketahui dalam file yang Anda forwardkan mendeklarasikannya.

Ini akan mengesampingkan kelas dasar dan apa pun kecuali kelas yang digunakan melalui referensi dan petunjuk.


6
Hampir. Anda juga bisa merujuk ke tipe "polos" (yaitu non-pointer / referensi) yang tidak lengkap sebagai parameter atau mengembalikan tipe dalam prototipe fungsi.
j_random_hacker

Bagaimana dengan kelas yang ingin saya gunakan sebagai anggota kelas yang saya definisikan di file header? Bisakah saya meneruskannya?
Igor Oks

1
Ya, tetapi dalam hal ini Anda hanya dapat menggunakan referensi atau pointer ke kelas yang dideklarasikan ke depan. Tetapi itu membuat Anda memiliki anggota.
Reunanen

32

Lakos membedakan antara penggunaan kelas

  1. in-name-only (yang deklarasi majunya memadai) dan
  2. dalam ukuran (yang membutuhkan definisi kelas).

Saya belum pernah melihatnya diucapkan dengan lebih ringkas :)


2
Apa yang dimaksud hanya dalam nama?
Boon

4
@Boon: berani saya katakan ...? Jika Anda menggunakan hanya kelas nama ?
Marc Mutz - mmutz

1
Plus satu untuk Lakos, Marc
mlvljr

28

Selain petunjuk dan referensi ke tipe tidak lengkap, Anda juga dapat mendeklarasikan prototipe fungsi yang menentukan parameter dan / atau mengembalikan nilai yang merupakan tipe tidak lengkap. Namun, Anda tidak dapat menentukan fungsi yang memiliki parameter atau tipe pengembalian yang tidak lengkap, kecuali itu adalah pointer atau referensi.

Contoh:

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

Sejauh ini tidak ada jawaban yang menjelaskan kapan orang bisa menggunakan deklarasi templat kelas yang diteruskan. Jadi begini saja.

Templat kelas dapat diteruskan dinyatakan sebagai:

template <typename> struct X;

Mengikuti struktur jawaban yang diterima ,

Inilah yang bisa dan tidak bisa Anda lakukan.

Apa yang dapat Anda lakukan dengan jenis yang tidak lengkap:

  • Menyatakan anggota untuk menjadi penunjuk atau referensi ke tipe tidak lengkap di templat kelas lain:

    template <typename T>
    class Foo {
        X<T>* ptr;
        X<T>& ref;
    };
    
  • Nyatakan anggota sebagai penunjuk atau referensi ke salah satu instantiasinya yang tidak lengkap:

    class Foo {
        X<int>* ptr;
        X<int>& ref;
    };
    
  • Nyatakan templat fungsi atau templat fungsi anggota yang menerima / mengembalikan tipe tidak lengkap:

    template <typename T>
       void      f1(X<T>);
    template <typename T>
       X<T>    f2();
    
  • Menyatakan fungsi atau fungsi anggota yang menerima / mengembalikan salah satu instantiasinya yang tidak lengkap:

    void      f1(X<int>);
    X<int>    f2();
    
  • Tentukan templat fungsi atau templat fungsi anggota yang menerima / mengembalikan pointer / referensi ke tipe tidak lengkap (tetapi tanpa menggunakan anggotanya):

    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; }
    
  • Tetapkan fungsi atau metode yang menerima / mengembalikan pointer / referensi ke salah satu instantiasinya yang tidak lengkap (tetapi tanpa menggunakan anggotanya):

    void      f3(X<int>*, X<int>&) {}
    X<int>&   f4(X<int>& in) { return in; }
    X<int>*   f5(X<int>* in) { return in; }
    
  • Gunakan itu sebagai kelas dasar dari kelas template lain

    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.
    
  • Gunakan untuk mendeklarasikan anggota templat kelas lain:

    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.
    
  • Tentukan templat fungsi atau metode menggunakan jenis ini

    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
    }
    

Apa yang tidak dapat Anda lakukan dengan jenis yang tidak lengkap:

  • Gunakan salah satu instantiasinya sebagai kelas dasar

    class Foo : X<int> {} // compiler error!
  • Gunakan salah satu instantiasinya untuk mendeklarasikan anggota:

    class Foo {
        X<int> m; // compiler error!
    };
    
  • Tetapkan fungsi atau metode menggunakan salah satu instantiasinya

    void      f1(X<int> x) {}            // compiler error!
    X<int>    f2() {return X<int>(); }   // compiler error!
    
  • Gunakan metode atau bidang dari salah satu instantiasinya, pada kenyataannya mencoba meredereferensi variabel dengan tipe tidak lengkap

    class Foo {
        X<int>* m;            
        void method()            
        {
            m->someMethod();      // compiler error!
            int i = m->someField; // compiler error!
        }
    };
    
  • Buat instantiasi eksplisit dari templat kelas

    template struct X<int>;

2
"Sejauh ini tidak ada jawaban yang menjelaskan kapan seseorang dapat meneruskan pernyataan templat kelas." Bukankah itu hanya karena semantik Xdan X<int>persis sama, dan hanya sintaks yang menyatakan maju berbeda dalam cara substantif, dengan semua kecuali 1 baris jawaban Anda sama dengan hanya mengambil Luc dan s/X/X<int>/g? Apakah itu benar-benar dibutuhkan? Atau apakah saya melewatkan detail kecil yang berbeda? Itu mungkin, tetapi saya telah membandingkan beberapa kali secara visual dan tidak dapat melihat ...
underscore_d

Terima kasih! Hasil edit itu menambahkan satu ton info berharga. Saya harus membacanya beberapa kali untuk memahami sepenuhnya ... atau mungkin menggunakan taktik menunggu yang lebih baik sampai saya benar-benar bingung dengan kode asli dan kembali ke sini! Saya menduga saya akan dapat menggunakan ini untuk mengurangi ketergantungan di berbagai tempat.
underscore_d

4

Dalam file di mana Anda hanya menggunakan Pointer atau Referensi ke suatu kelas. Dan tidak ada fungsi anggota / anggota yang harus dipikirkan jika Pointer / referensi tersebut.

dengan class Foo;// meneruskan deklarasi

Kami dapat mendeklarasikan data anggota tipe Foo * atau Foo &.

Kami dapat mendeklarasikan (tetapi tidak mendefinisikan) fungsi dengan argumen, dan / atau mengembalikan nilai, dari tipe Foo.

Kami dapat mendeklarasikan anggota data statis tipe Foo. Ini karena anggota data statis didefinisikan di luar definisi kelas.


4

Saya menulis ini sebagai jawaban terpisah daripada hanya komentar karena saya tidak setuju dengan jawaban Luc Touraille, bukan karena alasan legalitas tetapi untuk perangkat lunak yang kuat dan bahaya salah tafsir.

Secara khusus, saya memiliki masalah dengan kontrak tersirat tentang apa yang Anda harapkan agar diketahui oleh pengguna antarmuka Anda.

Jika Anda mengembalikan atau menerima jenis referensi, maka Anda hanya mengatakan mereka dapat melewati pointer atau referensi yang mereka mungkin hanya tahu melalui deklarasi maju.

Ketika Anda mengembalikan jenis yang tidak lengkap X f2();maka Anda mengatakan penelepon Anda harus memiliki spesifikasi tipe lengkap X. Mereka membutuhkannya untuk membuat LHS atau objek sementara di situs panggilan.

Demikian pula, jika Anda menerima jenis yang tidak lengkap, pemanggil harus membuat objek yang merupakan parameter. Bahkan jika objek itu dikembalikan sebagai tipe lain yang tidak lengkap dari suatu fungsi, situs panggilan membutuhkan deklarasi penuh. yaitu:

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

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

Saya pikir ada prinsip penting bahwa header harus menyediakan informasi yang cukup untuk menggunakannya tanpa ketergantungan yang membutuhkan header lainnya. Itu berarti tajuk harus dapat dimasukkan dalam unit kompilasi tanpa menyebabkan kesalahan kompiler ketika Anda menggunakan fungsi yang dinyatakannya.

Kecuali

  1. Jika ketergantungan eksternal ini perilaku yang diinginkan . Alih-alih menggunakan kompilasi bersyarat Anda bisa memiliki persyaratan yang terdokumentasi dengan baik bagi mereka untuk menyediakan header mereka sendiri yang menyatakan X. Ini adalah alternatif untuk menggunakan #ifdefs dan dapat menjadi cara yang berguna untuk memperkenalkan tiruan atau varian lainnya.

  2. Perbedaan penting adalah beberapa teknik templat di mana Anda secara eksplisit TIDAK diharapkan untuk membuat instantiate, disebutkan hanya supaya seseorang tidak marah kepada saya.


"Saya pikir ada prinsip penting bahwa tajuk harus menyediakan informasi yang cukup untuk menggunakannya tanpa ketergantungan yang membutuhkan tajuk lainnya." - Masalah lain disebutkan dalam komentar oleh Adrian McCarthy pada jawaban Naveen. Itu memberikan alasan yang kuat untuk tidak mengikuti prinsip "Anda harus menyediakan informasi yang cukup untuk menggunakan" bahkan untuk tipe yang saat ini tidak digunakan.
Tony Delroy

3
Anda berbicara tentang kapan Anda harus (atau tidak seharusnya) menggunakan penerusan maju. Namun, itu sama sekali bukan poin dari pertanyaan ini. Ini adalah tentang mengetahui kemungkinan teknis ketika (misalnya) ingin memecahkan masalah ketergantungan sirkuler.
JonnyJD

1
I disagree with Luc Touraille's answerJadi, beri dia komentar, termasuk tautan ke posting blog jika Anda membutuhkannya. Ini tidak menjawab pertanyaan yang diajukan. Jika semua orang memikirkan pertanyaan tentang cara kerja X jawaban yang dibenarkan tidak setuju dengan X melakukan hal itu atau memperdebatkan batasan di mana kita harus membatasi kebebasan kita untuk menggunakan X - kita hampir tidak akan memiliki jawaban nyata.
underscore_d

3

Aturan umum yang saya ikuti adalah untuk tidak menyertakan file header apa pun kecuali saya harus. Jadi, kecuali saya menyimpan objek kelas sebagai variabel anggota kelas saya, saya tidak akan memasukkannya, saya hanya akan menggunakan deklarasi maju.


2
Ini memecah enkapsulasi dan membuat kode rapuh. Untuk melakukan ini, Anda perlu tahu apakah jenisnya adalah typedef atau kelas untuk templat kelas dengan parameter templat default, dan jika implementasinya berubah, Anda harus memperbarui tempat yang pernah Anda gunakan deklarasi penerusan.
Adrian McCarthy

@AdrianMcCarthy benar, dan solusi yang masuk akal adalah memiliki tajuk deklarasi penerusan yang disertakan oleh tajuk yang isinya meneruskan, yang harus dimiliki / dipertahankan / dikirim oleh siapa pun yang memiliki tajuk itu juga. Sebagai contoh: header pustaka iosfwd Standard, yang berisi deklarasi maju dari konten iostream.
Tony Delroy

3

Selama Anda tidak membutuhkan definisi (pikirkan petunjuk dan referensi), Anda bisa lolos dengan deklarasi maju. Inilah sebabnya mengapa sebagian besar Anda akan melihatnya di header sementara file implementasi biasanya akan menarik header untuk definisi yang tepat.


0

Anda biasanya ingin menggunakan deklarasi maju dalam file header kelas ketika Anda ingin menggunakan tipe lain (kelas) sebagai anggota kelas. Anda tidak dapat menggunakan metode kelas yang dinyatakan maju dalam file header karena C ++ belum mengetahui definisi kelas itu pada saat itu. Itu logika Anda harus pindah ke file .cpp, tetapi jika Anda menggunakan fungsi template Anda harus mengurangi mereka hanya bagian yang menggunakan template dan memindahkan fungsi itu ke header.


Ini tidak masuk akal. Seseorang tidak dapat memiliki anggota dengan tipe yang tidak lengkap. Deklarasi kelas apa pun harus memberikan semua yang perlu diketahui semua pengguna tentang ukuran dan tata letaknya. Ukurannya termasuk ukuran semua anggota non-statisnya. Pernyataan ke depan seorang anggota membuat pengguna tidak tahu ukurannya.
underscore_d

0

Ambillah bahwa deklarasi maju akan membuat kode Anda dikompilasi (obj dibuat). Namun menghubungkan (penciptaan exe) tidak akan berhasil kecuali definisi yang ditemukan.


2
Mengapa 2 orang tidak mendukung ini? Anda tidak berbicara tentang apa yang dibicarakan pertanyaan itu. Maksud Anda normal - tidak maju - deklarasi fungsi . Pertanyaannya adalah tentang deklarasi maju kelas . Seperti yang Anda katakan, "deklarasi maju akan membuat kode Anda dikompilasi", bantu saya: kompilasi class A; class B { A a; }; int main(){}, dan beri tahu saya bagaimana hasilnya. Tentu saja itu tidak akan dikompilasi. Semua jawaban yang tepat di sini menjelaskan mengapa dan tepat, konteks terbatas di mana maju-deklarasi adalah valid. Anda malah menulis ini tentang sesuatu yang sama sekali berbeda.
underscore_d

0

Saya hanya ingin menambahkan satu hal penting yang dapat Anda lakukan dengan kelas yang diteruskan tidak disebutkan dalam jawaban Luc Touraille.

Apa yang dapat Anda lakukan dengan jenis yang tidak lengkap:

Tetapkan fungsi atau metode yang menerima / mengembalikan pointer / referensi ke tipe tidak lengkap dan meneruskan pointer / referensi ke fungsi lain.

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

Modul dapat melewati objek kelas yang dinyatakan maju ke modul lain.


"kelas yang diteruskan" dan "kelas yang dideklarasikan maju" mungkin keliru untuk merujuk pada dua hal yang sangat berbeda. Apa yang Anda tulis mengikuti langsung dari konsep-konsep yang tersirat dalam jawaban Luc, jadi sementara itu akan membuat komentar yang bagus menambahkan klarifikasi terbuka, saya tidak yakin itu membenarkan jawaban.
underscore_d

0

Seperti, Luc Touraille telah menjelaskan dengan sangat baik di mana harus digunakan dan tidak menggunakan pernyataan maju kelas.

Saya hanya akan menambah alasan mengapa kita perlu menggunakannya.

Kita harus menggunakan deklarasi Teruskan sedapat mungkin untuk menghindari injeksi ketergantungan yang tidak diinginkan.

Karena #includefile header ditambahkan pada banyak file, oleh karena itu, jika kita menambahkan header ke file header lain, itu akan menambahkan injeksi ketergantungan yang tidak diinginkan di berbagai bagian kode sumber yang dapat dihindari dengan menambahkan #includeheader ke .cppfile di mana saja memungkinkan daripada menambahkan ke file header lain dan gunakan deklarasi forward class sedapat mungkin dalam .hfile header .

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.