Разрешить ошибки сборки из-за круговой зависимости между классами

Я часто оказываюсь в ситуации, когда перед проектами С++ я сталкиваюсь с несколькими ошибками компиляции/компоновщика из-за некоторых неправильных дизайнерских решений (сделанных кем-то другим:)), которые приводят к циклическим зависимостям между классами С++ в разных файлах заголовков ( может произойти и в том же файле). Но, к счастью (?), Этого не достаточно часто, чтобы я мог вспомнить решение этой проблемы в следующий раз, когда это произойдет снова.

Итак, в целях легкого отзыва в будущем я собираюсь опубликовать репрезентативную проблему и решение вместе с ней. Лучшие решения приветствуются.


  • 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;
    }
    

Ответ 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