Ne zaman bir ileri bildirim kullanabilirim?


602

Başka bir sınıfın başlık dosyasındaki bir sınıfın ileri bildirimi yapmak için izin verildiğinde tanımını arıyorum:

Bir temel sınıf için, üye olarak tutulan bir sınıf için, üye işlevine başvuru yoluyla geçirilen bir sınıf için vb. Yapmama izin veriliyor mu?


14
Ben umutsuzca bu "ne zaman yapmalıyım " olarak yeniden adlandırılmasını istiyorum ve cevaplar uygun şekilde güncellenir ...
deworde

12
@deworde Ne zaman "gerekir" dediğiniz zaman sizden fikir almak istiyorsunuz.
AturSams

@deworde, oluşturma süresini iyileştirmek ve döngüsel referanslardan kaçınmak için mümkün olduğunda ileri bildirimleri kullanmak istediğinizi anlıyorum. Düşünebileceğim tek istisna, bir içerme dosyası typedefs içerdiğinde, bu durumda typedef'i yeniden tanımlamak (ve değişme riskini almak) ile tüm dosyayı (özyinelemeli içeriklerle birlikte) dahil etmek arasında bir ödünleşimdir.
Ohad Schneider

@OhadSchneider Pratik bir bakış açısıyla, büyük bir başlık hayranı değilim. ÷
deworde

temel olarak her zaman bunları kullanmak için farklı bir başlık eklemenizi gerektirir (yapıcı parametresinin ileriye doğru
kaldırılması

Yanıtlar:


962

Kendinizi derleyicinin konumuna koyun: bir tür bildirdiğinizde, tüm derleyici bu türün var olduğunu bilir; boyutu, üyeleri veya yöntemleri hakkında hiçbir şey bilmiyor. Bu yüzden eksik tip denir . Bu nedenle, bir üye veya temel sınıf bildirmek için türü kullanamazsınız, çünkü derleyicinin tür düzenini bilmesi gerekir.

Aşağıdaki ileriye dönük beyanı varsayarsak.

class X;

İşte yapabilecekleriniz ve yapamayacaklarınız.

Tamamlanmamış bir türle yapabilecekleriniz:

  • Bir üyeyi işaretçi veya eksik türe başvuru olarak bildirin:

    class Foo {
        X *p;
        X &r;
    };
  • Eksik türleri kabul eden / döndüren işlevleri veya yöntemleri bildirme :

    void f1(X);
    X    f2();
  • Tamamlanmamış türe işaretçileri / referansları kabul eden / döndüren işlevleri veya yöntemleri tanımlayın (ancak üyelerini kullanmadan):

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

Tamamlanmamış bir türle yapamayacağınız şeyler:

  • Temel sınıf olarak kullanın

    class Foo : X {} // compiler error!
  • Bir üyeyi beyan etmek için kullanın:

    class Foo {
        X m; // compiler error!
    };
  • Bu türü kullanarak işlevleri veya yöntemleri tanımlama

    void f1(X x) {} // compiler error!
    X    f2()    {} // compiler error!
  • Yöntemlerini veya alanlarını kullanın, aslında tamamlanmamış türdeki bir değişkeni kaldırmaya çalışın

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

Şablonlar söz konusu olduğunda, mutlak bir kural yoktur: eksik bir türü şablon parametresi olarak kullanıp kullanamayacağınız, türün şablonda kullanılma şekline bağlıdır.

Örneğin std::vector<T>, parametresinin tam bir tür olmasını gerektirir, ancak boost::container::vector<T>öyle olmaz. Bazen, yalnızca belirli üye işlevlerini kullanıyorsanız tam bir tür gerekir; mesela bu böyledirstd::unique_ptr<T> .

İyi belgelenmiş bir şablon, belgelerinde tam tür olması gerekip gerekmediği de dahil olmak üzere parametrelerinin tüm gereksinimlerini belirtmelidir.


4
Harika cevap ama katılmıyorum mühendislik noktası için lütfen benimkine bakın. Kısacası, kabul ettiğiniz veya iade ettiğiniz eksik türler için üstbilgiler eklemezseniz, başkalarının ihtiyaç duyduklarını bilmek zorunda kalmadan başlığınızın tüketicisine görünmez bir bağımlılığı zorlarsınız.
Andy Dent

2
@AndyDent: Doğru, ancak üstbilgi tüketicisinin yalnızca gerçekte kullandığı bağımlılık (ları) içermesi gerekir; Ancak aslında, başlığın bağımsız olmasını bekleyecek olan kullanıcı için rahatsız edici olabilir.
Luc Touraille

8
Bu kurallar kümesi çok önemli bir durumu göz ardı eder: standart kitaplıktaki çoğu şablonu somutlaştırmak için tam bir türe ihtiyacınız vardır. Kurala özellikle dikkat edilmesi gerekir, çünkü kuralı ihlal etmek tanımsız davranışa neden olur ve derleyici hatasına neden olmayabilir.
James Kanze

12
+1 "kendinizi derleyicinin yerine koy". "Derleyicinin bıyıklı olduğunu hayal ediyorum.
PascalVKooten

3
@JesusChrist: Kesinlikle: bir nesneyi değere göre ilettiğinizde, derleyicinin uygun yığın manipülasyonunu yapabilmesi için boyutunu bilmesi gerekir; bir işaretçi veya başvuru iletilirken, derleyicinin nesnenin boyutuna veya düzenine ihtiyacı yoktur, yalnızca işaret edilen türe bağlı olmayan bir adresin boyutu (yani bir işaretçinin boyutu) gerekir.
Luc Touraille

45

Ana kural, yalnızca bellek düzeninin (ve dolayısıyla üye işlevleri ve veri üyelerinin) ilettiğiniz dosyada bilinmesi gerekmeyen sınıfları iletebilmenizdir.

Bu, temel sınıfları ve referanslar ve işaretçilerle kullanılan sınıflar dışında her şeyi dışlar.


6
Neredeyse. Ayrıca, işlev prototiplerinde "düz" (örn. İşaretçi olmayan / referans) eksik türlere parametre veya döndürme türü olarak başvurabilirsiniz.
j_random_hacker

Üstbilgi dosyasında tanımladığım bir sınıfın üyeleri olarak kullanmak istediğim sınıflar ne olacak? Onları iletebilir miyim?
Igor Oks

1
Evet, ancak bu durumda yalnızca ileri bildirilen sınıfa bir başvuru veya işaretçi kullanabilirsiniz. Ama yine de üyeleriniz olmasına izin veriyor.
Reunanen

32

Lakos sınıf kullanımı arasında ayrım yapar

  1. in-adı okunur (ileri bir beyan yeterli) ve
  2. boyut olarak (sınıf tanımının gerekli olduğu).

Daha özlü olarak telaffuz edildiğini hiç görmedim :)


2
Sadece isminin anlamı nedir?
Boon

4
@Boon: söylemeye cesaret edemem ...? Eğer kullanırsanız sadece sınıf adını ?
Marc Mutz - mmutz

1
Lakos, Marc için bir artı
mlvljr

28

Eksik türlere yönelik işaretçilerin ve referansların yanı sıra, eksik tür olan parametreleri ve / veya dönüş değerlerini belirten işlev prototipleri de bildirebilirsiniz. Ancak, bir işaretçi veya başvuru olmadığı sürece, tamamlanmamış bir parametre veya dönüş türüne sahip bir işlevi tanımlayamazsınız .

Örnekler:

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

Şimdiye kadar cevapların hiçbiri, bir sınıf şablonunun ne zaman ileri bir bildirimini kullanabileceğini açıklamaz. Yani, işte gidiyor.

Bir sınıf şablonu şu şekilde bildirilebilir:

template <typename> struct X;

Kabul edilen cevabın yapısını takiben ,

İşte yapabilecekleriniz ve yapamayacaklarınız.

Tamamlanmamış bir türle yapabilecekleriniz:

  • Bir üyeyi başka bir sınıf şablonundaki işaretçi veya eksik türe başvuru olarak bildirin:

    template <typename T>
    class Foo {
        X<T>* ptr;
        X<T>& ref;
    };
  • Bir üyeyi işaretçi veya eksik örneklerinden birine başvuru olarak bildirin:

    class Foo {
        X<int>* ptr;
        X<int>& ref;
    };
  • Eksik türleri kabul eden / döndüren işlev şablonları veya üye işlev şablonları bildirme:

    template <typename T>
       void      f1(X<T>);
    template <typename T>
       X<T>    f2();
  • Eksik örneklerinden birini kabul eden / döndüren işlevler veya üye işlevler bildirme:

    void      f1(X<int>);
    X<int>    f2();
  • Tamamlanmamış türe işaretçileri / referansları kabul eden / döndüren işlev şablonlarını veya üye işlev şablonlarını tanımlayın (ancak üyelerini kullanmadan):

    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; }
  • Eksik örneklerinden birine (ancak üyelerini kullanmadan) işaretçileri / referansları kabul eden / döndüren işlevleri veya yöntemleri tanımlayın:

    void      f3(X<int>*, X<int>&) {}
    X<int>&   f4(X<int>& in) { return in; }
    X<int>*   f5(X<int>* in) { return in; }
  • Başka bir şablon sınıfının temel sınıfı olarak kullanın

    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.
  • Başka bir sınıf şablonunun bir üyesini bildirmek için kullanın:

    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.
  • Bu türü kullanarak işlev şablonlarını veya yöntemleri tanımlama

    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
    }

Tamamlanmamış bir türle yapamayacağınız şeyler:

  • Örneklerinden birini temel sınıf olarak kullanma

    class Foo : X<int> {} // compiler error!
  • Bir üyeyi beyan etmek için örneklerinden birini kullanın:

    class Foo {
        X<int> m; // compiler error!
    };
  • Örneklerinden birini kullanarak işlevleri veya yöntemleri tanımlama

    void      f1(X<int> x) {}            // compiler error!
    X<int>    f2() {return X<int>(); }   // compiler error!
  • Gerçekleştirilen türlerden birinin yöntemlerini veya alanlarını kullanın, aslında tamamlanmamış türdeki bir değişkeni kaldırmaya çalışın

    class Foo {
        X<int>* m;            
        void method()            
        {
            m->someMethod();      // compiler error!
            int i = m->someField; // compiler error!
        }
    };
  • Sınıf şablonunun açık örneklerini oluşturma

    template struct X<int>;

2
"Şimdiye kadar cevapların hiçbiri, bir sınıf şablonunun ne zaman iletilebileceğini açıklamıyor." Is not anlambilimleri çünkü o Xve X<int>tam olarak aynıdır ve tüm ile herhangi bir ehemmiyetli bir şekilde sadece ileriye ilan sözdizimi farklıdır, ama sadece Luc en ve alarak cevabınız tutarında 1 satır s/X/X<int>/g? Gerçekten gerekli mi? Yoksa küçük bir detayı kaçırdım mı? Mümkün, ancak görsel olarak birkaç kez karşılaştırdım ve göremiyorum ...
underscore_d

Teşekkür ederim! Bu düzenleme bir ton değerli bilgi ekliyor. Tam olarak anlamak için birkaç kez okumak zorunda kalacağım ... ya da gerçek kodda çok karışana ve buraya geri dönene kadar beklemenin daha iyi taktiğini kullanmalıyım! Çeşitli yerlerde bağımlılıkları azaltmak için bunu kullanabileceğimden şüpheleniyorum.
underscore_d

4

İçinde yalnızca İşaretçi'yi veya bir sınıfa Başvuruyu kullandığınız dosyada ve bu İşaretçi / başvuruyu düşünerek hiçbir üye / üye işlevi çağrılmamalıdır.

ile class Foo; // ileri beyanı

Foo * veya Foo & türündeki veri üyelerini bildirebiliriz.

İşlevleri Foo türünde argümanlar ve / veya döndürme değerleri ile bildirebiliriz (ancak tanımlayamayız).

Foo türündeki statik veri üyelerini bildirebiliriz. Bunun nedeni, statik veri üyelerinin sınıf tanımının dışında tanımlanmasıdır.


4

Bunu sadece bir yorumdan ziyade ayrı bir cevap olarak yazıyorum çünkü yasallık temelinde değil, sağlam yazılım ve yanlış yorumlanma tehlikesi nedeniyle Luc Touraille'in cevabına katılmıyorum.

Özellikle, arayüzünüzün kullanıcılarının bilmesini beklediğiniz şeyle ilgili zımni sözleşme ile ilgili bir sorunum var.

Referans türlerini iade ediyorsanız veya kabul ediyorsanız, sadece bir işaretçi veya referanstan geçebildiklerini söylüyorsunuz, bu da sadece ileri bir bildirim yoluyla bildikleri anlamına gelir.

Eğer tamamlanmamış bir türü döndüren zaman X f2();sonra arayan söylediğini mutlaka X'in tam tür özelliklerine sahip Çağrı sitesinde LHS veya geçici nesne oluşturmak için buna ihtiyaç duyuyorlar.

Benzer şekilde, eksik bir türü kabul ederseniz, arayanın parametre olan nesneyi oluşturması gerekir. Bu nesne bir işlevden eksik başka bir tür olarak döndürülse bile, çağrı sitesi tam bildirime ihtiyaç duyar. yani:

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

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

Bir üstbilginin, diğer üstbilgileri gerektiren bir bağımlılık olmadan kullanmak için yeterli bilgi sağlaması gerektiği önemli bir ilke olduğunu düşünüyorum. Bu, bildirdiği herhangi bir işlevi kullandığınızda başlık derleyici birimine derleme hatasına neden olmadan eklenebileceği anlamına gelir.

Dışında

  1. Bu dış bağımlılık isteniyorsa davranış. Koşullu derleme kullanmak yerine, X bildiren kendi başlıklarını sağlamaları için iyi belgelenmiş bir gereksiniminiz olabilir.

  2. Önemli bir ayrım, onları somutlaştırmamanız gereken bazı şablon teknikleridir, sadece birisinin benimle sersemlememesi için bahsedilmiştir.


"Bir başlığın, diğer üstbilgileri gerektirecek bir bağımlılık olmadan kullanmak için yeterli bilgi sağlamasının önemli bir ilke olduğunu düşünüyorum." - Naveen'in cevabı üzerine Adrian McCarthy'nin yaptığı bir yorumda başka bir konudan bahsediliyor. Bu, şu anda şablonlanmamış tipler için bile "kullanmak için yeterli bilgi sağlamalıdır" ilkenize uymamanız için sağlam bir neden sağlar.
Tony Delroy

3
Sen ne zaman bahsediyorsun gerekir (veya gerekmeyen) ileri Beyannamesi kullanın. Yine de bu sorunun asıl amacı bu değil. Bu, dairesel bir bağımlılık sorununu (örneğin) kırmak istediğinde teknik olasılıkları bilmekle ilgilidir.
JonnyJD

1
I disagree with Luc Touraille's answerBu yüzden uzunluğa ihtiyacınız varsa ona bir blog yazısı bağlantısı da dahil olmak üzere bir yorum yazın. Bu, sorulan soruya cevap vermiyor. Herkes X'in nasıl çalıştığı ile ilgili soruları düşünürse, X'in bunu yapmasına katılmama veya X'i kullanma özgürlüğümüzü kısıtlamamız gereken sınırlama tartışmalarına katılmamaya karar verirse - neredeyse gerçek cevaplarımız olmazdı.
underscore_d

3

İzlediğim genel kural, zorunlu olmadıkça herhangi bir başlık dosyası eklememektir. Bu yüzden, bir sınıfın nesnesini sınıfımın üye değişkeni olarak saklamıyorsam, onu dahil etmeyeceğim, sadece ileri bildirimi kullanacağım.


2
Bu kapsüllemeyi keser ve kodu kırılgan hale getirir. Bunu yapmak için, türün varsayılan şablon parametrelerine sahip bir sınıf şablonu için bir typedef mi yoksa bir sınıf mı olduğunu bilmeniz gerekir ve uygulama hiç değişiyorsa, bir ileri bildirim kullandığınız yeri güncellemeniz gerekir.
Adrian McCarthy

@AdrianMcCarthy haklıdır ve makul bir çözüm, ilettiği içeriği bildirdiği başlık tarafından dahil edilen ve bu başlığa sahip olanın sahibi olması / bakımı / gönderilmesi gereken ileri bildirim başlığına sahip olmaktır. Örneğin: iostream içeriğinin ileri bildirimlerini içeren iosfwd Standart kitaplık başlığı.
Tony Delroy

3

Tanıma ihtiyaç duymadığınız sürece (düşünme işaretçileri ve referanslar) ileri bildirimlerden kurtulabilirsiniz. Uygulama dosyaları genellikle uygun tanım (lar) için üstbilgiyi alırken, çoğunlukla onları başlıklarda görürsünüz.


0

Sınıfın bir üyesi olarak diğer türü (sınıf) kullanmak istediğinizde genellikle bir sınıf üstbilgisi dosyasında ileri bildirimi kullanmak isteyeceksiniz. Üstbilgi dosyasında ileri bildirilen sınıflar yöntemlerini kullanamazsınız, çünkü C ++ o sınıfın tanımını o noktada henüz bilmiyor. Bu, .cpp dosyalarına taşınmanız gereken mantıktır, ancak şablon işlevlerini kullanıyorsanız, bunları yalnızca şablonu kullanan parçaya indirmeli ve bu işlevi başlığa taşımalısınız.


Bu anlamlı değil. Eksik türden bir üye olamaz. Herhangi bir sınıfın beyanı, tüm kullanıcıların büyüklüğü ve düzeni hakkında bilmeleri gereken her şeyi sağlamalıdır. Boyutu, statik olmayan tüm üyelerinin boyutlarını içerir. Bir üyeyi ileriye beyan etmek, kullanıcıları kendi büyüklüğü hakkında hiçbir fikre sahip değildir.
underscore_d

0

İleri bildirimin kodunuzun derlenmesini sağlayacağını düşünün (obj oluşturulur). Bununla birlikte, tanımlar bulunmadıkça (exe oluşturma) bağlantı başarılı olmaz.


2
Neden 2 kişi bunu onayladı? Sorunun neden bahsettiği hakkında konuşmuyorsunuz. Fonksiyonların normal - ileri değil - beyanı demek istediniz . Soru, sınıfların ileri bildirimi ile ilgilidir . Söylediğiniz gibi "ileri bildirim kodunuzu derleyecek", bana bir iyilik yapın: derleyin class A; class B { A a; }; int main(){}ve bunun nasıl olduğunu bana bildirin. Tabii ki derlenmeyecek. Tüm uygun cevaplar burada niçin ve ileriye beyan ettiği hassas, sınırlı bağlamları açıklamak olduğunu geçerlidir. Bunun yerine bunu tamamen farklı bir şey hakkında yazdınız.
underscore_d

0

Luc Touraille'in cevabında belirtilmeyen bir ileri sınıfla yapabileceğiniz önemli bir şey eklemek istiyorum.

Tamamlanmamış bir türle yapabilecekleriniz:

İşaretçileri / tamamlanmamış tipe başvuruları kabul eden / döndüren işlevleri veya yöntemleri tanımlayın ve bu işaretçileri / başvuruları başka bir işleve iletin.

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

Bir modül ileri bildirilen sınıftaki bir nesneden başka bir modüle geçebilir.


"iletilen bir sınıf" ve "iletilen ileri bir sınıf" çok farklı iki şeye atıfta bulunabilir. Yazdıklarınız doğrudan Luc'ın cevabında yer alan kavramlardan kaynaklanıyor, bu yüzden açık bir açıklama ekleyerek iyi bir yorum yapmış olsa da, bir cevabı haklı çıkardığından emin değilim.
underscore_d

0

Luc Touraille, sınıfın ileri bildirimini nerede kullanacağını ve kullanmamasını çoktan açıkladı.

Sadece neden kullanmamız gerektiğini ekleyeceğim.

İstenmeyen bağımlılık enjeksiyonundan kaçınmak için mümkün olduğunda İleri bildirimi kullanmalıyız.

Gibi #includebaşlık dosyaları bu nedenle birden çok dosya üzerinde eklenir biz başka başlık dosyasına bir başlık eklerseniz, o ekleyerek önlenebilir kaynak kodu çeşitli yerlerinde istenmeyen bağımlılık enjeksiyon katacak #includeiçine başlığını .cppdosyaları mümkün olduğunca yerine başka başlık dosyasına eklenmesi ve başlık .hdosyalarında mümkün olduğunda sınıf ileri bildirimini kullanın .

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.