Устранение ошибок построения из-за циклической зависимости между классами


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

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.