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


353

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

23
Visual Studio로 작업 할 때 / showIncludes 플래그는 이러한 종류의 문제를 디버깅하는 데 많은 도움이됩니다.
wip

답변:


288

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

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

// 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.


20
"B에 대한 컴파일러를 알려주는"B. 순방향 선언라고도
베드로 Ajtai을

8
세상에! 점유 공간에 대한 참조가 알려져 있다는 사실을 완전히 놓쳤다. 마지막으로 이제 제대로 디자인 할 수 있습니다!
kellogs

47
하지만 여전히 당신은 (_b-> Printt ()이 문제로) B의 모든 기능을 사용할 수 없습니다
rank1

3
이것이 내가 겪고있는 문제입니다. 헤더 파일을 완전히 다시 작성하지 않고 함수를 앞으로 선언하여 어떻게 가져올 수 있습니까?
sydan


101

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

감사. 이것은 문제를 쉽게 해결했다. 원형 포함을 .cpp 파일로 옮겼습니다.
Lenar Hoyt

3
템플릿 방법이 있다면 어떻습니까? 그런 다음 템플릿을 수동으로 인스턴스화하지 않으면 실제로 CPP 파일로 이동할 수 없습니다.
Malcolm

항상 "Ah"와 "Bh"를 함께 포함하십시오. "Bh"에 "Ah"를 포함시킨 다음 "A.cpp"및 "B.cpp"모두에 "Bh"만 포함하지 않겠습니까?
구 세프 슬라바

28

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

모범 사례 : 전달 선언 헤더

표준 라이브러리의 <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적용되지 않습니다.


2
이것은 순방향 선언을 처리하는 정말 깨끗한 방법입니다. 유일한 "불이익" 은 여분의 파일에있을 것입니다. 나는 당신이 항상 포함 가정 a.fwd.h에서 a.h그들이 동기를 유지 보장. 이 클래스가 사용되는 예제 코드가 없습니다. a.h그리고 b.h둘 필요가 포함될 때문에 그들은 것이다 격리에서 작동하지 : "BH"```//main.cpp 사용법 #include "아"#INCLUDE INT 주 () {...}```또는 그 중 하나 시작 질문과 같이 다른 부분에 완전히 포함되어야합니다. 어디 b.h포함 a.h하고 main.cpp포함b.h
Farway

2
@Farway 맞습니다. 나는 귀찮게하지 않았지만 main.cpp주석에 포함해야 할 것을 문서화 한 것이 좋습니다. 건배
토니 델로이

1
찬반 양론으로 인해 왜,하지 말아야하는지에 대한 자세한 설명으로 더 나은 답변 중 하나입니다.
Francis Cugler

1
@RezaHajianpour : 순방향 선언을 원하는 모든 클래스에 대해 순방향 선언 헤더를 갖는 것이 좋습니다. 즉, 1) 실제 선언을 포함하여 비용이 많이 들거나 (나중에 나올 것으로 예상 될 수 있음) (예 : 번역 단위가 필요하지 않을 수있는 많은 헤더가 포함되어 있음) 2) 클라이언트 코드가 객체에 대한 포인터 또는 참조를 사용할 수 있습니다. <iosfwd>고전적인 예입니다. 많은 곳에서 참조되는 몇 개의 스트림 객체가있을 수 있으며 포함해야 할 것이 <iostream>많습니다.
Tony Delroy

1
@RezaHajianpour : 올바른 아이디어가 있다고 생각하지만, "우리 는 선언 할 형식 만 있으면됩니다"라는 말에 용어 문제 가 있습니다. 선언 되는 유형은 정방향 선언이 표시되었음을 의미합니다. 그것은 것 정의 전체 정의 구문 분석 된 후에 (그리고 당신은 할 수 있습니다 더 필요 #include들).
Tony Delroy

20

기억해야 할 것 :

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

FAQ를 읽으십시오 :


1
제공 한 링크가 더 이상 작동하지 않습니다. 참조 할 새 링크를 알고 있습니까?
Ramya Rao

11

나는 한 번에 이런 종류의 문제를 해결하여 클래스 정의 후 인라인 하고 헤더 파일 #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


왜? 나는 인라인을 원할 때 까다로운 문제에 대한 우아한 해결책이라고 생각합니다. 인라인을 원하지 않으면 시작부터 작성된 코드를 작성해서는 안됩니다.
epatel

사용자가 B.h먼저 포함하면 어떻게됩니까 ?
Mr Fooz

3
헤더 가드는 예약 된 식별자를 사용하고 있으며, 이중 밑줄이있는 것은 예약되어 있습니다.
Lars Viklund 15:09에

6

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

2
인터페이스 사용 virtual은 런타임 성능에 영향을 미칩니다.
cemper93

4

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

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


2

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

1

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

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

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


1
이것이 Tony Delroy의 솔루션에 비해 어떤 이점이 있습니까? 둘 다 "도우미"헤더를 기반으로하지만 Tony는 더 작으며 (앞으로 선언 만 포함) 적어도 같은 방식으로 작동하는 것 같습니다 (적어도 언뜻보기에).
Fabio는 Reinstate Monica가

1
그 대답은 원래 문제를 해결하지 못합니다. "선언을 별도의 헤더에 넣습니다"라고 표시됩니다. 순환 의존성을 해결하는 것에 대해서는 아무것도 없습니다 (질문 AB정의를 사용할 수 있고 앞으로 선언으로는 충분하지 않은 솔루션이 필요합니다 ).
geza

0

어떤 경우에는 수있다 정의 된 방법 또는 정의와 관련된 순환 종속성을 해결하기위한 클래스 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();
}

0

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

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

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.