전방 선언 (Forward Declarations)¶
가능한 한 전방 선언을 피하고, 필요한 헤더를 직접 포함하세요.
"전방 선언(forward declaration)"은 정의 없이 엔티티만 선언하는 것입니다.
전방 선언은 컴파일 시간을 절약할 수 있습니다. #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; // 변수가 있다고 선언만 함
전방 선언의 장점¶
-
컴파일 시간 절약:
#include를 사용하면 컴파일러가 해당 헤더 파일 전체를 읽고 처리해야 합니다. 전방 선언을 사용하면 헤더 파일을 읽을 필요가 없어 컴파일이 빨라질 수 있습니다. -
불필요한 재컴파일 방지: 헤더 파일이 변경되면 그 헤더를 포함한 모든 파일이 재컴파일됩니다. 전방 선언을 사용하면 헤더 파일의 내부 구현이 변경되어도 재컴파일이 필요 없을 수 있습니다.
-
의존성 숨김: 전방 선언을 사용하면 실제 헤더 파일에 대한 의존성이 숨겨져, 헤더가 변경되어도 사용자 코드가 영향을 받지 않을 수 있습니다.
전방 선언의 단점¶
-
자동화 도구의 어려움: 전방 선언을 사용하면 자동화 도구가 실제 정의가 어디에 있는지 찾기 어렵습니다. 이는 리팩토링이나 코드 분석 도구의 동작에 영향을 줄 수 있습니다.
-
호환성 문제: 전방 선언된 함수나 템플릿의 시그니처가 변경되면 전방 선언이 깨질 수 있습니다. 예를 들어:
-
매개변수 타입이 변경되면
- 템플릿 매개변수가 추가되면
- 네임스페이스가 변경되면
전방 선언이 더 이상 유효하지 않게 됩니다.
-
std:: 네임스페이스의 위험:
std::네임스페이스의 심볼을 전방 선언하면 정의되지 않은 동작이 발생할 수 있습니다. 표준 라이브러리의 타입들은 반드시 적절한 헤더를 포함해서 사용해야 합니다. -
코드 의미 변경:
#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*)가 호출됩니다. 이는 원래 의도와 다릅니다!
-
가독성 저하: 여러 심볼을 전방 선언하는 것은 단순히 헤더를 포함하는 것보다 더 장황할 수 있습니다.
-
성능 및 복잡성: 전방 선언을 가능하게 하기 위해 코드 구조를 변경해야 할 수 있습니다 (예: 객체 멤버 대신 포인터 멤버 사용). 이는 코드를 더 느리게 만들고 복잡하게 만들 수 있습니다.
권장 사항¶
- 가능한 한 전방 선언을 피하고 필요한 헤더를 직접 포함하세요.
- 특히 다른 프로젝트에 정의된 엔티티의 전방 선언은 피하세요.
- 전방 선언을 사용할 때는 코드의 의미가 변경되지 않는지 주의하세요.
정리¶
- 원칙: 가능한 한 전방 선언을 피하고 필요한 헤더를 직접 포함한다.
- 이유:
- 전방 선언은 코드 의미를 조용히 변경할 수 있음
- 자동화 도구의 동작을 방해할 수 있음
- 라이브러리 변경에 취약함
std::네임스페이스의 경우 정의되지 않은 동작 발생 가능- 예외: 컴파일 시간이 매우 중요한 경우에만 신중하게 사용