Programing

클래스 간의 순환 종속성으로 인한 빌드 오류 해결

lottogame 2020. 3. 6. 08:16
반응형

클래스 간의 순환 종속성으로 인한 빌드 오류 해결


내가 프로젝트 ++은 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;
    }
    

이것을 생각하는 방법은 "컴파일러처럼 생각하는 것"입니다.

컴파일러를 작성한다고 가정하십시오. 그리고 당신은 이런 코드를 볼 수 있습니다.

// 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 파일을 컴파일 할 때 ( .h아닌 .cc 는 컴파일 단위 임을 기억하십시오 ) object에 대한 공간을 할당해야합니다 . 그렇다면 공간이 얼마나됩니까? 저장하기에 충분합니다 ! 그때 의 크기는 얼마입니까? 저장하기에 충분합니다 ! 죄송합니다.ABBA

분명히 반드시 참조해야하는 순환 참조입니다.

예를 들어, 컴파일러는 선행 아키텍처에 대해 알고있는만큼 많은 공간을 예약하여이를 깨뜨릴 수 있습니다. 예를 들어, 아키텍처 및 아키텍처에 따라 포인터 및 참조는 항상 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오류가 발생하기 때문입니다. 선언을 Ah 자체 로 옮깁니다 .

// file: A.h
class B;
class A {
  B* _b; // or any of the other variants.
};

그리고 Bh 에서는이 시점에서 #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; 
}

HTH.


헤더 파일에서 메소드 정의를 제거하고 클래스에 메소드 선언 및 변수 선언 / 정의 만 포함 시키도록하면 컴파일 오류를 피할 수 있습니다. 분석법 정의는 모범 사례 지침과 같이 .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;
}

나는 이것에 늦게 대답하고 있지만, 고도로 찬성 된 답변으로 인기있는 질문이기는하지만 현재까지 합리적인 대답은 없습니다 ....

모범 사례 : 전달 선언 헤더

표준 라이브러리의 <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;

bh :

#pragma once
#include "b.fwd.h"
#include "a.fwd.h"

class B
{
  public:
    void f(A*);
};

의 메인테이너 AB예를 들어 - - 라이브러리는 각 그래서, 자신의 헤더와 구현 파일과 동기화 자신의 앞으로 선언 헤더를 유지하기위한 책임을 져야한다 "B"의 메인테이너가 따라오고 코드로 재 작성하는 경우 ...

b.fwd.h :

template <typename T> class Basic_B;
typedef Basic_B<char> B;

bh :

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가 결국 포함하지 않은 경우-A가 b.h포인터 및 / 또는 참조로 B 주위에 저장 / 전달하는 경우 가능)
    • #include분석에 의존하는 빌드 도구 및 변경된 파일 타임 스탬프는 AB로 변경 한 후 다시 빌드되지 않으며 링크 타임 또는 런타임시 오류가 발생합니다. B가 런타임로드 DLL로 배포 된 경우 "A"의 코드는 런타임에 다르게 얽힌 기호를 찾지 못할 수 있습니다.이 기호는 순서대로 종료되거나 기능이 상당히 저하 될 정도로 충분히 처리되지 않을 수 있습니다.

A의 코드에 old에 대한 템플릿 전문화 / "특성"이 있으면 B적용되지 않습니다.


기억해야 할 것 :

  • 멤버로서의 class A오브젝트가 class B있거나 그 반대 경우 에는 작동하지 않습니다 .
  • 앞으로 선언하는 것이 좋습니다.
  • 선언 순서가 중요합니다 (이것이 정의를 옮기는 이유입니다).
    • 두 클래스가 다른 클래스의 함수를 호출하면 정의를 이동해야합니다.

FAQ를 읽으십시오 :


클래스 정의 후 모든 인라인 을 이동 하고 헤더 파일 #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


나는 이것에 대해 한 번 글을 썼습니다 : 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;
}

템플릿 솔루션은 다음과 같습니다. 템플릿 을 사용하여 순환 종속성을 처리하는 방법

이 문제를 해결하는 단서는 정의 (구현)를 제공하기 전에 두 클래스를 모두 선언하는 것입니다. 선언과 정의를 별도의 파일로 분할 할 수는 없지만 마치 별도의 파일에있는 것처럼 구성 할 수 있습니다.


Wikipedia에 제시된 간단한 예가 저에게 효과적이었습니다. ( 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;
}

불행히도, 이전의 모든 답변에 일부 세부 정보가 누락되었습니다. 올바른 해결책은 약간 성가 시지만 이것이 제대로하는 유일한 방법입니다. 또한 쉽게 확장되고 더 복잡한 종속성도 처리합니다.

모든 세부 사항과 유용성을 정확하게 유지 하면서이 작업을 수행하는 방법은 다음과 같습니다.

  • 해결책은 원래 의도 한 것과 정확히 동일합니다.
  • 인라인 함수는 여전히 인라인
  • 사용자 AB임의의 순서로 아와 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

그리고 Ah와 Bh는 이것을 포함 할 것입니다 :

// 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입니다.


어떤 경우에는 수있다 정의 된 방법 또는 정의와 관련된 순환 종속성을 해결하기위한 클래스 A의 헤더 파일 클래스 B의 생성자를. 이런 식 .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();
}


불행히도 나는 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

이 지시문은 현재 파일 처리를 지연시키고 보류중인 모든 포함을 완료해야합니다.

참고 URL : https://stackoverflow.com/questions/625799/resolve-build-errors-due-to-circular-dependency-amongst-classes



반응형