Устранение ошибок построения из-за циклической зависимости между классами
Я часто оказываюсь в ситуации, когда я сталкиваюсь с несколькими ошибками компиляции / компоновщика в проекте C++ из-за некоторых плохих проектных решений (сделанных кем-то другим:)), которые приводят к циклическим зависимостям между классами C++ в разных заголовочных файлах (может произойти и в том же файле). Но к счастью(?) это не происходит достаточно часто, а решение этой проблемы в следующий раз это повторится.
так для целей легкого отзыва в будущем я собираюсь опубликовать репрезентативную проблему и решение вместе с ней. Лучшие решения, конечно, приветствуются.
-
A.hclass 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; }
9 ответов:
способ думать об этом "думать, как компилятор".
представьте, что вы пишите компилятор. И вы видите такой код.
// 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 файл (помните, что . cc а не .h - это единица компиляции), вам нужно выделить место для объекта
A. Ну и сколько же тогда места? Достаточно для храненияB! Какой размерBтогда? Достаточно для храненияA! Ой.ясно круговая ссылка, которую вы должны разорвать.
вы можете разбить его, разрешив компилятору вместо этого зарезервировать столько места, сколько он знает о передних указателях и ссылках, например, всегда будет 32 или 64 бита (в зависимости от архитектуры), и поэтому, если вы замените (либо один) указателем или ссылкой, Все будет отлично. Допустим, мы заменим в
A:// file: A.h class A { // both these are fine, so are various const versions of the same. B& _b_ref; B* _b_ptr; };теперь все лучше. Отчасти.
main()еще говорит:// file: main.cc #include "A.h" // <-- Houston, we have a problem
#include, для всех степеней и целей( если вы берете препроцессор) просто копирует файл в. cc. так что на самом деле .cc выглядит так:// file: partially_pre_processed_main.cc class A { B& _b_ref; B* _b_ptr; }; #include "B.h" int main (...) { A a; }вы можете понять, почему компилятор не может справиться с этим - она понятия не имеет, что
Bэто-он никогда даже не видел символ раньше.Итак, давайте расскажем компилятору о
B. Это известно как вперед декларации, и обсуждается далее в ответ.// main.cc class B; #include "A.h" #include "B.h" int main (...) { A a; }этой работает. Это не большой. Но на этом этапе вы должны иметь представление о проблеме круговой ссылки и о том, что мы сделали, чтобы "исправить" ее, хотя исправление плохо.
причина, по которой это исправление плохо, заключается в том, что следующий человек
#include "A.h"придется объявитьBпрежде чем они смогут использовать его и получат ужасный#includeошибка. Так что давайте перенесем объявление в A. h сам по себе.// file: A.h class B; class A { B* _b; // or any of the other variants. };и B. h, в этот момент, Вы можете просто
#include "A.h"напрямую.// 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; }НТН.
вы можете избежать ошибок компиляции, если удалите определения методов из файлов заголовков и разрешите классам содержать только объявления методов и объявления/определения переменных. Определения метода должны быть помещены в a .файл cpp (так же, как говорится в руководстве по лучшей практике).
нижняя сторона следующего решения (предполагая, что вы поместили методы в файл заголовка, чтобы встроить их), что методы больше не встроены компилятором и попытка использовать встроенное ключевое слово приводит к ошибкам компоновщика.
//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; }
запомните:
- Это не будет работать, если
class Aесть объектclass Bкак член или наоборот.- вперед объявление путь идти.
- порядок объявления имеет значение (именно поэтому вы перемещаете определения).
- если оба класса вызывают функции другого, вы должны переместить определения.
прочитайте FAQ:
Я когда-то решил эту проблему, перемещая все inlines после определения класса и поставить
#includeдля других классов непосредственно перед inlines в заголовочном файле. Таким образом, убедитесь, что все определения+inlines установлены до того, как будут проанализированы inlines.выполнение этого позволяет по-прежнему иметь кучу строк в обоих(или нескольких) заголовочных файлах. Но это необходимо иметь включить охранники.
такой
// 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__ */...и делать то же самое в
B.h
я поздно отвечаю на это, но на сегодняшний день нет ни одного разумного ответа, несмотря на то, что это популярный вопрос с высоко оцененными ответами....
Лучшая практика: заголовки прямого объявления
как показано в стандартной библиотеке
<iosfwd>заголовок, правильный способ предоставить форвардные объявления для других-это иметь заголовок прямого объявления. Для пример:а.переднеприводных.h:
#pragma once class A;а.ч:
#pragma once #include "a.fwd.h" #include "b.fwd.h" class A { public: void f(B*); };б.переднеприводных.h:
#pragma once class B;б.ч:
#pragma once #include "b.fwd.h" #include "a.fwd.h" class B { public: void f(A*); };разработчики
AиBбиблиотеки должны нести ответственность за сохранение своих заголовков прямого объявления в синхронизации с их заголовками и файлами реализации, поэтому - например - если сопровождающий "B" приходит и переписывает код быть...б.переднеприводных.h:
template <typename T> class Basic_B; typedef Basic_B<char> B;б.ч:
template <typename T> class Basic_B { ...class definition... }; typedef Basic_B<char> B;...тогда перекомпиляция кода для "А" будет вызвана изменениями в included
b.fwd.hи должна завершиться без ошибок.
плохая, но распространенная практика: вперед объявлять вещи в других библиотеках
скажем-вместо использования заголовка прямого объявления, как описано выше-код в
a.hилиa.ccвместо вперед-заявляетclass B;сам по себе:
- если
a.hилиa.ccвключены :
- компиляция A завершится с ошибкой, как только она доберется до конфликтующего объявления/определения
B(т. е. вышеуказанное изменение на B сломало A и любые другие клиенты, злоупотребляющие прямыми объявлениями, вместо того, чтобы работать прозрачно).- в противном случае (если A в конечном итоге не включал
b.h- возможно, если a просто хранит / передает Bs указателем и / или ссылка)
- построить инструменты, опираясь на
#includeанализ и измененные временные метки файлов не будут перестраиватьсяA(и его дальнейший зависимый код) после изменения на B, вызывая ошибки во время ссылки или во время выполнения. Если B распространяется как DLL, загруженная во время выполнения, код в "A" может не найти по-разному искаженные символы во время выполнения, которые могут или не могут быть обработаны достаточно хорошо, чтобы вызвать упорядоченное завершение работы или приемлемо уменьшить функциональность.если код A имеет шаблонные специализации / "черты" для старого
B, они не вступят в силу.
Я написал сообщение об этом один раз:разрешение циклических зависимостей в c++
основной метод заключается в разделении классов с помощью интерфейсов. Так что в вашем случае:
//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; }
вот решение для шаблонов: Как обрабатывать циклические зависимости с помощью шаблонов
ключом к решению этой проблемы является объявление обоих классов перед предоставлением определений (реализаций). Невозможно разделить объявление и определение на отдельные файлы, но вы можете структурировать их так, как если бы они были в отдельных файлах.
простой пример, представленный в Википедии, работал для меня. (вы можете прочитать полное описание по адресу http://en.wikipedia.org/wiki/Circular_dependency#Example_of_circular_dependencies_in_C.2B.2B)
файл "a. h":
#ifndef A_H #define A_H class B; //forward declaration class A { public: B* b; }; #endif //A_Hфайл "'b. h"':
#ifndef B_H #define B_H class A; //forward declaration class B { public: A* a; }; #endif //B_HFile "' main.cpp"':
#include "a.h" #include "b.h" int main() { A a; B b; a.b = &b; b.a = &a; }
к сожалению, во всех предыдущих ответах отсутствуют некоторые детали. Правильное решение немного громоздко, но это единственный способ сделать это правильно. И он легко масштабируется, обрабатывает более сложные зависимости.
вот как вы можете это сделать, точно сохраняя все детали и удобство использования:
- решение точно такое же, как изначально предполагалось
- встроенные функции по-прежнему inline
- пользователи
AиBможет включать A. H и B. H в любом порядкесоздайте два файла, A_def.h, B_def.ч. Они будут содержать только
Aи С:// 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и затем, A. h и B. h будет содержать это:
// 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 и B_def.h-это" частные " заголовки, пользователи
AиBне следует их использовать. Публичный заголовок-A. h и B. h.