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


78

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

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

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


5
음-질문에 대한 답이 맨 위 또는 맨 아래입니까?
Mat

1
실제 질문 (하단)-AFAIK이 경우에 전방 선언을 사용하지 않을 이유가 없습니다 ...
Nim

13
"다른 클래스를 포인터로만 사용"한다는 의미에 따라 약간 다릅니다. delete포워드 선언 만 사용하여 포인터를 사용할 수있는 불쾌한 경우가 있지만 실제로 클래스에 사소하지 않은 소멸자가 있으면 UB를 얻게됩니다. 따라서 delete"포인터 만 사용"하면 그렇습니다. 이유가 있습니다. 중요하지 않다면 그다지 중요하지 않습니다.
Steve Jessop 2012 년

순환 의존성이 여전히 존재하고 컴파일러에서 숨겨져 있지 않습니까? 그렇다면 항상 포워드 선언을 포함하고 항상 수행하는 두 가지 전술은 순환 종속성을 피하는 방법을 가르쳐주지 않습니다. 그러나 앞으로 선언하면 더 쉽게 찾을 수 있음을 인정해야합니다.
grenix

1
클래스가 실제로 구조체로 정의 된 A이면 일부 컴파일러가 불평 할 수 있습니다. 클래스 A가 파생 클래스이면 문제가 있습니다. 클래스 A가 다른 네임 스페이스에 정의되어 있고 헤더가 using 선언을 사용하여이 네임 스페이스로 가져 오면 문제가 발생할 수 있습니다. 클래스 A가 실제로 별칭 (또는 매크로!)이면 문제가 있습니다. 클래스 A가 실제로 typedef이면 문제가 있습니다. 클래스 A가 실제로 기본 템플릿 매개 변수가있는 클래스 템플릿이면 문제가 있습니다. 예, 선언을 전달하지 않는 이유가 있습니다. 구현 세부 사항의 캡슐화를 중단합니다.
Adrian McCarthy

답변:


60

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

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

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

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

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

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


11
또한 재 컴파일 가능성이 증가했습니다.
Peter Wood

9
"순방향 선언을 사용할 수있는 파일을 포함하는 것이 더 좋은 상황을 생각할 수 없습니다."-순방향 선언이 UB를 산출 할 때 주요 질문에 대한 제 의견을 참조하십시오. 당신이 바로 내가 :-) 생각주의 할 것
스티브 Jessop

1
@Luchian : 답변인지 아닌지는 질문자가 원래 의미 한 바에 따라 다르기 때문에 그 정도의 "답변"을 게시하고 싶지 않습니다. 아마 질문자는 기록의 꿈을 결코 delete에서 문을 C.h.
Steve Jessop 2012

2
단점은 더 많은 작업과 더 많은 코드입니다! 그리고 더 취약합니다. 단점이 없다고 말할 수는 없습니다.
usr

2
5 개의 수업이 있으면 어떻게 되나요? 나중에 추가해야하는 경우 어떻게합니까? 당신은 당신의 요점에 대한 최선의 사례에 집중했습니다.
usr

38

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

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

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

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

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

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

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

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

1
"그러나 클래스를 Forward 선언하면 특정 클래스가 Incomplete 유형이되며 이는 Incomplete 유형에서 수행 할 수있는 작업을 심각하게 제한합니다." 네,하지만 포워드 선언 할 있다면 헤더에 완전한 유형이 필요하지 않다는 뜻입니다. 해당 헤더를 포함하는 파일에 완전한 유형이 필요한 경우 필요한 유형에 대한 헤더를 포함하십시오. IMO는 장점입니다. 구현 파일에 필요한 모든 것을 포함하고 다른 곳에 포함되는 것에 의존하지 않습니다.
Luchian Grigore 2012 년

누군가가 해당 헤더를 변경하고 포함을 포워드 선언으로 바꾼다고 가정 해 보겠습니다. 그런 다음 해당 헤더를 포함하는 모든 파일을 변경하고 누락 된 유형을 사용하지만 누락 된 유형의 헤더 자체는 포함하지 않아야합니다.
Luchian Grigore

1
@LuchianGrigore : ..하지만 포워드 선언 할 수있는 경우 ... , 체크 아웃을 시도해야합니다. 따라서 포워드 선언을 수행하고 헤더를 포함하지 않는 고정 된 규칙은 없습니다. .Forward 선언의 가장 일반적인 사용은 순환 종속성을 끊는 것입니다. 불완전한 유형 으로 할 수없는 일은 일반적으로 사용자를 물립니다. 각 소스 파일 및 헤더 파일에는 컴파일에 필요한 모든 헤더가 포함되어야하므로 두 번째 인수는 그렇지 않습니다. 적용, 시작하기에는 단순히 잘못 구성된 코드입니다.
Alok Save

1
PIMPL의 경우에는 클래스의 private 부분에서만 포워드 선언을 사용하는 것이 합리적 일 수 있습니다.
grenix

21

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

편의.

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

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


5
이것은 생산성 비용이 있음을 지적하는 유일한 대답입니다. +1
usr

사용자의 관점에서. 모든 것을 전달하면 사용자가 해당 파일을 포함하고 즉시 작업을 시작할 수 없다는 의미입니다. 그들은 종속성이 무엇인지 (아마도 컴파일러가 불완전한 유형에 대해 불평하기 때문에) 알아 내고 클래스 사용을 시작하기 전에 해당 파일을 포함해야합니다. 대안은 "shared.hpp"파일 또는 모든 헤더가 해당 파일에있는 라이브러리 용으로 생성하는 것입니다 (위에서 언급 한 부스트처럼). 단순히 "포함하고 이동"할 수없는 이유를 파악하지 않고도 쉽게 포함시킬 수 있습니다.
Todd

11

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

// 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 곳 이상에서 변경해야하며 이는 좋지 않습니다.


2
포워드 선언이 항상 더 낫다는 합의를 망치기 위해 +1 :-) IIRC typedef를 통해 "비밀하게"템플릿 인스턴스화 인 유형에서 동일한 문제가 발생합니다. namespace std { class string; }클래스 선언을 네임 스페이스 std에 넣는 것이 허용 되더라도 잘못된 것입니다. 왜냐하면 (내 생각에) typedef를 마치 클래스 인 것처럼 법적으로 앞으로 선언 할 수 없기 때문입니다.
Steve Jessop 2012 년


8

가능한 한 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

5

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

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

함수 선언 :

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

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

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

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

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

클래스를 포워드 선언 :

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

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

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

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

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

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

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


마지막에는 pimpl 관용구를 사용하는 것이 좋지만 해당 관용구의 전체 유용성은 포워드 선언을 기반으로합니다. 포워드 선언과 pimpl 관용구는 거의 동일합니다.
user2445507

@ user2445507 : 질문은 "가능할 때마다"였습니다. 내 요점은 "가능할 때마다"하는 것은 일반적으로 좋은 생각이 아니라는 것입니다. 두 번째 단락에서 말했듯이 인터페이스 소유자는 전달 선언을 제공하는 것이 좋습니다. 전달 선언을 실제 인터페이스와 동기화 할 수 있기 때문입니다. pimpl 관용구를 사용하면 동일한 프로그래머가 forward 선언과 impl 객체의 구현을 담당하므로 완벽하게 괜찮습니다.
Adrian McCarthy

2

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

내가 생각하는 유일한 이유는 타이핑을 절약하는 것입니다.

포워드 선언이 없으면 헤더 파일을 한 번만 포함 할 수 있지만 다른 사람들이 지적한 단점으로 인해 다소 큰 프로젝트에서는 그렇게하도록 조언하지 않습니다.


1
@Luchian 고르 : 몇 가지 간단한 테스트 프로그램에 대한 아마의 확인
ks1322

-1

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

예-성능. 클래스 객체는 데이터 멤버와 함께 메모리에 저장됩니다. 포인터를 사용하면 가리키는 실제 개체에 대한 메모리가 일반적으로 멀리 떨어진 힙의 다른 곳에 저장됩니다. 즉, 해당 개체에 액세스하면 캐시 누락 및 다시로드가 발생합니다. 이것은 성능이 중요한 상황에서 큰 차이를 만들 수 있습니다.

내 PC에서 Faster () 함수는 Slower () 함수보다 약 2000 배 빠르게 실행됩니다.

class SomeClass
{
public:
    void DoSomething()
    {
        val++;
    }
private:
    int val;
};

class UsesPointers
{
public:
    UsesPointers() {a = new SomeClass;}
    ~UsesPointers() {delete a; a = 0;}
    SomeClass * a;
};

class NonPointers
{
public:
    SomeClass a;
};

#define ARRAY_SIZE 100000
void Slower()
{
    UsesPointers list[ARRAY_SIZE];
    for (int i = 0; i < ARRAY_SIZE; i++)
    {
        list[i].a->DoSomething();
    }
}

void Faster()
{
    NonPointers list[ARRAY_SIZE];
    for (int i = 0; i < ARRAY_SIZE; i++)
    {
        list[i].a.DoSomething();
    }
}

성능이 중요한 응용 프로그램의 일부 또는 특히 캐시 일관성 문제가 발생하기 쉬운 하드웨어에서 작업 할 때 데이터 레이아웃 및 사용이 큰 차이를 만들 수 있습니다.

주제 및 기타 성능 요소에 대한 좋은 프레젠테이션입니다. http://research.scee.net/files/presentations/gcapaustralia09/Pitfalls_of_Object_Oriented_Programming_GCAP_09.pdf


9
당신은 ( "포인터 만 사용할 때, 앞으로 선언을 사용하지 않을 이유가 있습니까?") 질문이 아닌 다른 질문 ( "포인터를 사용해야합니까?")에 대답하고 있습니다.
아무도

@AndrewMedico 나는 이것이 좋은 대답이라고 생각하며 성능 단점을 지적하여 질문에 대답했습니다. "앞으로 선언 == 포인터를 사용하여"
에릭
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.