Sınıflar arasındaki dairesel bağımlılık nedeniyle derleme hatalarını çözme


353

Sık sık farklı başlık dosyalarında C ++ sınıfları arasında dairesel bağımlılıklara yol açan bazı kötü tasarım kararları (başka biri tarafından yapılan :) nedeniyle bir C ++ projesinde birden fazla derleme / bağlayıcı hatasıyla karşılaştığım bir durumda kendimi buluyorum (ayrıca olabilir) aynı dosyada) . Ama neyse ki (?) Bir dahaki sefere bu sorunun çözümünü hatırlamam için yeterli değil.

Gelecekte kolay hatırlama amacıyla, temsili bir sorun ve bununla birlikte bir çözüm göndereceğim. Daha iyi çözümler elbette kabul edilir.


  • A.h

    class B;
    class A
    {
        int _val;
        B *_b;
    public:
    
        A(int val)
            :_val(val)
        {
        }
    
        void SetB(B *b)
        {
            _b = b;
            _b->Print(); // COMPILER ERROR: C2027: use of undefined type 'B'
        }
    
        void Print()
        {
            cout<<"Type:A val="<<_val<<endl;
        }
    };

  • B.h

    #include "A.h"
    class B
    {
        double _val;
        A* _a;
    public:
    
        B(double val)
            :_val(val)
        {
        }
    
        void SetA(A *a)
        {
            _a = a;
            _a->Print();
        }
    
        void Print()
        {
            cout<<"Type:B val="<<_val<<endl;
        }
    };

  • main.cpp

    #include "B.h"
    #include <iostream>
    
    int main(int argc, char* argv[])
    {
        A a(10);
        B b(3.14);
        a.Print();
        a.SetB(&b);
        b.Print();
        b.SetA(&a);
        return 0;
    }

23
Visual Studio ile çalışırken, / showIncludes bayrağı bu tür sorunları ayıklamak için çok yardımcı olur.
çalışması devam ediyor

Yanıtlar:


288

Bunu düşünmenin yolu "derleyici gibi düşünmek" tir.

Bir derleyici yazdığınızı düşünün. Ve bunun gibi bir kod görüyorsunuz.

// file: A.h
class A {
  B _b;
};

// file: B.h
class B {
  A _a;
};

// file main.cc
#include "A.h"
#include "B.h"
int main(...) {
  A a;
}

.Cc dosyasını derlerken ( .h'nin değil .cc'nin derleme birimi olduğunu unutmayın ), nesne için alan ayırmanız gerekir A. Peki, o zaman ne kadar alan var? Saklamak için yeterli B! O zaman büyüklüğü nedir B? Saklamak için yeterli A! Hata.

Kesinlikle kırmanız gereken dairesel bir referans.

Derleyicinin açık işaretçiler ve referanslar hakkında bildiği kadar yer ayırmasına izin vererek onu kırabilirsiniz, örneğin, her zaman 32 veya 64 bit (mimariye bağlı olarak) olacaktır ve bir işaretçi veya referans, işler harika olurdu. Diyelim ki yerini alıyoruz A:

// file: A.h
class A {
  // both these are fine, so are various const versions of the same.
  B& _b_ref;
  B* _b_ptr;
};

Şimdi işler daha iyi. Biraz. main()hala diyor ki:

// file: main.cc
#include "A.h"  // <-- Houston, we have a problem

#include, tüm uzantılar ve amaçlar için (önişlemciyi çıkarırsanız) dosyayı yalnızca .cc'ye kopyalar . Gerçekten, .cc şöyle görünür:

// file: partially_pre_processed_main.cc
class A {
  B& _b_ref;
  B* _b_ptr;
};
#include "B.h"
int main (...) {
  A a;
}

Derleyicinin bununla neden başa çıkamayacağını görebilirsiniz - ne Bolduğu hakkında hiçbir fikri yoktur - daha önce hiç sembolü görmemişti.

Derleyiciye anlatalım B. Bu ileri bir bildiri olarak bilinir ve bu cevapta daha ayrıntılı olarak ele alınmaktadır .

// main.cc
class B;
#include "A.h"
#include "B.h"
int main (...) {
  A a;
}

Bu işe yarıyor . O değil harika . Ancak bu noktada, döngüsel referans problemini ve düzeltmeyi kötü de olsa "düzeltmek" için ne yaptığımızı anlamalısınız.

Bu düzeltmenin kötü olmasının nedeni, bir sonraki kişinin kullanmadan önce #include "A.h"beyan etmesi Bve korkunç bir #includehata almasıdır. O halde bildirgeyi Ah'in kendisine taşıyalım .

// file: A.h
class B;
class A {
  B* _b; // or any of the other variants.
};

Ve Bh'de , bu noktada, #include "A.h"doğrudan doğrudan yapabilirsiniz .

// file: B.h
#include "A.h"
class B {
  // note that this is cool because the compiler knows by this time
  // how much space A will need.
  A _a; 
}

HTH.


20
"Derleyiciye B hakkında bilgi vermek" B'nin ileri bir bildirimi olarak bilinir.
Peter Ajtai

8
Aman Tanrım! referansların işgal edilen alan açısından bilindiği gerçeğini tamamen kaçırdı. Son olarak, şimdi düzgün bir şekilde tasarlayabilirim!
kellogs

47
Ama yine de B'de herhangi bir işlevi kullanamazsınız (_b-> Printt () sorusunda olduğu gibi)
rank1

3
Yaşadığım sorun bu. Başlık dosyasını tamamen yeniden yazmadan işlevleri ileri bildirim ile nasıl getirebilirsiniz?
sydan


101

Üstbilgi dosyalarından yöntem tanımlarını kaldırır ve sınıfların yalnızca yöntem bildirimlerini ve değişken bildirimlerini / tanımlarını içermesine izin verirseniz derleme hatalarından kaçınabilirsiniz. Yöntem tanımları bir .cpp dosyasına yerleştirilmelidir (en iyi uygulama kılavuzunun söylediği gibi).

Aşağıdaki çözümün aşağı tarafı (bunları satır içi yapmak için üstbilgi dosyasına yöntemleri yerleştirmiş olduğunuzu varsayarak) yöntemlerin derleyici tarafından artık satır içi olmadığı ve inline anahtar sözcüğünü kullanmaya çalışarak bağlantı hataları üretir.

//A.h
#ifndef A_H
#define A_H
class B;
class A
{
    int _val;
    B* _b;
public:

    A(int val);
    void SetB(B *b);
    void Print();
};
#endif

//B.h
#ifndef B_H
#define B_H
class A;
class B
{
    double _val;
    A* _a;
public:

    B(double val);
    void SetA(A *a);
    void Print();
};
#endif

//A.cpp
#include "A.h"
#include "B.h"

#include <iostream>

using namespace std;

A::A(int val)
:_val(val)
{
}

void A::SetB(B *b)
{
    _b = b;
    cout<<"Inside SetB()"<<endl;
    _b->Print();
}

void A::Print()
{
    cout<<"Type:A val="<<_val<<endl;
}

//B.cpp
#include "B.h"
#include "A.h"
#include <iostream>

using namespace std;

B::B(double val)
:_val(val)
{
}

void B::SetA(A *a)
{
    _a = a;
    cout<<"Inside SetA()"<<endl;
    _a->Print();
}

void B::Print()
{
    cout<<"Type:B val="<<_val<<endl;
}

//main.cpp
#include "A.h"
#include "B.h"

int main(int argc, char* argv[])
{
    A a(10);
    B b(3.14);
    a.Print();
    a.SetB(&b);
    b.Print();
    b.SetA(&a);
    return 0;
}

Teşekkürler. Bu sorunu kolayca çözdü. Ben sadece dairesel içerir .cpp dosyaları taşındı.
Lenar Hoyt

3
Bir şablon yönteminiz varsa ne olur? Ardından, şablonları manuel olarak başlatmazsanız gerçekten bir CPP dosyasına taşıyamazsınız.
Malcolm

Her zaman "Ah" ve "Bh" ifadelerini birlikte dahil edersiniz. Neden "Ah" kelimesine "Ah" ifadesini ve sonra "A.cpp" ve "B.cpp" ifadelerine yalnızca "Bh" eklemiyorsunuz?
Gusev Slava

28

Buna geç cevap veriyorum, ancak son derece güncel cevapları olan popüler bir soru olmasına rağmen bugüne kadar makul bir cevap yok ....

En iyi yöntem: ileri bildirim başlıkları

Standart kitaplığın <iosfwd>başlığı tarafından gösterildiği gibi , başkaları için ileri bildirimler sağlamanın doğru yolu, ileri bir bildirim üstbilgisine sahip olmaktır . Örneğin:

a.fwd.h:

#pragma once
class A;

Ah:

#pragma once
#include "a.fwd.h"
#include "b.fwd.h"

class A
{
  public:
    void f(B*);
};

b.fwd.h:

#pragma once
class B;

bh:

#pragma once
#include "b.fwd.h"
#include "a.fwd.h"

class B
{
  public:
    void f(A*);
};

İçinde sürdüren Ave Börneğin - - kütüphaneler her böylece onların başlıkları ve uygulama dosyaları ile senkronize onların ileri beyanı başlıklarını tutulmasından sorumlu olmalıdır "B" nin sürdürücü ortaya çıkınca ve kod olmaya yeniden yazar eğer ...

b.fwd.h:

template <typename T> class Basic_B;
typedef Basic_B<char> B;

bh:

template <typename T>
class Basic_B
{
    ...class definition...
};
typedef Basic_B<char> B;

... daha sonra "A" kodunun yeniden derlenmesi, dahil edilen değişikliklerle tetiklenir b.fwd.hve temiz şekilde tamamlanır.


Zayıf ama yaygın uygulama: diğer kitaplarda ileri bildirim

Diyelim ki - yukarıda açıklandığı gibi bir ileri bildirim üstbilgisi kullanmak yerine, kendisini ileri a.hveya a.ccgeri bildirir class B;:

  • eğer a.hya a.ccda hesaba katılmamaktadır b.hsonra:
    • A'nın derlenmesi, çelişkili bildirime / tanımına ulaştığında bir hatayla sona erer B(örn., B'nin A'yı kırdığı ve saydam olarak çalışmak yerine ileri bildirimleri kötüye kullanan diğer tüm müşteriler).
  • Aksi takdirde (A sonunda içermiyorsa b.h- A, işaretçilerin ve / veya referansın yanında Bs'ı depolar / geçirirse)
    • #includeanalize ve değiştirilen dosya zaman damgalarına dayanan derleme araçları A, B'deki değişiklikten sonra yeniden oluşturulmaz (ve diğer bağımlı kodunu), bağlantı zamanında veya çalışma zamanında hatalara neden olur. B, çalışma zamanı yüklü bir DLL dosyası olarak dağıtılırsa, "A" içindeki kod, çalışma zamanında farklı şekilde yönetilen sembolleri bulamayabilir;

A kodunun eskisi için şablon uzmanlıkları / "özellikleri" varsa B, geçerli olmazlar.


2
Bu, ileri bildirimleri ele almanın gerçekten temiz bir yoludur. Tek "dezavantaj" ekstra dosyalarda olacaktır. Ben her zaman dahil farz a.fwd.hiçinde a.honlar senkronize durumda temin etmek. Bu sınıfların kullanıldığı yerde örnek kod eksik. a.hve b.hher ikisinin de dahil edilmesi gerekecek, çünkü ayrı ayrı işlev görmeyecekler: `` //main.cpp #include "ah" #incelik "bh" int main () {...} `` `` Veya bunlardan biri açılış sorusu gibi diğerine tam olarak dahil edilmesi gerekir. Nerede b.hiçerir a.hve main.cppkapsarb.h
Farway

2
@Farway Her açıdan doğru. Göstermeyi zahmet etmedim main.cpp, ancak yorumunuzda neleri içermesi gerektiğini belgelemeniz güzel. Şerefe
Tony Delroy

1
Artıları ve eksileri nedeniyle nedenleri ve yapmamaları ile ilgili güzel ve ayrıntılı bir açıklama ile daha iyi cevaplar ...
Francis Cugler

1
@RezaHajianpour: Dairesel olsun ya da olmasın, ileri bildirimler yapmak istediğiniz tüm sınıflar için bir ileri bildirim başlığına sahip olmak mantıklıdır. Bununla birlikte, bunları yalnızca şu durumlarda isteyeceksiniz: 1) gerçek beyan dahil olmak üzere (veya daha sonra olması beklenebilir) pahalıya mal olur (örneğin, çeviri biriminizin başka türlü ihtiyaç duymayabileceği birçok başlık içerir) ve 2) müşteri kodu işaretçilerden veya nesnelere göndermelerden faydalanma olasılığı yüksektir. <iosfwd>klasik bir örnektir: birçok yerden başvurulan birkaç akış nesnesi olabilir ve <iostream>eklenecek çok şey vardır.
Tony Delroy

1
@RezaHajianpour: Sanırım doğru fikre sahipsiniz, ancak ifadenizle ilgili bir terminolojik sorun var: "sadece beyan edilecek tipe ihtiyacımız var " doğru olurdu. Beyan edilen tür , ileri bildirimin görüldüğü anlamına gelir; o oluyor tanımlanan tam tanımı ayrıştırıldı edildikten sonra (ve bunun için olabilir fazlasına ihtiyaç #includes).
Tony Delroy

20

Hatırlanacak şeyler:

  • Üye olarak veya tam tersi class Abir nesneye sahipse bu çalışmaz class B.
  • İleriye doğru bildirim yoludur.
  • Beyanname sırası önemlidir (işte bu yüzden tanımları kaldırıyorsunuz).
    • Her iki sınıf da diğerinin işlevlerini çağırıyorsa, tanımları dışarı taşımalısınız.

SSS bölümünü okuyun:


1
sağladığınız bağlantılar artık çalışmıyor, başvuracağınız yenileri biliyor musunuz?
Ramya Rao

11

Bir keresinde sınıf tanımından sonra tüm satırları taşıyarak #includeve diğer sınıfları başlık dosyasındaki satır satırlarının hemen önüne koyarak bu tür bir sorunu çözdüm . Bu şekilde, tüm tanımların + satır içi satırların satır içi çözümlenmeden önce ayarlandığından emin olunur.

Bunu yapmak, her iki (veya birden çok) başlık dosyasında bir sürü satır içi çizginin olmasını mümkün kılar. Ancak korumaları dahil etmek gerekir .

Bunun gibi

// File: A.h
#ifndef __A_H__
#define __A_H__
class B;
class A
{
    int _val;
    B *_b;
public:
    A(int val);
    void SetB(B *b);
    void Print();
};

// Including class B for inline usage here 
#include "B.h"

inline A::A(int val) : _val(val)
{
}

inline void A::SetB(B *b)
{
    _b = b;
    _b->Print();
}

inline void A::Print()
{
    cout<<"Type:A val="<<_val<<endl;
}

#endif /* __A_H__ */

... ve aynısını B.h


Neden? Sanırım zor bir soruna zarif bir çözüm ... biri satır içi istediğinde.
Satır içi

Bir kullanıcı B.hönce eklerse ne olur ?
Bay Fooz

3
Üstbilgi korumanızın ayrılmış bir tanımlayıcı kullandığını, iki bitişik alt çizgisi olan her şeyin ayrıldığını unutmayın.
Lars Viklund

6

Ben bu konuda bir kez yazdım: c + 'da dairesel bağımlılıkları çözme

Temel teknik arabirimleri kullanarak sınıfları ayırmaktır. Yani sizin durumunuzda:

//Printer.h
class Printer {
public:
    virtual Print() = 0;
}

//A.h
#include "Printer.h"
class A: public Printer
{
    int _val;
    Printer *_b;
public:

    A(int val)
        :_val(val)
    {
    }

    void SetB(Printer *b)
    {
        _b = b;
        _b->Print();
    }

    void Print()
    {
        cout<<"Type:A val="<<_val<<endl;
    }
};

//B.h
#include "Printer.h"
class B: public Printer
{
    double _val;
    Printer* _a;
public:

    B(double val)
        :_val(val)
    {
    }

    void SetA(Printer *a)
    {
        _a = a;
        _a->Print();
    }

    void Print()
    {
        cout<<"Type:B val="<<_val<<endl;
    }
};

//main.cpp
#include <iostream>
#include "A.h"
#include "B.h"

int main(int argc, char* argv[])
{
    A a(10);
    B b(3.14);
    a.Print();
    a.SetB(&b);
    b.Print();
    b.SetA(&a);
    return 0;
}

2
Arayüz kullanımının ve virtualçalışma zamanı performansının etkilendiğini lütfen unutmayın .
cemper93

4

Şablonlar için çözüm: Şablonlarla dairesel bağımlılıklar nasıl ele alınır?

Bu sorunu çözmenin ipucu, tanımları (uygulamaları) sağlamadan önce her iki sınıfı da bildirmektir. Bildirimi ve tanımı ayrı dosyalara bölmek mümkün değildir, ancak bunları ayrı dosyadaymış gibi yapılandırabilirsiniz.


2

Wikipedia'da sunulan basit örnek benim için çalıştı. (açıklamanın tamamını http://en.wikipedia.org/wiki/Circular_dependency#Example_of_circular_dependencies_in_C.2B.2B adresinde okuyabilirsiniz )

Dosya '' 'a.h' '':

#ifndef A_H
#define A_H

class B;    //forward declaration

class A {
public:
    B* b;
};
#endif //A_H

'' 'B.h' '' dosyası:

#ifndef B_H
#define B_H

class A;    //forward declaration

class B {
public:
    A* a;
};
#endif //B_H

'' 'Main.cpp' '' dosyası:

#include "a.h"
#include "b.h"

int main() {
    A a;
    B b;
    a.b = &b;
    b.a = &a;
}

1

Ne yazık ki, önceki tüm cevaplarda bazı ayrıntılar eksik. Doğru çözüm biraz hantaldır, ancak düzgün bir şekilde yapmanın tek yolu budur. Kolayca ölçeklenir, daha karmaşık bağımlılıkları da ele alır.

Tüm ayrıntıları ve kullanılabilirliği tam olarak koruyarak bunu nasıl yapabileceğiniz aşağıda açıklanmıştır:

  • çözüm başlangıçta amaçlananla tamamen aynı
  • satır içi işlevler hala satır içi
  • kullanıcıları Ave Bherhangi bir sırada Ah ve Bh içerebilir

İki dosya oluşturun, A_def.h, B_def.h. Bunlar yalnızca A've B' tanımlarını içerecektir :

// A_def.h
#ifndef A_DEF_H
#define A_DEF_H

class B;
class A
{
    int _val;
    B *_b;

public:
    A(int val);
    void SetB(B *b);
    void Print();
};
#endif

// B_def.h
#ifndef B_DEF_H
#define B_DEF_H

class A;
class B
{
    double _val;
    A* _a;

public:
    B(double val);
    void SetA(A *a);
    void Print();
};
#endif

Ve sonra Ah ve Bh bunu içerecek:

// A.h
#ifndef A_H
#define A_H

#include "A_def.h"
#include "B_def.h"

inline A::A(int val) :_val(val)
{
}

inline void A::SetB(B *b)
{
    _b = b;
    _b->Print();
}

inline void A::Print()
{
    cout<<"Type:A val="<<_val<<endl;
}

#endif

// B.h
#ifndef B_H
#define B_H

#include "A_def.h"
#include "B_def.h"

inline B::B(double val) :_val(val)
{
}

inline void B::SetA(A *a)
{
    _a = a;
    _a->Print();
}

inline void B::Print()
{
    cout<<"Type:B val="<<_val<<endl;
}

#endif

A_def.h ve B_def.h "özel" üstbilgilerdir, kullanıcılarıdır Ave Bkullanmamalıdır. Genel üstbilgi Ah ve Bh


1
Bunun Tony Delroy'un çözümüne göre herhangi bir avantajı var mı ? Her ikisi de "yardımcı" başlıklara dayanır, ancak Tony'nin küçük (sadece ileri bildirimi içerir) ve aynı şekilde çalışıyor gibi görünüyorlar (en azından ilk bakışta).
Fabio, Reinstate Monica'ya

1
Bu cevap asıl sorunu çözmez. Sadece "açıklamaları ayrı bir başlığa koy" diyor. Dairesel bağımlılığı çözmekle ilgili bir şey yok (soru A've B' tanımının mevcut olduğu bir çözüme ihtiyaç duyuyor , ileri bildirim yeterli değil).
geza

0

Bazı durumlarda , tanımları içeren dairesel bağımlılıkları çözmek için A sınıfının başlık dosyasında B sınıfının bir yöntemini veya yapıcısını tanımlamak mümkündür . Bu şekilde .cc, örneğin yalnızca başlık içeren bir kitaplık uygulamak istiyorsanız, dosyalara tanım koymak zorunda kalmazsınız .

// file: a.h
#include "b.h"
struct A {
  A(const B& b) : _b(b) { }
  B get() { return _b; }
  B _b;
};

// note that the get method of class B is defined in a.h
A B::get() {
  return A(*this);
}

// file: b.h
class A;
struct B {
  // here the get method is only declared
  A get();
};

// file: main.cc
#include "a.h"
int main(...) {
  B b;
  A a = b.get();
}

0

Ne yazık ki geza'nın cevabını yorumlayamıyorum.

Sadece "beyanları ayrı bir başlığa koy" demiyor. "Ertelenmiş bağımlılıklara" izin vermek için sınıf tanımı başlıklarını ve satır içi işlev tanımlarını farklı başlık dosyalarına dökmeniz gerektiğini söylüyor.

Ama onun resmi gerçekten iyi değil. Çünkü her iki sınıfın (A ve B) yalnızca birbirinin tamamlanmamış tipine (işaretçi alanları / parametreleri) ihtiyacı vardır.

Daha iyi anlamak için A sınıfı B * değil B tipi bir alana sahiptir. Ek olarak A ve B sınıfı, diğer türdeki parametrelerle bir satır içi işlev tanımlamak ister:

Bu basit kod çalışmaz:

// A.h
#pragme once
#include "B.h"

class A{
  B b;
  inline void Do(B b);
}

inline void A::Do(B b){
  //do something with B
}

// B.h
#pragme once
class A;

class B{
  A* b;
  inline void Do(A a);
}

#include "A.h"

inline void B::Do(A a){
  //do something with A
}

//main.cpp
#include "A.h"
#include "B.h"

Aşağıdaki kodla sonuçlanır:

//main.cpp
//#include "A.h"

class A;

class B{
  A* b;
  inline void Do(A a);
}

inline void B::Do(A a){
  //do something with A
}

class A{
  B b;
  inline void Do(B b);
}

inline void A::Do(B b){
  //do something with B
}
//#include "B.h"

Bu kod derlenmez, çünkü B :: Do daha sonra tanımlanacak tam bir A tipine ihtiyaç duyar.

Kaynak kodunu derlediğinden emin olmak için şöyle görünmelidir:

//main.cpp
class A;

class B{
  A* b;
  inline void Do(A a);
}

class A{
  B b;
  inline void Do(B b);
}

inline void B::Do(A a){
  //do something with A
}

inline void A::Do(B b){
  //do something with B
}

Bu, satır içi işlevleri tanımlaması gereken her sınıf için bu iki başlık dosyasıyla tam olarak mümkündür. Tek sorun, dairesel sınıfların sadece "genel üstbilgiyi" içerememesi.

Bu sorunu çözmek için bir önişlemci uzantısı önermek istiyorum: #pragma process_pending_includes

Bu yönerge, geçerli dosyanın işlenmesini ertelemeli ve bekleyen tüm içeriği tamamlamalıdır.

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.