콘텐츠로 이동

전방 선언 (Forward Declarations)

가능한 한 전방 선언을 피하고, 필요한 헤더를 직접 포함하세요.

"전방 선언(forward declaration)"은 정의 없이 엔티티만 선언하는 것입니다.

// C++ 소스 파일에서:
class B;
void FuncInB();
extern int variable_in_b;
ABSL_DECLARE_FLAG(flag_in_b);

전방 선언은 컴파일 시간을 절약할 수 있습니다. #include는 컴파일러가 더 많은 파일을 열고 더 많은 입력을 처리하도록 강제하기 때문입니다.

전방 선언은 불필요한 재컴파일을 줄일 수 있습니다. #include는 헤더의 관련 없는 변경으로 인해 코드를 더 자주 재컴파일하도록 강제할 수 있습니다.

전방 선언은 의존성을 숨길 수 있어, 헤더가 변경될 때 사용자 코드가 필요한 재컴파일을 건너뛸 수 있게 합니다.

#include 문과 달리 전방 선언은 자동화 도구가 심볼을 정의하는 모듈을 발견하기 어렵게 만듭니다.

전방 선언은 라이브러리의 후속 변경으로 인해 깨질 수 있습니다. 함수와 템플릿의 전방 선언은 헤더 소유자가 매개변수 타입을 넓히거나, 기본값이 있는 템플릿 매개변수를 추가하거나, 새로운 네임스페이스로 마이그레이션하는 것과 같은 호환 가능한 변경을 하는 것을 방지할 수 있습니다.

namespace std::의 심볼을 전방 선언하면 정의되지 않은 동작(undefined behavior)이 발생합니다.

전방 선언이 필요한지 전체 #include가 필요한지 판단하기 어려울 수 있습니다. #include를 전방 선언으로 교체하면 코드의 의미가 조용히 변경될 수 있습니다:

// b.h:
struct B {};
struct D : B {};

// good_user.cc:
#include "b.h"
void f(B*);
void f(void*);
void test(D* x) { f(x); }  // f(B*)를 호출함

만약 #include가 B와 D의 전방 선언으로 교체되었다면, test()f(void*)를 호출할 것입니다.

헤더에서 여러 심볼을 전방 선언하는 것은 단순히 헤더를 #include하는 것보다 더 장황할 수 있습니다.

전방 선언을 가능하게 하도록 코드를 구조화하는 것(예: 객체 멤버 대신 포인터 멤버 사용)은 코드를 더 느리게 만들고 더 복잡하게 만들 수 있습니다.

다른 프로젝트에 정의된 엔티티의 전방 선언을 피하도록 노력하세요.


이해하기 쉽게 설명하기

전방 선언이란?

전방 선언(forward declaration)은 정의 없이 엔티티의 선언만 하는 것입니다. 즉, "이런 게 있다"고 알려주지만, 실제로 어떻게 생겼는지는 나중에 보여주겠다는 의미입니다.

class B;  // B 클래스가 있다고 선언만 함 (정의는 나중에)
void FuncInB();  // FuncInB 함수가 있다고 선언만 함
extern int variable_in_b;  // 변수가 있다고 선언만 함

전방 선언의 장점

  1. 컴파일 시간 절약: #include를 사용하면 컴파일러가 해당 헤더 파일 전체를 읽고 처리해야 합니다. 전방 선언을 사용하면 헤더 파일을 읽을 필요가 없어 컴파일이 빨라질 수 있습니다.

  2. 불필요한 재컴파일 방지: 헤더 파일이 변경되면 그 헤더를 포함한 모든 파일이 재컴파일됩니다. 전방 선언을 사용하면 헤더 파일의 내부 구현이 변경되어도 재컴파일이 필요 없을 수 있습니다.

  3. 의존성 숨김: 전방 선언을 사용하면 실제 헤더 파일에 대한 의존성이 숨겨져, 헤더가 변경되어도 사용자 코드가 영향을 받지 않을 수 있습니다.

전방 선언의 단점

  1. 자동화 도구의 어려움: 전방 선언을 사용하면 자동화 도구가 실제 정의가 어디에 있는지 찾기 어렵습니다. 이는 리팩토링이나 코드 분석 도구의 동작에 영향을 줄 수 있습니다.

  2. 호환성 문제: 전방 선언된 함수나 템플릿의 시그니처가 변경되면 전방 선언이 깨질 수 있습니다. 예를 들어:

  3. 매개변수 타입이 변경되면

  4. 템플릿 매개변수가 추가되면
  5. 네임스페이스가 변경되면

전방 선언이 더 이상 유효하지 않게 됩니다.

  1. std:: 네임스페이스의 위험: std:: 네임스페이스의 심볼을 전방 선언하면 정의되지 않은 동작이 발생할 수 있습니다. 표준 라이브러리의 타입들은 반드시 적절한 헤더를 포함해서 사용해야 합니다.

  2. 코드 의미 변경: #include를 전방 선언으로 교체하면 코드의 의미가 조용히 변경될 수 있습니다.

예시: 코드 의미 변경

// b.h:
struct B {};
struct D : B {};  // D는 B를 상속받음

// good_user.cc:
#include "b.h"
void f(B*);
void f(void*);
void test(D* x) { f(x); }  // f(B*)를 호출함 (D*가 B*로 변환됨)

위 코드에서 #include "b.h"를 사용하면 상속 관계를 알 수 있어 D*B*로 변환되어 f(B*)가 호출됩니다.

하지만 전방 선언만 사용하면:

// good_user.cc:
class B;  // 전방 선언
class D;  // 전방 선언 (상속 관계를 모름)

void f(B*);
void f(void*);
void test(D* x) { f(x); }  // f(void*)를 호출함 (상속 관계를 모르므로)

상속 관계를 알 수 없어 D*void*로 변환되어 f(void*)가 호출됩니다. 이는 원래 의도와 다릅니다!

  1. 가독성 저하: 여러 심볼을 전방 선언하는 것은 단순히 헤더를 포함하는 것보다 더 장황할 수 있습니다.

  2. 성능 및 복잡성: 전방 선언을 가능하게 하기 위해 코드 구조를 변경해야 할 수 있습니다 (예: 객체 멤버 대신 포인터 멤버 사용). 이는 코드를 더 느리게 만들고 복잡하게 만들 수 있습니다.

권장 사항

  • 가능한 한 전방 선언을 피하고 필요한 헤더를 직접 포함하세요.
  • 특히 다른 프로젝트에 정의된 엔티티의 전방 선언은 피하세요.
  • 전방 선언을 사용할 때는 코드의 의미가 변경되지 않는지 주의하세요.

정리

  • 원칙: 가능한 한 전방 선언을 피하고 필요한 헤더를 직접 포함한다.
  • 이유:
  • 전방 선언은 코드 의미를 조용히 변경할 수 있음
  • 자동화 도구의 동작을 방해할 수 있음
  • 라이브러리 변경에 취약함
  • std:: 네임스페이스의 경우 정의되지 않은 동작 발생 가능
  • 예외: 컴파일 시간이 매우 중요한 경우에만 신중하게 사용