[c++] 가능한 한 include 대신 포워드 선언을 사용해야합니까?

클래스 선언이 다른 클래스를 포인터로만 사용할 때마다 순환 종속성 문제를 선제 적으로 방지하기 위해 헤더 파일을 포함하는 대신 클래스 전달 선언을 사용하는 것이 합리적입니까? 그래서 대신 :

//file C.h
#include "A.h"
#include "B.h"

class C{
    A* a;
    B b;
    ...
};

대신 다음을 수행하십시오.

//file C.h
#include "B.h"

class A;

class C{
    A* a;
    B b;
    ...
};


//file C.cpp
#include "C.h"
#include "A.h"
...

가능할 때마다 이것을하지 않는 이유가 있습니까?



답변

앞으로 선언하는 방법은 거의 항상 더 좋습니다. (정방향 선언을 사용할 수있는 파일을 포함하는 것이 더 좋은 상황은 생각할 수 없지만 혹시라도 항상 더 좋다고 말할 수는 없습니다).

포워드 선언 클래스에는 단점이 없지만 불필요하게 헤더를 포함 할 경우 몇 가지 단점을 생각할 수 있습니다.

  • 더 이상 컴파일 시간, 모든 번역 단위를 포함하여 이후 C.h도 포함됩니다 A.h그들이 필요하지 않을 수도 있지만.

  • 간접적으로 필요하지 않은 다른 헤더를 포함 할 수 있습니다.

  • 필요하지 않은 기호로 번역 단위를 오염시킵니다.

  • 헤더가 변경되면 해당 헤더를 포함하는 소스 파일을 다시 컴파일해야 할 수 있습니다 (@PeterWood)


답변

예, 앞으로 선언을 사용하는 것이 항상 더 좋습니다.

그들이 제공하는 몇 가지 장점은 다음과 같습니다.

  • 컴파일 시간 단축.
  • 네임 스페이스 오염이 없습니다.
  • (경우에 따라) 생성 된 바이너리의 크기를 줄일 수 있습니다.
  • 재 컴파일 시간을 크게 줄일 수 있습니다.
  • 전 처리기 이름의 잠재적 충돌 방지.
  • 구현 PIMPL 관용구 따라서 인터페이스 구현을 은폐하는 수단을 제공한다.

그러나 클래스를 Forward로 선언하면 특정 클래스가 Incomplete 유형이 되고 이는 Incomplete 유형에서 수행 할 수있는 작업을 심각하게 제한합니다.
클래스의 레이아웃을 알기 위해 컴파일러가 필요한 작업을 수행 할 수 없습니다.

불완전한 유형으로 다음을 수행 할 수 있습니다.

  • 멤버를 불완전한 유형에 대한 포인터 또는 참조로 선언하십시오.
  • 불완전한 유형을 허용 / 반환하는 함수 또는 메소드를 선언하십시오.
  • 불완전한 유형에 대한 포인터 / 참조를 수락 / 반환하는 함수 또는 메서드를 정의합니다 (하지만 멤버를 사용하지 않음).

불완전한 유형으로 다음을 수행 할 수 없습니다.

  • 기본 클래스로 사용하십시오.
  • 이를 사용하여 구성원을 선언하십시오.
  • 이 유형을 사용하여 함수 또는 방법을 정의하십시오.


답변

가능할 때마다 이것을하지 않는 이유가 있습니까?

편의.

이 헤더 파일의 사용자 A가 무엇이든 (또는 대부분의 경우) 수행 할 정의를 반드시 포함해야한다는 것을 미리 알고 있다면 . 그런 다음 한 번만 포함하는 것이 편리합니다.

이 엄지 손가락 규칙을 너무 자유로이 사용하면 거의 컴파일 할 수없는 코드가 생성되기 때문에 이것은 다소 민감한 주제입니다. Boost는 몇 가지 밀접한 기능을 함께 묶는 특정 “편의”헤더를 제공함으로써 문제에 다르게 접근합니다.


답변

포워드 선언을 원하지 않는 한 가지 경우는 그 자체가 까다로울 때입니다. 다음 예제와 같이 일부 클래스가 템플릿 화 된 경우 이러한 상황이 발생할 수 있습니다.

// Forward declarations
template <typename A> class Frobnicator;
template <typename A, typename B, typename C = Frobnicator<A> > class Gibberer;

// Alternative: more clear to the reader; more stable code
#include "Gibberer.h"

// Declare a function that does something with a pointer
int do_stuff(Gibberer<int, float>*);

순방향 선언은 코드 복제와 동일합니다. 코드가 많이 변경되는 경향이있는 경우 매번 2 곳 이상에서 변경해야하며 이는 좋지 않습니다.


답변

재미있는 사실 은 C ++ 스타일 가이드 에서 Google은 #include순환 종속성을 피하기 위해 모든 곳에서 사용 하도록 권장 합니다.


답변

가능한 한 include 대신 포워드 선언을 사용해야합니까?

아니요, 명시적인 전방 선언은 일반적인 지침으로 간주되어서는 안됩니다. 포워드 선언은 본질적으로 복사하여 붙여 넣거나 철자가 틀린 코드로, 버그를 발견 한 경우 포워드 선언이 사용되는 모든 곳에서 수정해야합니다. 이는 오류가 발생하기 쉽습니다.

“앞으로”선언과 해당 정의 사이의 불일치를 방지하려면 선언을 헤더 파일에 넣고 해당 헤더 파일을 정의 및 선언 사용 소스 파일 모두에 포함합니다.

그러나 불투명 한 클래스 만 포워드 선언되는이 특별한 경우에는이 포워드 선언을 사용해도 괜찮지 만, 일반적으로이 스레드의 제목처럼 “가능하면 항상 포함 대신 포워드 선언을 사용”할 수 있습니다. 꽤 위험합니다.

다음은 전방 선언과 관련된 “보이지 않는 위험”의 몇 가지 예입니다 (보이지 않는 위험 = 컴파일러 또는 링커에서 감지하지 않는 선언 불일치).

  • 데이터를 나타내는 기호의 명시 적 전방 선언은 안전하지 않을 수 있습니다. 이러한 전방 선언에는 데이터 유형의 풋 프린트 (크기)에 대한 정확한 지식이 필요할 수 있기 때문입니다.

  • 함수를 나타내는 기호의 명시 적 전방 선언도 매개 변수 유형 및 매개 변수 수와 같이 안전하지 않을 수 있습니다.

아래의 예는이를 보여줍니다. 예를 들어, 데이터와 함수의 두 가지 위험한 전방 선언이 있습니다.

파일 ac :

#include <iostream>
char data[128][1024];
extern "C" void function(short truncated, const char* forgotten) {
  std::cout << "truncated=" << std::hex << truncated
            << ", forgotten=\"" << forgotten << "\"\n";
}

파일 bc :

#include <iostream>
extern char data[1280][1024];           // 1st dimension one decade too large
extern "C" void function(int tooLarge); // Wrong 1st type, omitted 2nd param

int main() {
  function(0x1234abcd);                         // In worst case: - No crash!
  std::cout << "accessing data[1270][1023]\n";
  return (int) data[1270][1023];                // In best case:  - Boom !!!!
}

g ++ 4.7.1로 프로그램 컴파일 :

> g++ -Wall -pedantic -ansi a.c b.c

참고 : g ++는 컴파일러 또는 링커 오류 / 경고를 제공하지 않으므로 보이지 않는 위험입니다.
참고 : 생략 하면 C ++ 이름 변경으로 인해 extern "C"연결 오류가 function()발생합니다.

프로그램 실행 :

> ./a.out
truncated=abcd, forgotten="♀♥♂☺☻"
accessing data[1270][1023]
Segmentation fault


답변

가능할 때마다 이것을하지 않는 이유가 있습니까?

절대적으로 : 클래스 또는 함수의 사용자가 구현 세부 정보를 알고 복제하도록 요구하여 캡슐화를 중단합니다. 이러한 구현 세부 정보가 변경되면 앞으로 선언하는 코드가 손상 될 수 있지만 헤더에 의존하는 코드는 계속 작동합니다.

함수 선언 :

  • 정적 펑터 객체 또는 매크로가 아닌 함수로 구현되었음을 알아야합니다.

  • 기본 매개 변수의 기본값을 복제해야합니다.

  • 실제 이름과 네임 스페이스를 알아야합니다. 왜냐하면 using별칭 아래에서 다른 네임 스페이스로 가져 오는 선언 일 수도 있기 때문입니다.

  • 인라인 최적화를 잃을 수 있습니다.

소비 코드가 헤더에 의존하는 경우, 이러한 모든 구현 세부 사항은 코드를 손상시키지 않고 함수 공급자에 의해 변경 될 수 있습니다.

클래스를 포워드 선언 :

  • 파생 클래스인지 그리고 파생 된 기본 클래스인지 알아야합니다.

  • typedef 또는 클래스 템플릿의 특정 인스턴스화가 아닌 클래스임을 알아야합니다 (또는 클래스 템플릿임을 알고 모든 템플릿 매개 변수와 기본값을 올바르게 가져옴).

  • 클래스의 실제 이름과 네임 스페이스를 알아야합니다. 이는 using아마도 별칭 아래에있는 다른 네임 스페이스로 가져 오는 선언 일 수 있기 때문입니다.

  • 올바른 속성을 알아야합니다 (아마도 특별한 정렬 요구 사항이있을 수 있습니다).

다시 말하지만, 포워드 선언은 이러한 구현 세부 정보의 캡슐화를 해제하여 코드를 더 취약하게 만듭니다.

컴파일 시간을 단축하기 위해 헤더 종속성을 잘라 내야하는 경우 클래스 / 함수 / 라이브러리 공급자에게 특수 정방향 선언 헤더를 제공하도록 요청하세요. 표준 라이브러리는 <iosfwd>. 이 모델은 구현 세부 사항의 캡슐화를 유지하고 라이브러리 관리자에게 코드를 손상시키지 않고 이러한 구현 세부 사항을 변경할 수있는 기능을 제공하는 동시에 컴파일러의 부하를 줄입니다.

또 다른 옵션은 구현 세부 사항을 훨씬 더 잘 숨기고 작은 런타임 오버 헤드로 컴파일 속도를 높이는 pimpl 관용구를 사용하는 것입니다.