전달 선언은 언제 사용할 수 있습니까?


602

다른 클래스의 헤더 파일에서 클래스를 전달할 수있는 시점에 대한 정의를 찾고 있습니다.

기본 클래스, 멤버로 보유한 클래스, 참조로 멤버 함수에 전달 된 클래스 등에 대해 수행 할 수 있습니까?


14
나는 필사적으로 "때이 이름을 변경하려는 해야 내가"하고 대답은 ... 적절하게 업데이트
deworde

12
@deworde "해야한다"라고 말할 때 의견을 묻는 것입니다.
AturSams

@deworde 빌드 타임을 개선하고 순환 참조를 피하기 위해 가능하면 앞으로 선언을 사용하고 싶다는 것을 이해합니다. 내가 생각할 수있는 유일한 예외는 포함 파일에 typedef가 포함되어있는 경우입니다.
하드 슈나이더

@OhadSchneider 실제적인 관점에서, 나는 저의 헤더를 좋아하지 않습니다. ÷
deworde

기본적으로 항상 사용하기 위해 다른 헤더를 포함해야합니다 (생성자 매개 변수의 전달 decl은 큰 범인입니다)
deworde

답변:


962

컴파일러의 위치에 자신을 두십시오. 형식을 전달할 때 컴파일러가 알고있는 것은이 형식이 존재한다는 것입니다. 크기, 멤버 또는 방법에 대해서는 아무것도 모릅니다. 이것이 불완전한 유형 이라고 불리는 이유 입니다. 따라서 컴파일러는 형식의 레이아웃을 알아야하기 때문에 형식을 사용하여 멤버 또는 기본 클래스를 선언 할 수 없습니다.

다음과 같은 선언을 가정합니다.

class X;

할 수있는 것과 할 수없는 것이 있습니다.

불완전한 유형으로 할 수있는 작업 :

  • 멤버를 불완전한 유형에 대한 포인터 또는 참조로 선언하십시오.

    class Foo {
        X *p;
        X &r;
    };
    
  • 불완전한 유형을 수락 / 반환하는 함수 또는 메소드를 선언하십시오 .

    void f1(X);
    X    f2();
    
  • 불완전한 유형에 대한 포인터 / 참조를 수락 / 반환하는 함수 또는 메소드를 정의하십시오 (그러나 멤버를 사용하지 않음).

    void f3(X*, X&) {}
    X&   f4()       {}
    X*   f5()       {}
    

불완전한 유형으로는 할 수없는 일 :

  • 기본 클래스로 사용

    class Foo : X {} // compiler error!
  • 이를 사용하여 멤버를 선언하십시오.

    class Foo {
        X m; // compiler error!
    };
    
  • 이 유형을 사용하여 함수 또는 메소드 정의

    void f1(X x) {} // compiler error!
    X    f2()    {} // compiler error!
    
  • 불완전한 유형의 변수를 역 참조하려는 경우 해당 메소드 또는 필드를 사용하십시오.

    class Foo {
        X *m;            
        void method()            
        {
            m->someMethod();      // compiler error!
            int i = m->someField; // compiler error!
        }
    };
    

템플릿에 관해서는 절대 규칙이 없습니다. 불완전한 유형을 템플릿 매개 변수로 사용할 수 있는지 여부는 템플릿에서 유형이 사용되는 방식에 따라 다릅니다.

예를 들어, std::vector<T>매개 변수는 완전한 유형이어야하지만 boost::container::vector<T>그렇지는 않습니다. 때로는 특정 멤버 함수를 사용하는 경우에만 완전한 유형이 필요할 수 있습니다. 예를 들어이 경우입니다std::unique_ptr<T> .

잘 문서화 된 템플릿은 완전한 유형이 필요한지 여부를 포함하여 매개 변수의 모든 요구 사항을 문서에 표시해야합니다.


4
큰 대답이지만 동의하지 않는 엔지니어링 포인트는 아래를 참조하십시오. 요컨대, 수락하거나 반환하는 불완전한 유형의 헤더를 포함하지 않으면 헤더 소비자에게 보이지 않는 종속성이 있어야 다른 헤더가 필요하다는 것을 알 수 있습니다.
Andy Dent

2
@AndyDent : True. 그러나 헤더 소비자는 실제로 사용하는 종속성 만 포함하면되므로 "사용하는 것만 지불"이라는 C ++ 원칙을 따릅니다. 그러나 실제로 헤더가 독립형 일 것으로 기대하는 사용자에게는 불편할 수 있습니다.
Luc Touraille

8
이 규칙 집합은 매우 중요한 경우를 무시합니다. 표준 라이브러리에서 대부분의 템플릿을 인스턴스화하려면 완전한 유형이 필요합니다. 규칙을 위반하면 정의되지 않은 동작이 발생하고 컴파일러 오류가 발생하지 않을 수 있으므로 특히주의를 기울여야합니다.
James Kanze

12
"컴파일러 위치에 두십시오"에 대해 +1 "컴파일러"가 콧수염을 앓고 있다고 상상합니다.
PascalVKooten

3
@JesusChrist : 정확히 : 값으로 객체를 전달할 때, 적절한 스택 조작을하기 위해 컴파일러는 크기를 알아야합니다. 포인터 나 참조를 전달할 때 컴파일러는 객체의 크기 나 레이아웃이 필요하지 않으며 주소의 크기 (예 : 포인터의 크기) 만 필요합니다. 이는 지정된 유형에 의존하지 않습니다.
Luc Touraille

45

기본 규칙은 메모리 레이아웃 (및 멤버 함수 및 데이터 멤버)을 전달할 파일에서 알 필요가없는 클래스 만 전달 선언 할 수 있다는 것입니다.

이것은 기본 클래스와 참조 및 포인터를 통해 사용되는 클래스 이외의 것을 배제합니다.


6
거의. 함수 프로토 타입에서 "일반"(포인터 / 참조가 아닌) 불완전한 유형을 매개 변수 또는 리턴 유형으로 참조 할 수도 있습니다.
j_random_hacker

헤더 파일에서 정의한 클래스의 멤버로 사용하려는 클래스는 어떻습니까? 전달할 수 있습니까?
Igor Oks

1
예, 그러나이 경우 앞으로 선언 된 클래스에 대한 참조 또는 포인터 만 사용할 수 있습니다. 그럼에도 불구하고 회원을 가질 수 있습니다.
Reunanen

32

Lakos 는 클래스 사용을 구별합니다

  1. in-name-only (정방향 선언으로 충분)
  2. 크기 (클래스 정의가 필요한 경우)

나는 그것이 간결하게 발음되는 것을 본 적이 없다. :)


2
성명 전용이란 무엇입니까?
Boon

4
@Boon : 감히 말하면 ...? 클래스 이름 사용한다면 ?
Marc Mutz-mmutz

1
Lakos, Marc를위한 1 개
mlvljr

28

불완전한 유형에 대한 포인터 및 참조뿐만 아니라 불완전한 유형의 매개 변수 및 / 또는 반환 값을 지정하는 함수 프로토 타입을 선언 할 수도 있습니다. 그러나 포인터 또는 참조가 아닌 한 매개 변수 또는 리턴 유형이 불완전한 함수를 정의 할 수 없습니다 .

예 :

struct X;              // Forward declaration of X

void f1(X* px) {}      // Legal: can always use a pointer
void f2(X&  x) {}      // Legal: can always use a reference
X f3(int);             // Legal: return value in function prototype
void f4(X);            // Legal: parameter in function prototype
void f5(X) {}          // ILLEGAL: *definitions* require complete types

19

지금까지 답변은 클래스 템플릿의 전달 선언을 사용할 수있는 시점을 설명하지 않습니다. 그래서, 여기에 간다.

클래스 템플릿은 다음과 같이 선언하여 전달할 수 있습니다.

template <typename> struct X;

의 구조에 따라 허용 대답 ,

할 수있는 것과 할 수없는 것이 있습니다.

불완전한 유형으로 할 수있는 작업 :

  • 다른 클래스 템플릿에서 멤버를 불완전한 형식에 대한 포인터 또는 참조로 선언합니다.

    template <typename T>
    class Foo {
        X<T>* ptr;
        X<T>& ref;
    };
  • 불완전한 인스턴스화 중 하나에 대한 포인터 또는 참조가되도록 멤버를 선언하십시오.

    class Foo {
        X<int>* ptr;
        X<int>& ref;
    };
  • 불완전한 유형을 수락하거나 반환하는 함수 템플릿 또는 멤버 함수 템플릿을 선언합니다.

    template <typename T>
       void      f1(X<T>);
    template <typename T>
       X<T>    f2();
  • 불완전한 인스턴스화 중 하나를 수락 / 반환하는 함수 또는 멤버 함수를 선언하십시오.

    void      f1(X<int>);
    X<int>    f2();
  • 불완전한 유형에 대한 포인터 / 참조를 수락 / 반환하는 함수 템플리트 또는 멤버 함수 템플리트를 정의하십시오 (하지만 멤버를 사용하지 않음).

    template <typename T>
       void      f3(X<T>*, X<T>&) {}
    template <typename T>
       X<T>&   f4(X<T>& in) { return in; }
    template <typename T>
       X<T>*   f5(X<T>* in) { return in; }
  • 불완전한 인스턴스화 중 하나에 대한 포인터 / 참조를 수락 / 반환하는 함수 또는 메소드를 정의하십시오 (그러나 멤버를 사용하지 않음).

    void      f3(X<int>*, X<int>&) {}
    X<int>&   f4(X<int>& in) { return in; }
    X<int>*   f5(X<int>* in) { return in; }
  • 다른 템플릿 클래스의 기본 클래스로 사용

    template <typename T>
    class Foo : X<T> {} // OK as long as X is defined before
                        // Foo is instantiated.
    
    Foo<int> a1; // Compiler error.
    
    template <typename T> struct X {};
    Foo<int> a2; // OK since X is now defined.
  • 이를 사용하여 다른 클래스 템플릿의 멤버를 선언하십시오.

    template <typename T>
    class Foo {
        X<T> m; // OK as long as X is defined before
                // Foo is instantiated. 
    };
    
    Foo<int> a1; // Compiler error.
    
    template <typename T> struct X {};
    Foo<int> a2; // OK since X is now defined.
  • 이 유형을 사용하여 함수 템플릿 또는 메소드 정의

    template <typename T>
      void    f1(X<T> x) {}    // OK if X is defined before calling f1
    template <typename T>
      X<T>    f2(){return X<T>(); }  // OK if X is defined before calling f2
    
    void test1()
    {
       f1(X<int>());  // Compiler error
       f2<int>();     // Compiler error
    }
    
    template <typename T> struct X {};
    
    void test2()
    {
       f1(X<int>());  // OK since X is defined now
       f2<int>();     // OK since X is defined now
    }

불완전한 유형으로는 할 수없는 일 :

  • 인스턴스화 중 하나를 기본 클래스로 사용

    class Foo : X<int> {} // compiler error!
  • 인스턴스화 중 하나를 사용하여 멤버를 선언하십시오.

    class Foo {
        X<int> m; // compiler error!
    };
  • 인스턴스화 중 하나를 사용하여 함수 또는 메소드 정의

    void      f1(X<int> x) {}            // compiler error!
    X<int>    f2() {return X<int>(); }   // compiler error!
  • 불완전한 유형의 변수를 역 참조하려는 경우 인스턴스화 중 하나의 메소드 또는 필드를 사용하십시오.

    class Foo {
        X<int>* m;            
        void method()            
        {
            m->someMethod();      // compiler error!
            int i = m->someField; // compiler error!
        }
    };
  • 클래스 템플릿의 명시 적 인스턴스 생성

    template struct X<int>;

2
"지금까지 답변 중 어느 것도 클래스 템플릿의 선언을 전달할 수있는 시점을 설명하지 못했습니다." 아닌가의 의미는 간단하기 때문에 그 XX<int>동일하며, 모두 어떤 실질적인 방법으로 단지 앞으로 선언 구문 다르다, 그러나 다만 루크의과 복용에 대한 당신의 대답에 상당한다 1 개 라인 s/X/X<int>/g? 정말 필요한가요? 아니면 다른 작은 세부 사항을 놓쳤습니까? 가능하지만 시각적으로 몇 번 비교했지만 아무것도 볼 수 없습니다.
underscore_d

감사합니다! 그 편집은 많은 귀중한 정보를 추가합니다. 나는 그것을 완전히 이해하기 위해 여러 번 읽어야 할 것입니다. 나는 이것을 사용하여 다양한 장소에서 의존성을 줄일 수 있다고 생각합니다.
underscore_d

4

클래스에 대한 포인터 또는 참조 만 사용하는 파일에서 포인터 / 참조를 생각하면 멤버 / 멤버 함수를 호출해서는 안됩니다.

class Foo;// 앞으로 선언

Foo * 또는 Foo & 유형의 데이터 멤버를 선언 할 수 있습니다.

Foo 유형의 인수 및 / 또는 반환 값으로 함수를 선언 할 수 있지만 정의 할 수는 없습니다.

Foo 타입의 정적 데이터 멤버를 선언 할 수 있습니다. 정적 데이터 멤버는 클래스 정의 외부에 정의되어 있기 때문입니다.


4

합법성에 근거한 것이 아니라 강력한 소프트웨어 및 잘못된 해석의 위험에 대한 Luc Touraille의 답변에 동의하지 않기 때문에 이것을 주석이 아닌 별도의 답변으로 작성하고 있습니다.

특히 인터페이스 사용자가 알아야 할 내용에 대한 묵시적 계약에 문제가 있습니다.

참조 유형을 반환하거나 수락하는 경우 포인터 또는 참조를 전달할 수 있으며 순방향 선언을 통해서만 알 수 있습니다.

불완전한 유형 X f2();을 반환하는 경우 호출자 X의 전체 유형 사양을 가져야 한다고 말하고 있습니다. 호출 사이트에서 LHS 또는 임시 개체를 만들려면 필요합니다.

마찬가지로, 불완전한 유형을 허용하면 호출자는 매개 변수 인 오브젝트를 구성해야합니다. 해당 객체가 함수에서 다른 불완전한 유형으로 반환 되더라도 호출 사이트에는 전체 선언이 필요합니다. 즉 :

class X;  // forward for two legal declarations 
X returnsX();
void XAcceptor(X);

XAcepptor( returnsX() );  // X declaration needs to be known here

헤더가 다른 헤더를 필요로하는 종속성없이 사용하기에 충분한 정보를 제공해야한다는 중요한 원칙이 있다고 생각합니다. 즉, 선언하는 함수를 사용할 때 컴파일러 오류를 발생시키지 않고 헤더를 컴파일 단위에 포함 할 수 있어야합니다.

  1. 이 외부 의존성이 필요한 경우 행동. 조건부 컴파일을 사용하는 대신 X를 선언하는 자체 헤더를 제공하기 위해 잘 문서화 된 요구 사항을 가질 수 있습니다. 이것은 #ifdefs를 사용하는 대안이며 모의 품 또는 기타 변형을 도입하는 유용한 방법이 될 수 있습니다.

  2. 중요한 차이점은 명시 적으로 인스턴스화 할 것으로 예상되지 않는 템플릿 기술이며 누군가가 나에게 소리를 지르지 않도록 언급했습니다.


"헤더가 다른 헤더를 요구하는 종속성없이 헤더를 사용하기에 충분한 정보를 제공해야한다는 중요한 원칙이 있다고 생각합니다." -또 다른 문제는 Adrian McCarthy의 Naveen의 답변에 대한 의견에서 언급됩니다. 이는 현재 템플릿이 아닌 유형의 경우에도 "사용하기에 충분한 정보를 제공해야합니다"원칙을 따르지 않는 적절한 이유를 제공합니다.
Tony Delroy 2014 년

3
앞으로 선언 을 사용해야 할 때 (또는 사용하지 않아야 할 경우)에 대해 이야기하고 있습니다 . 그러나이 질문의 핵심은 아닙니다. 이것은 (예를 들어) 순환 종속성 문제를 깰 때 기술적 가능성을 아는 것입니다.
JonnyJD

1
I disagree with Luc Touraille's answer따라서 길이가 필요한 경우 블로그 게시물에 대한 링크를 포함하여 의견을 작성하십시오. 이 질문에 대답하지 않습니다. 모든 사람들이 X가 어떻게 작동하는지에 대한 질문을 X가 X를 사용하는 자유를 제한해야하는 토론이나 한계에 동의하지 않는 경우, 답변이 거의 없습니다.
underscore_d

3

내가 따르는 일반적인 규칙은 내가하지 않는 한 헤더 파일을 포함시키지 않는 것입니다. 따라서 클래스의 객체를 클래스의 멤버 변수로 저장하지 않으면 포함시키지 않고 앞으로 선언을 사용합니다.


2
이로 인해 캡슐화가 중단되고 코드가 취약 해집니다. 이렇게하려면 형식이 기본 템플릿 매개 변수가있는 클래스 템플릿의 typedef 또는 클래스인지 알아야하며 구현이 변경되는 경우 정방향 선언을 사용한 위치를 업데이트해야합니다.
Adrian McCarthy

@AdrianMcCarthy가 옳으며 합리적인 해결책은 전달하는 내용이 선언 된 헤더에 포함 된 전달 선언 헤더를 갖는 것입니다. 예를 들면 다음과 같습니다. iosfwd 표준 라이브러리 헤더 (iostream 콘텐츠의 정방향 선언 포함).
Tony Delroy

3

정의가 필요하지 않은 한 (포인터 및 참조 생각) 순방향 선언으로 벗어날 수 있습니다. 이것이 대부분 헤더에서 볼 수있는 반면 구현 파일은 일반적으로 적절한 정의를 위해 헤더를 가져옵니다.


0

일반적으로 다른 유형 (클래스)을 클래스의 멤버로 사용하려는 경우 클래스 헤더 파일에서 정방향 선언을 사용하려고합니다. C ++은 그 시점에서 해당 클래스의 정의를 아직 알지 못하므로 헤더 파일에서 앞으로 선언 된 클래스 메소드 를 사용할 수 없습니다 . 그것이 .cpp 파일로 옮겨야하는 논리이지만, 템플릿 함수를 사용하는 경우 템플릿을 사용하는 부분으로 축소하고 해당 함수를 헤더로 옮겨야합니다.


말이되지 않습니다. 불완전한 유형의 멤버는 가질 수 없습니다. 클래스 선언은 모든 사용자가 크기와 레이아웃에 대해 알아야하는 모든 것을 제공해야합니다. 크기에는 모든 비 정적 멤버의 크기가 포함됩니다. 멤버를 미리 선언하면 크기에 대해 전혀 알지 못합니다.
underscore_d

0

앞으로 선언하면 코드를 컴파일 할 수 있습니다 (obj가 생성됨). 그러나 정의를 찾을 수 없으면 연결 (exe 작성)에 실패합니다.


2
왜 2 명이 이것을 찬성 했습니까? 당신은 그 질문에 대해 말하는 것이 아닙니다. 당신은 함수의 일반적인 선언이 아니라 선언을 의미 합니다 . 문제는 클래스의 선언에 관한 것입니다 . "앞으로 선언하면 코드를 컴파일 할 수 있습니다"라고 말했듯이 나에게 호의를 베푸십시오 : compile class A; class B { A a; }; int main(){}, 그것이 어떻게되는지 알려주십시오. 물론 컴파일되지 않습니다. 여기에있는 모든 정답은 왜 앞으로 선언 유효한지 , 정확하고 제한된 상황을 설명 합니다. 대신 완전히 다른 것에 대해 이것을 작성했습니다.
underscore_d

0

Luc Touraille의 답변에 언급되지 않은 전달 클래스로 할 수있는 중요한 일을 하나 추가하고 싶습니다.

불완전한 유형으로 할 수있는 작업 :

불완전한 유형에 대한 포인터 / 참조를 수락 / 반환하고 해당 포인터 / 참조를 다른 함수에 전달하는 함수 또는 메소드를 정의하십시오 .

void  f6(X*)       {}
void  f7(X&)       {}
void  f8(X* x_ptr, X& x_ref) { f6(x_ptr); f7(x_ref); }

모듈은 정방향 선언 된 클래스의 객체를 다른 모듈로 전달할 수 있습니다.


"전달 된 클래스"와 "전달 된 클래스"는 두 가지 매우 다른 것을 지칭하는 것으로 오해 될 수 있습니다. 작성한 내용은 Luc의 답변에 암시 된 개념에서 직접 따르므로 명백한 설명을 추가하여 좋은 의견을 제시했지만 답변을 정당화 할 수는 없습니다.
underscore_d

0

마찬가지로 Luc Touraille은 클래스의 앞으로 선언을 사용하고 사용하지 않는 곳을 이미 잘 설명했습니다.

나는 우리가 그것을 사용해야하는 이유를 덧붙일 것입니다.

원치 않는 의존성 주입을 피하기 위해 가능하면 Forward 선언을 사용해야합니다.

마찬가지로 #include헤더 파일 따라서 복수의 파일에 추가된다 우리가 다른 헤더 파일에 헤더를 추가 할 경우, 추가로 방지 할 수있는 소스 코드의 다양한 부분에서 불필요한 의존성 주입을 추가 할 것이다 #include에 헤더 .cpp파일 가능한보다는 다른 헤더 파일에 추가하고 헤더 .h파일 에서 클래스 포워드 선언을 사용 하십시오.

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