Разрешить ошибки сборки из-за круговой зависимости между классами
Я часто оказываюсь в ситуации, когда перед проектами С++ я сталкиваюсь с несколькими ошибками компиляции/компоновщика из-за некоторых неправильных дизайнерских решений (сделанных кем-то другим:)), которые приводят к циклическим зависимостям между классами С++ в разных файлах заголовков ( может произойти и в том же файле). Но, к счастью (?), Этого не достаточно часто, чтобы я мог вспомнить решение этой проблемы в следующий раз, когда это произойдет снова.
Итак, в целях легкого отзыва в будущем я собираюсь опубликовать репрезентативную проблему и решение вместе с ней. Лучшие решения приветствуются.
Ответы
Ответ 1
Способ думать об этом - "думать как компилятор".
Представьте, что вы пишете компилятор. И вы видите такой код.
// 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
. Это называется forward declaration и рассматривается далее в этом ответе.
// 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;
}
НТН.
Ответ 2
Вы можете избежать ошибок компиляции, если вы удалите определения методов из файлов заголовков и пусть классы содержат только декларации методов и объявления/определения переменных. Определения методов должны быть помещены в файл .cpp(как говорится в рекомендациях по лучшей практике).
Нижняя сторона следующего решения (предполагая, что вы поместили методы в файл заголовка для их встроенных), что методы больше не встроены в компилятор, и попытка использовать ключевое слово inline приводит к ошибкам компоновщика.
//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;
}
Ответ 3
То, что нужно запомнить:
- Это не будет работать, если
class A
имеет объект class B
в качестве члена или наоборот. - Передовая декларация - это путь.
- Применяется порядок декларирования (именно поэтому вы выходите из определений).
- Если оба класса называют функции другого, вы должны перенести определения.
Читайте FAQ:
Ответ 4
Однажды я решил эту проблему, перемещая все строки после определения класса и помещая #include
для других классов непосредственно перед строками в файле заголовка. Таким образом, убедитесь, что все определения + строки установлены до того, как строки развернуты.
Выполнение этого действия позволяет по-прежнему иметь множество строк в обоих (или нескольких) файлах заголовков. Но нужно включить охранников.
Подобно этому
// 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
Ответ 5
Я опоздал с ответом на этот вопрос, но на данный момент нет ни одного разумного ответа, несмотря на то, что это популярный вопрос с очень высокими ответами....
Лучшая практика: заголовки деклараций вперед
Как проиллюстрировано заголовком стандартной библиотеки <iosfwd>
, правильный способ предоставления форвардных деклараций для других должен содержать заголовок forward декларации. Например:
a.fwd.h:
#pragma once
class A;
хиджры:
#pragma once
#include "a.fwd.h"
#include "b.fwd.h"
class A
{
public:
void f(B*);
};
b.fwd.h:
#pragma once
class B;
b.h:
#pragma once
#include "b.fwd.h"
#include "a.fwd.h"
class B
{
public:
void f(A*);
};
Составители библиотек A
и B
должны отвечать за сохранение заголовков своих прямых деклараций в соответствии с их заголовками и файлами реализации, так что, например, если сопровождающий "B" приходит и перезаписывает код, который будет...
b.fwd.h:
template <typename T> class Basic_B;
typedef Basic_B<char> B;
b.h:
template <typename T>
class Basic_B
{
...class definition...
};
typedef Basic_B<char> B;
... тогда перекомпиляция кода для "А" будет вызвана изменениями в включенном b.fwd.h
и должна завершиться чисто.
Плохая, но обычная практика: переслать декларацию в другие библиотеки
Скажите - вместо использования заголовка прямого объявления, как описано выше, - код в a.h
или a.cc
вместо forward-declares class B;
сам:
- Если
a.h
или a.cc
включили b.h
позже:
- компиляция A завершится с ошибкой, когда она попадет в противоречивое объявление/определение
B
(т.е. приведенное выше изменение на B сломало A и любые другие клиенты, злоупотребляющие объявлениями вперед, вместо прозрачной работы).
- в противном случае (если A в конечном итоге не включил
b.h
- возможно, если A просто хранит/пропускает Bs указателем и/или ссылкой)
-
Инструменты
- основанные на анализе
#include
и измененных временных меток файла, не будут восстанавливать A
(и его зависимый от кода код) после изменения на B, вызывая ошибки во время соединения или времени выполнения. Если B распределяется как загружаемая DLL во время выполнения, код в "A" может не отображать символы с разным образом во время выполнения, которые могут обрабатываться или не обрабатываться достаточно хорошо, чтобы инициировать упорядоченное завершение работы или приемлемо уменьшенную функциональность.
Если код имеет специализированные шаблоны/ "признаки" для старого B
, они не вступят в силу.
Ответ 6
Я написал сообщение об этом один раз: Разрешение круговых зависимостей в С++
Основной метод состоит в том, чтобы отделить классы, используя интерфейсы. Итак, в вашем случае:
//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;
}
Ответ 7
Вот решение для шаблонов: Как обрабатывать круговые зависимости с шаблонами
Ключ к решению этой проблемы состоит в том, чтобы объявить оба класса до предоставления определений (реализаций). Невозможно разбить объявление и определение на отдельные файлы, но вы можете структурировать их так, как если бы они были в отдельных файлах.
Ответ 8
Простой пример, представленный в Википедии, работал у меня.
(вы можете прочитать полное описание в 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
Файл '' 'main.cpp' '':
#include "a.h"
#include "b.h"
int main() {
A a;
B b;
a.b = &b;
b.a = &a;
}
Ответ 9
К сожалению, во всех предыдущих ответах отсутствуют некоторые детали. Правильное решение немного громоздко, но это единственный способ сделать это правильно. И он масштабируется легко, обрабатывает более сложные зависимости.
Здесь, как вы можете это сделать, точно сохраняя все детали и удобство использования:
- решение точно такое же, как и первоначально
- встроенные функции все еще встроены
- пользователи
A
и B
могут включать Ah и Bh в любом порядке
Создайте два файла: A_def.h, B_def.h. Они будут содержать только определения A
и B
:
// 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
#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
не должны их использовать. Публичный заголовок - Ah и Bh