Я часто нахожусь в ситуации, когда я сталкиваюсь с множественными ошибками компиляции / компоновщика в проекте 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; }
c++
compiler-errors
circular-dependency
c++-faq
самоучка
источник
источник
Ответы:
Способ думать об этом - «думать как компилятор».
Представьте, что вы пишете компилятор. И вы видите такой код.
Когда вы компилируете файл .cc (помните, что .cc, а не .h - это единица компиляции), вам нужно выделить место для объекта
A
. Итак, сколько же места тогда? Хватит хранитьB
! Каков размерB
тогда? Хватит хранитьA
! К сожалению.Ясно, что круговая ссылка, которую вы должны сломать.
Вы можете сломать его, позволив компилятору зарезервировать столько места, сколько ему известно о начальном этапе - например, указатели и ссылки всегда будут 32 или 64 битами (в зависимости от архитектуры), и поэтому, если вы заменили (любой из них) на указатель или ссылка, все было бы здорово. Допустим, мы заменим в
A
:Теперь все стало лучше. В некотором роде.
main()
все еще говорит:#include
Для всех экстентов и целей (если вы удалите препроцессор) просто скопируйте файл в .cc . Так что на самом деле .cc выглядит так:Вы можете понять, почему компилятор не может справиться с этим - он понятия не имеет, что это
B
такое - он никогда раньше не видел этот символ.Итак, давайте расскажем компилятору о
B
. Это известно как предварительная декларация и обсуждается далее в этом ответе .Это работает . Это не здорово . Но в этот момент у вас должно быть понимание проблемы циклических ссылок и того, что мы сделали, чтобы «исправить» ее, хотя это и исправление плохо.
Причина, по которой это исправление плохое, заключается в том, что следующий человек должен
#include "A.h"
будет объявить,B
прежде чем сможет его использовать, и получит ужасную#include
ошибку. Так давайте перейдем к декларации в Ач сам.И в Bh , на данный момент, вы можете просто
#include "A.h"
напрямую.НТН.
источник
Вы можете избежать ошибок компиляции, если удалите определения методов из заголовочных файлов и позволите классам содержать только объявления методов и объявления / определения переменных. Определения методов должны быть помещены в файл .cpp (как указано в рекомендациях).
Недостаток следующего решения (при условии, что вы поместили методы в заголовочный файл для их встраивания), что методы больше не встроены компилятором, а попытка использовать ключевое слово inline приводит к ошибкам компоновщика.
источник
Я опаздываю с ответом на этот вопрос, но на сегодняшний день нет ни одного разумного ответа, несмотря на то, что он является популярным вопросом с высоко голосуемыми ответами ...
Лучшая практика: заголовки форвардной декларации
Как показано в
<iosfwd>
заголовке Стандартной библиотеки , правильный способ предоставления предварительных объявлений для других - наличие заголовка прямой декларации . Например:a.fwd.h:
ах:
b.fwd.h:
БХ:
Сопровождающие эти
A
иB
библиотек должны быть друг друг ответственности за сохранение их вперед заголовков декларации в синхронизации с их заголовками и файлами реализация, а значит - к примеру - если сопровождающие «Б» приходит и переписывает код , чтобы быть ...b.fwd.h:
БХ:
... тогда перекомпиляция кода для "A" будет вызвана изменениями во включенном
b.fwd.h
и должна завершиться чисто.Бедная, но обычная практика: вперёд объявлять вещи в других библиотеках
Скажите - вместо использования заголовка прямого объявления, как объяснено выше, - введите код
a.h
илиa.cc
вместо этого объявитеclass B;
:a.h
илиa.cc
включилb.h
позже:B
(то есть вышеупомянутое изменение в B сломало A и любые другие клиенты, злоупотребляющие предварительными объявлениями, вместо того, чтобы работать прозрачно).b.h
- возможно, если A просто хранит / передает Bs по указателю и / или ссылке)#include
анализе и измененных временных метках файлов, не будут перестраиватьсяA
(и его зависимый от кода) после изменения на B, вызывая ошибки во время соединения или выполнения. Если B распространяется как загруженная во время выполнения библиотека DLL, код в «A» может не найти символы с различными искажениями во время выполнения, что может или не может быть обработано достаточно хорошо, чтобы вызвать упорядоченное завершение работы или приемлемо сниженную функциональность.Если код А имеет шаблонные специализации / «черты» для старых
B
, они не вступят в силу.источник
a.fwd.h
вa.h
, чтобы убедиться , что они остаются синхронизированными. Пример кода отсутствует там, где используются эти классы.a.h
иb.h
оба должны будут быть включены, так как они не будут работать изолированно: `` `//main.cpp #include" ah "#include" bh "int main () {...}` `` Или один из них должен быть полностью включен в другой, как в первом вопросе. Гдеb.h
включаетa.h
иmain.cpp
включаетb.h
main.cpp
, но приятно, что вы задокументировали, что это должно содержать в вашем комментарии. Приветствия<iosfwd>
классический пример: может быть несколько потоковых объектов, на которые ссылаются из разных мест, и их<iostream>
очень много.#include
s).То, что нужно запомнить:
class A
есть объектclass B
или наоборот.Прочитайте FAQ:
источник
Однажды я решил эту проблему, переместив все строки после определения класса и поместив их
#include
для других классов непосредственно перед строками в заголовочном файле. Таким образом, убедитесь, что все определения + строки установлены перед анализом строк.Поступая так, вы можете иметь несколько строк в обоих (или нескольких) заголовочных файлах. Но необходимо иметь охранников .
Нравится
... и делать то же самое в
B.h
источник
B.h
первым?Я написал пост об этом однажды: Разрешение циклических зависимостей в C ++
Основная техника состоит в разделении классов с использованием интерфейсов. Итак, в вашем случае:
источник
virtual
оказывает влияние на производительность во время выполнения.Вот решение для шаблонов: Как обрабатывать циклические зависимости с помощью шаблонов
Ключом к решению этой проблемы является объявление обоих классов перед предоставлением определений (реализаций). Невозможно разделить объявление и определение на отдельные файлы, но вы можете структурировать их, как если бы они были в отдельных файлах.
источник
Простой пример, представленный в Википедии, работал на меня. (вы можете прочитать полное описание на http://en.wikipedia.org/wiki/Circular_dependency#Example_of_circular_dependencies_in_C.2B.2B )
Файл '' 'a.h' '':
Файл '' 'b.h' '':
Файл '' 'main.cpp' '':
источник
К сожалению, во всех предыдущих ответах отсутствуют некоторые детали. Правильное решение немного обременительно, но это единственный способ сделать это правильно. И он легко масштабируется, обрабатывает и более сложные зависимости.
Вот как вы можете это сделать, точно сохранив все детали и удобство использования:
A
иB
могут включать А и В в любом порядкеСоздайте два файла, A_def.h, B_def.h. Они будут содержать только определения
A
's иB
':И тогда А и Бх будут содержать это:
Обратите внимание, что A_def.h и B_def.h являются «частными» заголовками, пользователи
A
иB
не должны их использовать. Публичный заголовок - А и Бисточник
A
s и, предварительноеB
объявление недостаточно).В некоторых случаях можно определить метод или конструктор класса B в заголовочном файле класса A для разрешения циклических зависимостей, включающих определения. Таким образом, вы можете избежать необходимости помещать определения в
.cc
файлы, например, если вы хотите реализовать библиотеку только с заголовками.источник
К сожалению, я не могу прокомментировать ответ от Геза.
Он не просто говорит «выдвигать декларации в отдельный заголовок». Он говорит, что вы должны разложить заголовки определений классов и определения встроенных функций в разные заголовочные файлы, чтобы разрешить «отклоненные зависимости».
Но его иллюстрация не очень хороша. Потому что оба класса (A и B) нуждаются только в неполном типе друг друга (поля / параметры указателя).
Чтобы понять это лучше, представьте, что класс A имеет поле типа B, а не B *. Кроме того, классы A и B хотят определить встроенную функцию с параметрами другого типа:
Этот простой код не будет работать:
Это приведет к следующему коду:
Этот код не компилируется, потому что B :: Do требуется полный тип A, который будет определен позже.
Чтобы убедиться, что он компилирует исходный код, должен выглядеть так:
Это точно возможно с этими двумя заголовочными файлами для каждого класса, который должен определять встроенные функции. Единственная проблема заключается в том, что циклические классы не могут просто включать «публичный заголовок».
Чтобы решить эту проблему, я хотел бы предложить расширение препроцессора:
#pragma process_pending_includes
Эта директива должна отложить обработку текущего файла и завершить все ожидающие включения.
источник