Устранение ошибок построения из-за циклической зависимости между классами
Я часто оказываюсь в ситуации, когда я сталкиваюсь с несколькими ошибками компиляции / компоновщика в проекте C++ из-за некоторых плохих проектных решений (сделанных кем-то другим:)), которые приводят к циклическим зависимостям между классами C++ в разных заголовочных файлах (может произойти и в том же файле). Но к счастью(?) это не происходит достаточно часто, а решение этой проблемы в следующий раз это повторится.
так для целей легкого отзыва в будущем я собираюсь опубликовать репрезентативную проблему и решение вместе с ней. Лучшие решения, конечно, приветствуются.
-
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; }
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_H
File "' 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.