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; }
c++
compiler-errors
circular-dependency
c++-faq
Otodidak
sumber
sumber
Jawaban:
Cara untuk berpikir tentang ini adalah dengan "berpikir seperti seorang penyusun".
Bayangkan Anda sedang menulis kompiler. Dan Anda melihat kode seperti ini.
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 menyimpanB
! Berapa ukurannyaB
? Cukup untuk menyimpanA
! 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
:Sekarang semuanya lebih baik. Agak.
main()
masih mengatakan:#include
, untuk semua luasan dan tujuan (jika Anda mengeluarkan preprocessor), cukup salin file ke .cc . Jadi sungguh, .cc terlihat seperti:Anda dapat melihat mengapa kompiler tidak dapat menangani hal ini - ia tidak tahu apa
B
itu - 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 .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 menyatakanB
sebelum mereka dapat menggunakannya dan akan mendapatkan#include
kesalahan yang mengerikan . Jadi mari kita pindahkan deklarasi ke Ah sendiri.Dan di Bh , pada titik ini, Anda bisa
#include "A.h"
langsung.HTH.
sumber
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.
sumber
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:
ah:
b.fwd.h:
bh:
Pemelihara perpustakaan
A
danB
masing-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:
bh:
... lalu kompilasi ulang kode untuk "A" akan dipicu oleh perubahan yang disertakan
b.fwd.h
dan 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.h
ataua.cc
alih-alih menyatakanclass B;
sendiri:a.h
ataua.cc
tidak menyertakanb.h
nanti:B
(yaitu perubahan di atas menjadi B break A dan klien lain yang menyalahgunakan deklarasi maju, alih-alih bekerja secara transparan).b.h
- mungkin jika A hanya menyimpan / melewati B oleh pointer dan / atau referensi)#include
analisis dan stempel waktu file yang diubah tidak akan dibangun kembaliA
(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.sumber
a.fwd.h
dalama.h
, untuk memastikan mereka tinggal di sync. Kode contoh tidak ada di mana kelas-kelas ini digunakan.a.h
danb.h
keduanya 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 manab.h
termasuka.h
danmain.cpp
termasukb.h
main.cpp
, tetapi Anda senang mendokumentasikan apa yang seharusnya terkandung dalam komentar Anda. Cheers<iosfwd>
adalah contoh klasik: mungkin ada beberapa aliran objek yang dirujuk dari banyak tempat, dan<iostream>
banyak yang harus dimasukkan.#include
s).Hal-hal untuk diingat:
class A
memiliki objekclass B
sebagai anggota atau sebaliknya.Baca FAQ:
sumber
Saya pernah memecahkan masalah semacam ini dengan memindahkan semua inline setelah definisi kelas dan meletakkan
#include
untuk 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
... dan melakukan hal yang sama di
B.h
sumber
B.h
terlebih dahulu?Saya sudah menulis posting tentang ini sekali: Menyelesaikan dependensi melingkar di c ++
Teknik dasarnya adalah memisahkan kelas menggunakan antarmuka. Jadi dalam kasus Anda:
sumber
virtual
memiliki dampak kinerja runtime.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.
sumber
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' '':
File '' 'b.h' '':
File '' 'main.cpp' '':
sumber
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:
A
danB
dapat menyertakan Ah dan Bh dalam urutan apa punBuat dua file, A_def.h, B_def.h. Ini hanya akan berisi
A
'danB
' s definisi:Dan kemudian, Ah dan Bh akan mengandung ini:
Perhatikan bahwa A_def.h dan B_def.h adalah header "pribadi", pengguna
A
danB
tidak boleh menggunakannya. Header publik adalah Ah dan Bhsumber
A
's danB
' tersedia, pernyataan ke depan tidak cukup).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
.cc
file, misalnya jika Anda ingin menerapkan pustaka header saja.sumber
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:
Ini akan menghasilkan kode berikut:
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:
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.
sumber