기술 노트/Google C++ Style Guide

Google C++ Style Guide(2024) - 2장 헤더 파일 (Header File)

anothel 2024. 11. 5. 18:57

2장 헤더 파일 (Header File)

일반적으로, 모든 .cc 파일에는 관련된 .h 파일이 있어야 한다. 다만, 단위 테스트 파일이나 main() 함수만 포함하는 작은 .cc 파일 등은 예외로 인정된다.

헤더 파일을 올바르게 사용하는 것은 코드의 가독성, 크기, 성능에 큰 영향을 미친다. 이 가이드에서는 헤더 파일 사용 시 발생할 수 있는 여러 문제점을 피할 수 있는 규칙들을 제시한다.

2.1 자체 포함 헤더 (Self-contained Headers)

헤더 파일은 독립적으로 컴파일될 수 있도록 자체 포함형으로 작성해야 하며, 확장자는 .h로 끝나야 한다. 헤더가 아닌 포함용 파일은 .inc 확장자를 사용하며, 필요한 경우에만 제한적으로 사용해야 한다.

모든 헤더 파일은 자체 포함형이어야 하며, 이를 포함하는 사용자나 리팩토링 도구가 특별한 조건 없이 포함할 수 있어야 한다. 이를 위해 헤더 가드가 필요하며, 필요한 모든 헤더를 포함해야 한다.

헤더에서 인라인 함수나 템플릿을 선언하고, 해당 헤더의 클라이언트가 이를 인스턴스화해야 한다면, 인라인 함수와 템플릿의 정의 역시 헤더에 직접 포함하거나 헤더가 포함하는 파일에 위치시켜야 한다. 예전에는 별도의 -inl.h 파일에 정의를 두는 방식이 있었으나, 이는 이제 허용되지 않는다. 템플릿 인스턴스가 특정 .cc 파일 내에서만 이루어지는 경우, 해당 템플릿 정의를 .cc 파일에 유지할 수 있다.

자체 포함되지 않은 파일이 필요한 경우는 매우 드물며, 보통 다른 파일의 중간에 포함되어야 할 때 발생한다. 이러한 파일에는 헤더 가드나 필요한 전제조건들이 포함되지 않을 수 있으며, .inc 확장자를 사용해 이름을 지정한다. 가능한 한 자체 포함형 헤더를 사용하는 것이 바람직하다.

2.2 #define 가드 (The #define Guard)

모든 헤더 파일은 중복 포함을 방지하기 위해 #define 가드를 사용해야 한다. 심볼 이름의 형식은 <PROJECT>_<PATH>_<FILE>_H_로 작성하는 것이 원칙이다.

가드의 고유성을 보장하기 위해 프로젝트 소스 트리 내 전체 경로를 바탕으로 해야 한다. 예를 들어, foo 프로젝트의 foo/src/bar/baz.h 파일에는 다음과 같은 가드를 사용한다.

#ifndef FOO_BAR_BAZ_H_
#define FOO_BAR_BAZ_H_

...

#endif  // FOO_BAR_BAZ_H_

이렇게 작성하면 파일이 여러 번 포함되더라도 중복 정의를 방지할 수 있다.

2.3 사용한 것만 포함하기 (Include What You Use)

소스 또는 헤더 파일에서 다른 파일에 정의된 심볼을 참조할 경우, 반드시 해당 심볼의 선언이나 정의를 제공하는 헤더 파일을 직접 포함해야 한다. 단순히 편의를 위해 다른 헤더 파일을 포함하는 것은 피해야 한다.

직접 포함 원칙을 따르는 이유는, 다른 헤더 파일에 의존하는 중간 포함(transitive inclusion)에 기대지 않게 하려는 것이다. 이를 통해 필요하지 않은 #include 구문을 제거하더라도 다른 파일들이 깨지지 않도록 한다. 이러한 원칙은 관련 헤더에도 적용되며, 예를 들어 foo.cc 파일이 bar.h의 심볼을 사용한다면, foo.h가 bar.h를 포함하더라도 foo.cc 파일에서 bar.h를 직접 포함해야 한다.

2.4 전방 선언 (Forward Declarations)

가능한 한 전방 선언을 피하고 필요한 헤더 파일을 포함하는 것이 좋다. 전방 선언은 정의 없이 엔티티를 선언하는 방식으로, 예를 들면 다음과 같다.

// In a C++ source file:
class B;
void FuncInB();
extern int variable_in_b;
ABSL_DECLARE_FLAG(flag_in_b);

전방 선언은 컴파일 시간을 단축하고 불필요한 재컴파일을 줄일 수 있는 장점이 있지만, 여러 가지 문제점이 있다.

  • 의존성 은닉
    • 전방 선언은 코드에서 의존성을 숨길 수 있어, 헤더 파일이 변경되더라도 사용자 코드에서 재컴파일이 누락될 수 있다.
  • 자동 도구 활용 어려움
    • 전방 선언은 심볼을 정의하는 모듈을 자동화된 도구가 찾기 어렵게 만든다.
  • 라이브러리 변경 취약성
    • 전방 선언을 사용하면 함수나 템플릿의 매개변수 확장, 기본 값이 있는 템플릿 매개변수 추가, 네임스페이스 변경 등 API 수정 시 호환성 문제가 발생할 수 있다.
  • 표준 네임스페이스 문제
    • std:: 네임스페이스 내의 심볼을 전방 선언하는 것은 정의되지 않은 동작을 초래할 수 있다.
  • 코드 의미 변경 가능성
    • #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); }  // Calls f(B*)
  • 위 코드 예시 중 good_user.cc에서 #include "b.h"를 통해 B와 D의 정의가 포함되면, test(D* x) 함수 내에서 f(x)를 호출할 때 D* 타입의 포인터가 B* 타입으로 암묵적 변환되어 f(B*)가 호출된다. 이는 D가 B를 상속했기 때문에 가능한 동작이다.
    • 그런데 만약 #include "b.h"를 제거하고 B와 D에 대해 전방 선언만 사용한다면, 컴파일러는 D가 B를 상속한다는 사실을 알지 못하게 된다. 결과적으로 test() 내의 f(x) 호출 시 D*를 B*로 변환할 수 없기 때문에, f(B*) 대신 f(void*)가 호출된다.
  • 코드 가독성 저하
    • 여러 심볼을 전방 선언하는 것보다 헤더 파일을 직접 포함하는 것이 간결하며, 포인터 멤버를 사용해 전방 선언을 강제하면 코드가 복잡해지고 성능에 영향을 미칠 수 있다.

다른 프로젝트에 정의된 엔티티에 대한 전방 선언은 피하는 것이 바람직하다.

2.5 인라인 함수 (Inline Functions)

함수를 인라인으로 정의할 때는 크기가 작은 경우, 보통 10줄 이하일 때만 사용하는 것이 좋다. 인라인 함수는 일반적인 함수 호출 방식 대신 컴파일러가 확장하여 실행할 수 있도록 정의하는 방식이다.

작은 함수라면 인라인으로 정의하면 더 효율적인 오브젝트 코드를 생성할 수 있다. 접근자(accessor)와 변경자(mutator), 그리고 성능이 중요한 짧은 함수는 인라인으로 정의해도 무방하다.

그러나 인라인 사용이 지나치면 오히려 프로그램이 느려질 수 있다. 함수 크기에 따라 인라인 사용이 코드 크기를 줄이거나 늘릴 수 있으며, 작은 접근자 함수는 코드 크기를 줄이지만, 큰 함수는 코드 크기를 크게 증가시킬 수 있다. 현대 프로세서에서 작은 코드 크기가 더 빠르게 실행되는 경우가 많기 때문에, 명령어 캐시 활용 측면에서 작은 함수에만 인라인을 권장한다.

규칙 요약:

  • 함수가 10줄을 넘지 않으면 인라인화해도 무방하다.
  • 소멸자(destructor)는 암시적인 멤버 및 기본 소멸자 호출로 인해 보이는 것보다 길어질 수 있으므로 주의가 필요하다.
  • 반복문이나 switch 문이 포함된 함수는 인라인화하지 않는 것이 좋다. 단, 반복문이나 switch 문이 일반적으로 실행되지 않는 경우는 예외가 될 수 있다.

함수가 인라인으로 선언되더라도 반드시 인라인 처리되는 것은 아니다. 예를 들어, 가상 함수나 재귀 함수는 보통 인라인 처리되지 않는다. 재귀 함수는 일반적으로 인라인으로 만들지 않는 것이 좋으며, 가상 함수는 클래스 정의 내에 편의를 위해 포함하거나 접근자와 변경자의 동작을 문서화하기 위해 인라인으로 정의할 수 있다.

2.6 Include 파일의 이름과 순서 (Names and Order of Includes)

헤더 파일은 다음 순서로 포함되어야 한다

  • 관련 헤더, C 시스템 헤더, C++ 표준 라이브러리 헤더, 다른 라이브러리의 헤더, 프로젝트의 헤더.

모든 프로젝트의 헤더 파일은 프로젝트의 소스 디렉터리의 하위로 나열되어야 하며, UNIX 디렉터리 별칭 .(현재 디렉터리) 또는 ..(부모 디렉터리)을 사용해서는 안 된다. 예를 들어, google-awesome-project/src/base/logging.h는 다음과 같이 포함되어야 한다.

#include "base/logging.h"

헤더 파일은 라이브러리가 그렇게 하도록 요구하는 경우에만 앵글 브래킷 경로를 사용하여 포함되어야 한다. 특히, 다음과 같은 헤더들은 앵글 브래킷이 필요하다:

  • C 및 C++ 표준 라이브러리 헤더 (예: <stdlib.h> 및 <string>).
  • POSIX, Linux, Windows 시스템 헤더 (예: <unistd.h> 및 <windows.h>).
  • 드문 경우지만, 서드 파티 라이브러리 (예: <Python.h>).

dir2/foo2.h에 있는 내용을 구현하거나 테스트하는 주된 목적을 가진 dir/foo.cc 또는 dir/foo_test.cc에서는 다음과 같은 순서로 포함 파일을 나열한다.

  1. dir2/foo2.h.
  2. 빈 줄.
  3. C 시스템 헤더 및 .h 확장자를 가진 다른 앵글 브래킷 헤더들 (예: <unistd.h>, <stdlib.h>, <Python.h>).
  4. 빈 줄.
  5. 파일 확장자 없이 C++ 표준 라이브러리 헤더들 (예: <algorithm>, <cstddef>).
  6. 빈 줄.
  7. 다른 라이브러리의 .h 파일들.
  8. 빈 줄.
  9. 프로젝트의 .h 파일들.

각 비어 있지 않은 그룹을 하나의 빈 줄로 구분한다.

권장되는 순서를 사용하면, 관련 헤더인 dir2/foo2.h가 필요한 포함 파일을 누락할 경우, dir/foo.cc 또는 dir/foo_test.cc의 빌드가 깨지게 된다. 따라서 이 규칙은 무고한 다른 패키지의 사람들에게 문제가 생기기 전에, 이 파일을 작업하는 사람들이 먼저 빌드 문제를 확인할 수 있게 해준다.

dir/foo.cc와 dir2/foo2.h는 일반적으로 같은 디렉터리(예: base/basictypes_test.cc와 base/basictypes.h)에 있지만, 때로는 다른 디렉터리에 있을 수도 있다.

stddef.h와 같은 C 헤더는 본질적으로 그 C++ 대응 헤더(cstddef)와 교체 가능하다. 어느 스타일이든 허용되지만, 기존 코드와의 일관성을 선호해야 한다.

각 섹션 내의 포함 파일들은 알파벳 순서로 정렬되어야 한다. 이전의 코드가 이 규칙을 따르지 않는다면, 편리할 때 수정하는 것이 좋다.

예를 들어, google-awesome-project/src/foo/internal/fooserver.cc 파일의 포함 파일은 다음과 같을 수 있다.

#include "foo/server/fooserver.h"

#include <sys/types.h>
#include <unistd.h>

#include <string>
#include <vector>

#include "base/basictypes.h"
#include "foo/server/bar.h"
#include "third_party/absl/flags/flag.h"

예외: 때때로, 시스템별 코드는 조건부 포함을 필요로 한다. 이러한 코드는 다른 포함 파일 이후에 조건부 포함을 할 수 있다. 물론, 시스템별 코드는 작고 국한되게 유지해야 한다.

#include "foo/public/fooserver.h"

#include "base/port.h"  // LANG_CXX11을 위해.

#ifdef LANG_CXX11
#include <initializer_list>
#endif  // LANG_CXX11

참조URL

https://google.github.io/styleguide/cppguide.html

728x90