월별 글 목록: 2021년 8월월

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

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


답변

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

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