Mengatasi galat build karena ketergantungan melingkar di antara kelas

353

Saya sering menemukan diri saya dalam situasi di mana saya menghadapi banyak kesalahan kompilasi / linker dalam proyek C ++ karena beberapa keputusan desain yang buruk (dibuat oleh orang lain :)) yang menyebabkan ketergantungan melingkar antara kelas C ++ dalam file header yang berbeda (dapat terjadi juga dalam file yang sama) . Tapi untungnya (?) Ini tidak sering terjadi bagi saya untuk mengingat solusi untuk masalah ini untuk waktu berikutnya terjadi lagi.

Jadi untuk tujuan mengingat kembali mudah di masa depan saya akan memposting masalah yang representatif dan solusi bersama dengannya. Tentu saja solusi yang lebih baik diterima.


  • 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;
    }
    
Otodidak
sumber
23
Saat bekerja dengan Visual Studio, flag / showIncludes banyak membantu untuk men-debug masalah seperti ini.
usap

Jawaban:

288

Cara untuk berpikir tentang ini adalah dengan "berpikir seperti seorang penyusun".

Bayangkan Anda sedang menulis kompiler. Dan Anda melihat kode seperti ini.

// 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;
}

Ketika Anda mengkompilasi file .cc (ingat bahwa .cc dan bukan .h adalah unit kompilasi), Anda perlu mengalokasikan ruang untuk objek A. Jadi, berapa ruang yang dibutuhkan? Cukup untuk menyimpan B! Berapa ukurannya B? Cukup untuk menyimpan A! Ups.

Jelas referensi melingkar yang harus Anda hancurkan.

Anda dapat memecahnya dengan membiarkan kompiler sebagai gantinya cadangan ruang sebanyak yang diketahui tentang dimuka - pointer dan referensi, misalnya, akan selalu menjadi 32 atau 64 bit (tergantung pada arsitektur) dan jadi jika Anda mengganti (salah satu) dengan sebuah penunjuk atau referensi, semuanya akan menjadi hebat. Katakanlah kita ganti A:

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

Sekarang semuanya lebih baik. Agak. main()masih mengatakan:

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

#include, untuk semua luasan dan tujuan (jika Anda mengeluarkan preprocessor), cukup salin file ke .cc . Jadi sungguh, .cc terlihat seperti:

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

Anda dapat melihat mengapa kompiler tidak dapat menangani hal ini - ia tidak tahu apa Bitu - ia bahkan belum pernah melihat simbol sebelumnya.

Jadi mari kita beri tahu kompilator tentang B. Ini dikenal sebagai deklarasi maju , dan dibahas lebih lanjut dalam jawaban ini .

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

Ini bekerja . Itu tidak bagus . Tetapi pada titik ini Anda harus memiliki pemahaman tentang masalah referensi melingkar dan apa yang kami lakukan untuk "memperbaikinya", meskipun perbaikannya buruk.

Alasan perbaikan ini buruk adalah karena orang berikutnya #include "A.h"harus menyatakan Bsebelum mereka dapat menggunakannya dan akan mendapatkan #includekesalahan yang mengerikan . Jadi mari kita pindahkan deklarasi ke Ah sendiri.

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

Dan di Bh , pada titik ini, Anda bisa #include "A.h"langsung.

// 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.

Roosh
sumber
20
"Memberitahu kompiler tentang B" dikenal sebagai deklarasi maju B.
Peter Ajtai
8
Oh Tuhan! benar-benar merindukan fakta bahwa referensi dikenal dalam hal ruang yang ditempati. Akhirnya, sekarang saya bisa mendesain dengan benar!
Kellogs
47
Tetapi Anda masih tidak dapat menggunakan fungsi apa pun pada B (seperti dalam pertanyaan _b-> Printt ())
rank1
3
Ini adalah masalah yang saya alami. Bagaimana Anda membawa fungsi dengan deklarasi maju tanpa sepenuhnya menulis ulang file header?
sydan
101

Anda dapat menghindari kesalahan kompilasi jika Anda menghapus definisi metode dari file header dan membiarkan kelas hanya berisi deklarasi metode dan deklarasi / definisi variabel. Definisi metode harus ditempatkan dalam file .cpp (seperti yang tercantum dalam panduan praktik terbaik).

Sisi bawah dari solusi berikut adalah (dengan asumsi bahwa Anda telah menempatkan metode dalam file header untuk sebaris mereka) bahwa metode tidak lagi diuraikan oleh kompiler dan mencoba menggunakan kata kunci inline menghasilkan kesalahan linker.

//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;
}
Otodidak
sumber
Terima kasih. Ini memecahkan masalah dengan mudah. Saya hanya memindahkan bundaran ke file .cpp.
Lenar Hoyt
3
Bagaimana jika Anda memiliki metode templat? Maka Anda tidak bisa benar-benar memindahkannya ke file CPP kecuali Anda membuat instantiate template secara manual.
Malcolm
Anda selalu menyertakan "Ah" dan "Bh" bersama. Mengapa Anda tidak memasukkan "Ah" dalam "Bh" dan kemudian hanya memasukkan "Bh" dalam "A.cpp" dan "B.cpp"?
Gusev Slava
28

Saya terlambat menjawab ini, tetapi tidak ada satu jawaban yang masuk akal sampai saat ini, meskipun ada pertanyaan populer dengan jawaban yang sangat tervvotasikan ....

Praktik terbaik: meneruskan deklarasi header

Seperti yang diilustrasikan oleh <iosfwd>tajuk pustaka standar , cara yang tepat untuk menyediakan deklarasi maju untuk orang lain adalah memiliki tajuk deklarasi penerusan . Sebagai contoh:

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*);
};

Pemelihara perpustakaan Adan Bmasing-masing harus bertanggung jawab untuk menjaga header deklarasi maju mereka disinkronkan dengan header dan file implementasi mereka, jadi - misalnya - jika pengelola "B" datang dan menulis ulang kode yang akan ...

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;

... lalu kompilasi ulang kode untuk "A" akan dipicu oleh perubahan yang disertakan b.fwd.hdan harus diselesaikan dengan bersih.


Praktik buruk tapi umum: maju menyatakan hal-hal di lib lain

Katakan - alih-alih menggunakan header deklarasi maju seperti yang dijelaskan di atas - kode dalam a.hatau a.ccalih-alih menyatakan class B;sendiri:

  • jika a.hatau a.cctidak menyertakan b.hnanti:
    • kompilasi A akan diakhiri dengan kesalahan begitu sampai pada deklarasi / definisi yang saling bertentangan B(yaitu perubahan di atas menjadi B break A dan klien lain yang menyalahgunakan deklarasi maju, alih-alih bekerja secara transparan).
  • jika tidak (jika A pada akhirnya tidak termasuk b.h- mungkin jika A hanya menyimpan / melewati B oleh pointer dan / atau referensi)
    • alat bangun yang mengandalkan #includeanalisis dan stempel waktu file yang diubah tidak akan dibangun kembali A(dan kode ketergantungan selanjutnya) setelah perubahan ke B, menyebabkan kesalahan pada waktu tautan atau waktu berjalan. Jika B didistribusikan sebagai DLL yang dimuat runtime, kode dalam "A" mungkin gagal menemukan simbol-simbol yang hancur pada saat runtime, yang mungkin atau mungkin tidak ditangani dengan cukup baik untuk memicu shutdown yang tertib atau fungsionalitas yang berkurang yang dapat diterima.

Jika kode A memiliki spesialisasi templat / "ciri" untuk yang lama B, mereka tidak akan berlaku.

Tony Delroy
sumber
2
Ini adalah cara yang sangat bersih untuk menangani deklarasi maju. Satu-satunya "kerugian" akan berada di file tambahan. Saya berasumsi bahwa Anda selalu menyertakan a.fwd.hdalam a.h, untuk memastikan mereka tinggal di sync. Kode contoh tidak ada di mana kelas-kelas ini digunakan. a.hdan b.hkeduanya harus dimasukkan karena keduanya tidak akan berfungsi secara terpisah: `` `//main.cpp #include" ah "#include" bh "int main () {...}` `Atau salah satu dari mereka harus sepenuhnya dimasukkan dalam yang lain seperti dalam pertanyaan pembukaan. Di mana b.htermasuk a.hdan main.cpptermasukb.h
Farway
2
@ Farway Tepat dalam semua hal. Saya tidak repot-repot menunjukkan main.cpp, tetapi Anda senang mendokumentasikan apa yang seharusnya terkandung dalam komentar Anda. Cheers
Tony Delroy
1
Salah satu jawaban yang lebih baik dengan penjelasan terperinci yang bagus tentang mengapa dengan yang boleh dan tidak boleh dilakukan karena pro dan kontra ...
Francis Cugler
1
@RezaHajianpour: masuk akal untuk memiliki header deklarasi maju untuk semua kelas yang Anda inginkan deklarasi maju, melingkar atau tidak. Yang mengatakan, Anda hanya ingin mereka ketika: 1) termasuk deklarasi aktual adalah (atau dapat diantisipasi nanti menjadi) mahal (misalnya itu mencakup banyak header unit terjemahan Anda mungkin tidak perlu), dan 2) kode klien adalah kemungkinan dapat memanfaatkan pointer atau referensi ke objek. <iosfwd>adalah contoh klasik: mungkin ada beberapa aliran objek yang dirujuk dari banyak tempat, dan <iostream>banyak yang harus dimasukkan.
Tony Delroy
1
@RezaHajianpour: Saya pikir Anda memiliki ide yang tepat, tetapi ada masalah terminologis dengan pernyataan Anda: "kita hanya perlu tipe yang akan diumumkan " akan benar. Jenis yang dideklarasikan berarti deklarasi maju telah terlihat; itu didefinisikan setelah definisi lengkap telah diuraikan (dan untuk itu Anda mungkin perlu lebih banyak #includes).
Tony Delroy
20

Hal-hal untuk diingat:

  • Ini tidak akan berfungsi jika class Amemiliki objek class Bsebagai anggota atau sebaliknya.
  • Deklarasi ke depan adalah jalan yang harus ditempuh.
  • Urutan pernyataan penting (itulah sebabnya Anda memindahkan definisi).
    • Jika kedua kelas memanggil fungsi dari yang lain, Anda harus memindahkan definisi.

Baca FAQ:

secara langsung
sumber
1
tautan yang Anda berikan tidak berfungsi lagi, apakah Anda tahu yang baru untuk dirujuk?
Ramya Rao
11

Saya pernah memecahkan masalah semacam ini dengan memindahkan semua inline setelah definisi kelas dan meletakkan #includeuntuk kelas-kelas lain tepat sebelum inlines dalam file header. Dengan cara ini, pastikan semua definisi + inline diatur sebelum inline diuraikan.

Melakukan hal ini memungkinkan masih memiliki banyak inline di kedua (atau beberapa) file header. Tapi perlu memiliki penjaga .

Seperti ini

// 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__ */

... dan melakukan hal yang sama di B.h

epatel
sumber
Mengapa? Saya pikir ini solusi elegan untuk masalah rumit ... ketika orang menginginkan inline. Jika salah satu tidak ingin inlines satu tidak harus menulis kode seperti ditulis dari awal ...
epatel
Apa yang terjadi jika pengguna memasukkan B.hterlebih dahulu?
Tuan Fooz
3
Perhatikan bahwa pelindung tajuk Anda menggunakan pengidentifikasi yang dicadangkan, apa pun dengan garis bawah bersebelahan digandakan dicadangkan.
Lars Viklund
6

Saya sudah menulis posting tentang ini sekali: Menyelesaikan dependensi melingkar di c ++

Teknik dasarnya adalah memisahkan kelas menggunakan antarmuka. Jadi dalam kasus Anda:

//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;
}
Eduard Wirch
sumber
2
Harap dicatat bahwa penggunaan antarmuka dan virtualmemiliki dampak kinerja runtime.
cemper93
4

Berikut ini adalah solusi untuk templat: Cara menangani dependensi melingkar dengan templat

Petunjuk untuk menyelesaikan masalah ini adalah dengan mendeklarasikan kedua kelas sebelum memberikan definisi (implementasi). Tidak mungkin untuk membagi deklarasi dan definisi menjadi file yang terpisah, tetapi Anda dapat menyusunnya seolah-olah mereka berada di file yang terpisah.

Tatyana
sumber
2

Contoh sederhana yang disajikan di Wikipedia bekerja untuk saya. (Anda dapat membaca deskripsi lengkap di http://en.wikipedia.org/wiki/Circular_dependency#Example_of_circular_dependencies_in_C.2B.2B )

File '' 'a.h' '':

#ifndef A_H
#define A_H

class B;    //forward declaration

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

File '' 'b.h' '':

#ifndef B_H
#define B_H

class A;    //forward declaration

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

File '' 'main.cpp' '':

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

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

Sayangnya, semua jawaban sebelumnya kehilangan beberapa detail. Solusi yang tepat agak rumit, tetapi ini adalah satu-satunya cara untuk melakukannya dengan benar. Dan itu bersisik dengan mudah, menangani dependensi yang lebih kompleks juga.

Inilah cara Anda dapat melakukan ini, dengan tepat mempertahankan semua detail, dan kegunaan:

  • solusinya persis sama seperti yang dimaksudkan semula
  • fungsi sebaris masih sebaris
  • pengguna Adan Bdapat menyertakan Ah dan Bh dalam urutan apa pun

Buat dua file, A_def.h, B_def.h. Ini hanya akan berisi A'dan B' s definisi:

// 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

Dan kemudian, Ah dan Bh akan mengandung ini:

// 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

Perhatikan bahwa A_def.h dan B_def.h adalah header "pribadi", pengguna Adan Btidak boleh menggunakannya. Header publik adalah Ah dan Bh

geza
sumber
1
Apakah ini memiliki keunggulan dibandingkan solusi Tony Delroy ? Keduanya didasarkan pada header "pembantu", tetapi Tony lebih kecil (mereka hanya berisi deklarasi maju) dan mereka tampaknya bekerja dengan cara yang sama (setidaknya pada pandangan pertama).
Fabio mengatakan Reinstate Monica
1
Jawaban itu tidak menyelesaikan masalah aslinya. Itu hanya mengatakan "mengajukan deklarasi ke header yang terpisah". Tidak ada masalah tentang penyelesaian ketergantungan sirkuler (pertanyaan ini membutuhkan solusi di mana definisi A's dan B' tersedia, pernyataan ke depan tidak cukup).
geza
0

Dalam beberapa kasus dimungkinkan untuk mendefinisikan metode atau konstruktor kelas B dalam file header kelas A untuk menyelesaikan dependensi melingkar yang melibatkan definisi. Dengan cara ini Anda dapat menghindari keharusan meletakkan definisi dalam .ccfile, misalnya jika Anda ingin menerapkan pustaka header saja.

// 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();
}
jkoendev
sumber
0

Sayangnya saya tidak bisa mengomentari jawaban dari geza.

Dia tidak hanya mengatakan "mengajukan deklarasi ke header yang terpisah". Dia mengatakan bahwa Anda harus menumpahkan header definisi kelas dan definisi fungsi sebaris ke dalam file header yang berbeda untuk memungkinkan "menangguhkan dependensi".

Tetapi ilustrasinya tidak terlalu bagus. Karena kedua kelas (A dan B) hanya perlu tipe yang tidak lengkap satu sama lain (bidang pointer / parameter).

Untuk memahaminya lebih baik bayangkan bahwa kelas A memiliki bidang tipe B bukan B *. Selain itu, kelas A dan B ingin mendefinisikan fungsi sebaris dengan parameter dari tipe lainnya:

Kode sederhana ini tidak akan berfungsi:

// 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"

Ini akan menghasilkan kode berikut:

//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"

Kode ini tidak dikompilasi karena B :: Do membutuhkan tipe A lengkap yang ditentukan kemudian.

Untuk memastikan bahwa ia mengkompilasi kode sumber harus terlihat seperti ini:

//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
}

Ini sangat mungkin dilakukan dengan dua file header ini untuk setiap kelas yang perlu mendefinisikan fungsi sebaris. Satu-satunya masalah adalah bahwa kelas melingkar tidak bisa hanya menyertakan "header publik".

Untuk mengatasi masalah ini, saya ingin menyarankan ekstensi preprosesor: #pragma process_pending_includes

Arahan ini harus menunda pemrosesan file saat ini dan menyelesaikan semua termasuk pending.

Bernd Baumanns
sumber