Устранить ошибки сборки из-за циклической зависимости между классами - PullRequest
310 голосов
/ 09 марта 2009

Я часто нахожусь в ситуации, когда я сталкиваюсь с множественными ошибками компиляции / компоновщика в проекте 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;
    }
    

Ответы [ 11 ]

252 голосов
/ 10 марта 2009

Способ думать об этом - «думать как компилятор».

Представьте, что вы пишете компилятор. И вы видите такой код.

// 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-битными (в зависимости от архитектуры), и поэтому, если вы заменили (либо 1) по указателю или по ссылке все было бы замечательно. Допустим, мы заменим в 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; 
}

НТН.

96 голосов
/ 09 марта 2009

Вы можете избежать ошибок компиляции, если вы удалите определения методов из заголовочных файлов и позволите классам содержать только объявления методов и объявления / определения переменных. Определения методов должны быть помещены в файл .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;
}
17 голосов
/ 09 марта 2009

Что нужно помнить:

  • Это не будет работать, если class A имеет объект class B в качестве члена или наоборот.
  • Форвардная декларация - это путь.
  • Порядок декларации имеет значение (именно поэтому вы убираете определения).
    • Если оба класса вызывают функции другого, вы должны удалить определения.

Прочитайте FAQ:

15 голосов
/ 23 марта 2015

Я опаздываю с ответом на этот вопрос, но на сегодняшний день не существует ни одного разумного ответа, несмотря на то, что он является популярным вопросом с высоко голосуемыми ответами. Лучшая практика: заголовки предварительной декларации Как показано в заголовке <iosfwd> Стандартной библиотеки, правильный способ предоставления предварительных объявлений для других - иметь заголовок прямой декларации . Например:

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;

... тогда перекомпиляция кода для «A» будет вызвана изменениями во включенном b.fwd.h и должна завершиться корректно.


Плохая, но распространенная практика: нападающие объявляют вещи в других библиотеках

Скажите - вместо использования заголовка прямого объявления, как описано выше - код в a.h или a.cc вместо прямого объявления class B;:

  • если a.h или a.cc включали b.h позже:
    • компиляция A завершится с ошибкой, как только она попадет в конфликтующее объявление / определение B (то есть вышеупомянутое изменение B сломало A и любые другие клиенты, злоупотребляющие форвардными объявлениями, вместо того, чтобы работать прозрачно).
  • в противном случае (если A в конечном итоге не включает b.h - возможно, если A просто хранит / передает Bs указателем и / или ссылкой)
    • инструменты сборки, основанные на анализе #include и измененных временных метках файлов, не будут перестраивать A (и его дополнительный зависимый код) после изменения на B, вызывая ошибки во время соединения или выполнения. Если B распространяется как загруженная во время выполнения библиотека DLL, код в «A» может не найти символы с различными искажениями во время выполнения, что может или не может быть обработано достаточно хорошо, чтобы вызвать упорядоченное завершение работы или приемлемо сниженную функциональность.

Если код А имеет шаблонные специализации / «черты» для старого B, они не вступят в силу.

11 голосов
/ 10 марта 2009

Однажды я решил эту проблему, переместив все строки после определения класса и поместив #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

6 голосов
/ 15 декабря 2013

Я однажды написал об этом пост: Устранение циклических зависимостей в 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;
}
3 голосов
/ 10 октября 2015

Вот решение для шаблонов: Как обрабатывать циклические зависимости с шаблонами

Ключом к решению этой проблемы является объявление обоих классов перед предоставлением определений (реализаций). Невозможно разделить объявление и определение на отдельные файлы, но вы можете структурировать их, как если бы они были в отдельных файлах.

2 голосов
/ 10 декабря 2014

Простой пример, представленный в Википедии, работал для меня. (Вы можете прочитать полное описание на 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;
}
0 голосов
/ 25 апреля 2019

К сожалению, я не могу прокомментировать ответ от geza.

Он не просто говорит "выдвигать объявления в отдельный заголовок". Он говорит, что вы должны разложить заголовки определений классов и определения встроенных функций в разные файлы заголовков, чтобы разрешить «испорченные зависимости».

Но его иллюстрация не очень хороша. Потому что оба класса (A и B) нуждаются только в неполном типе друг друга (поля / параметры указателя).

Чтобы понять это лучше, представьте, что в классе A есть поле типа B, а не B *. Кроме того, классы A и B хотят определить встроенную функцию с параметрами другого типа:

Этот простой код не будет работать:

// A.h
#pragme once
#include "B.h"

class A{
  B b;
  inline void Do(B b);
}

inline void A::Do(B b){
  //do something with B
}

// B.h
#pragme once
class A;

class B{
  A* b;
  inline void Do(A a);
}

#include "A.h"

inline void B::Do(A a){
  //do something with A
}

//main.cpp
#include "A.h"
#include "B.h"

Это приведет к следующему коду:

//main.cpp
//#include "A.h"

class A;

class B{
  A* b;
  inline void Do(A a);
}

inline void B::Do(A a){
  //do something with A
}

class A{
  B b;
  inline void Do(B b);
}

inline void A::Do(B b){
  //do something with B
}
//#include "B.h"

Этот код не компилируется, потому что B :: Do требуется полный тип A, который будет определен позже.

Чтобы убедиться, что исходный код компилируется, он должен выглядеть следующим образом:

//main.cpp
class A;

class B{
  A* b;
  inline void Do(A a);
}

class A{
  B b;
  inline void Do(B b);
}

inline void B::Do(A a){
  //do something with A
}

inline void A::Do(B b){
  //do something with B
}

Это в точности возможно с этими двумя заголовочными файлами для каждого класса, который должен определять встроенные функции. Единственная проблема заключается в том, что циклические классы не могут просто включать «публичный заголовок».

Для решения этой проблемы я хотел бы предложить расширение препроцессора: #pragma process_pending_includes

Эта директива должна отложить обработку текущего файла и завершить все ожидающие включения.

0 голосов
/ 31 марта 2019

В некоторых случаях можно определить метод или конструктор класса B в заголовочном файле класса A для разрешения циклических зависимостей, включающих определения. Таким образом, вы можете избежать необходимости помещать определения в файлы .cc, например, если вы хотите реализовать библиотеку только с заголовками.

// file: a.h
#include "b.h"
struct A {
  A(const B& b) : _b(b) { }
  B get() { return _b; }
  B _b;
};

// note that the get method of class B is defined in a.h
A B::get() {
  return A(*this);
}

// file: b.h
class A;
struct B {
  // here the get method is only declared
  A get();
};

// file: main.cc
#include "a.h"
int main(...) {
  B b;
  A a = b.get();
}

...