클래스 간의 순환 종속성으로 인한 빌드 오류 해결
내가 프로젝트 ++은 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에 대한 공간을 할당해야합니다 . 그렇다면 공간이 얼마나됩니까? 저장하기에 충분합니다 ! 그때 의 크기는 얼마입니까? 저장하기에 충분합니다 ! 죄송합니다.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
오류가 발생하기 때문입니다. 선언을 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*);
};
의 메인테이너 A
및 B
예를 들어 - - 라이브러리는 각 그래서, 자신의 헤더와 구현 파일과 동기화 자신의 앞으로 선언 헤더를 유지하기위한 책임을 져야한다 "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가 결국 포함하지 않은 경우-A가
b.h
포인터 및 / 또는 참조로 B 주위에 저장 / 전달하는 경우 가능)#include
분석에 의존하는 빌드 도구 및 변경된 파일 타임 스탬프는A
B로 변경 한 후 다시 빌드되지 않으며 링크 타임 또는 런타임시 오류가 발생합니다. 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;
}
불행히도, 이전의 모든 답변에 일부 세부 정보가 누락되었습니다. 올바른 해결책은 약간 성가 시지만 이것이 제대로하는 유일한 방법입니다. 또한 쉽게 확장되고 더 복잡한 종속성도 처리합니다.
모든 세부 사항과 유용성을 정확하게 유지 하면서이 작업을 수행하는 방법은 다음과 같습니다.
- 해결책은 원래 의도 한 것과 정확히 동일합니다.
- 인라인 함수는 여전히 인라인
- 사용자
A
및B
임의의 순서로 아와 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
이 지시문은 현재 파일 처리를 지연시키고 보류중인 모든 포함을 완료해야합니다.
'Programing' 카테고리의 다른 글
REST 웹 애플리케이션의 페이지 매김 (0) | 2020.03.06 |
---|---|
로깅 모범 사례 (0) | 2020.03.06 |
공백이 아닌 변경 사항 만 추가 (0) | 2020.03.06 |
Java에서 최종 키워드를 사용하면 성능이 향상됩니까? (0) | 2020.03.06 |
충돌하는 Git rebase 도중에 "그들의"변화를 얻는 방법? (0) | 2020.03.06 |