본문 바로가기
C++/Google C++ Style Guide

Google C++ Style Guide : 헤더 파일(Header Files)

by 개발자J의일상 2022. 5. 1.
반응형

헤더 파일(Header Files)

일반적으로 모든 .cc 파일에는 연관된 .h 파일이 있어야 한다.

유닛 테스트 및 main() 함수만 포함하는 작은 .cc 파일과 같은 몇 가지 common 예외가 있다.

 

헤더 파일을 올바르게 사용하면 코드의 가독성, 사이즈 및 성능에 큰 차이를 가져올 수 있다.

 

다음의 규칙들은 헤더 파일을 사용할 때 발생하는 다양한 위험에 대해 소개한다.


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

 

헤더 파일은 독립적이어야 하고(자체적으로 컴파일해야 함) .h로 끝나야 한다.

포함을 위한 비헤더 파일은 .inc로 끝나야 하며 신중하게 사용해야 한다.

 

여기서 포함을 위한 비헤더 파일은 무엇일까?

An .inc file is not a header file. it could be used to include a large defined array just to keep it out of the way of the source file editing.
 
– Weather Vane Jul 15, 2016 at 18:04

.inc 파일은 헤더가 아니고 크게 정의된 array를 넣어두는 곳이라고 소개하고 있다.

 

.inc
 files are often associated with templated classes and functions.

answered Jul 30, 2018 at 20:08

AllDay

 

선언(Declaration) 예제:

// Foo.h
#ifndef FOO_H
#define FOO_H

template<typename T>
class Foo {
public:
    Foo();
    void DoSomething(T x);
private:
    T x;
};

#include "Foo.inc"
#endif // FOO_H

정의(Definition) 예제:

// Foo.inc
#include "Foo.h"

template<typename T>
Foo<T>::Foo() {
    // ...
}

template<typename T>
void Foo<T>::DoSomething(T x) {
    // ...
}

다른 사람은 .inc는 template 클래스와 함수의 정의부를 구현할 때 종종 쓰인다고 한다.

보통 .cc나 .cpp에 구현하지 않나... 싶다.

 

모든 헤더 파일은 self-contained (독립적)이어야 한다.

사용자와 리팩토링 도구는 헤더를 포함하기 위해 특별한 조건을 따를 필요가 없다.

특히 헤더에는 헤더가드가 있어야 하고 필요한 다른 모든 헤더를 포함해야 한다.

 

여기서 독립적이라는 말은 "Foo.h", "Foo.cc"로 따로 파일을 만들어야 한다는 의미이다.

 

템플릿(template)인라인 함수(inline function)에 대한 정의를 동일한 파일에 배치하는 것을 선호한다.

이러한 construct의 정의는 이들을 사용하는 모든 .cc 파일에 포함되어야 한다. 그렇지 않으면 일부 빌드 구성에서 프로그램에 링크되지 않을 수 있다.

 

아래와 같이 정의와 선언을 동일한 곳에서 한다는 의미이다.

그렇기 때문에 사용하는 곳 모든 .cc 파일에 따로 작성을 해줘야 한다는 의미 같다.

#include <iostream>
 
using namespace std;

inline int Max(int x, int y) {
   return (x > y)? x : y;
}

// Main function for the program
int main() {
   cout << "Max (20,10): " << Max(20,10) << endl;
   cout << "Max (0,200): " << Max(0,200) << endl;
   cout << "Max (100,1010): " << Max(100,1010) << endl;
   
   return 0;
}

 

C++ 클래스에서 인라인 함수를 명시적으로 선언해야 하는 경우, 클래스 내에서 함수를 선언하고 인라인 키워드를 사용하여 클래스 외부에서 정의할 수 있다. 그러나 인라인 함수는 클래스 내부에서도 정의할 수 있다.

 

다음은 클래스를 처리하는 동안 인라인 함수를 구현하는 데 도움이 되는 C++ 코드의 예이다.

inline int A::max(int a, int b)//Inline function definition
{
return (a>b)?a:b;
} 
 
class A
{
 public:
    inline int max(int a, int b)//Inline function declaration with definition
    {
       return (a>b)?a:b;
    };
}

class A
{
 public:
    int max(int a, int b);//Inline function declaration
}

 

선언과 정의가 다른 파일에 있다면, 선언이 정의를 전이적으로 포함(include) 해야 한다.

이러한 정의를 별도로 포함된 헤더 파일(-inl.h)로 이동하지 마라. 이 관행은 과거에는 일반적이었지만 더 이상 허용되지 않는다.

 

예외로, 모든 관련 템플릿 인수 집합에 대해 명시적으로 인스턴스화 되는 템플릿 또는 클래스의 private 구현 세부 정보인 경우에는 템플릿을 인스턴스화 하는 하나의 .cc파일에만 정의할 수 있다.

 

포함되도록 설계된 파일이 독립적이지 않은 드문 경우가 있다. 

이들은 일반적으로 다른 파일의 중간과 같은 비정상적인 위치에 포함되도록 의도된 것이다.

이러한 경우들은 헤더 가드를 사용하지 않을 수 있으며, 전제 조건을 포함하지 않을 수 있다.

.inc 확장자를 사용하여 이러한 파일의 이름을 지정한다.

드물게 사용하고 가능한 경우 자체 포함된 헤더를 선호해라.

 

※ 위의 부분은 사실 명확하게 이해는 잘 되지 않는 부분이다.


#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_

사용하는 내용 포함 (Include What You Use)

 

소스 또는 헤더 파일이 다른 곳에 정의된 기호를 참조하는 경우, 파일에는 해당 기호의 선언 또는 정의를 적절하게 제공하려는 헤더 파일이 직접 포함되어야 합니다.

다른 이유로 헤더 파일을 포함하지 않아야 합니다.

 

전이적 포함(transitive inclusion)에 의존하지 마라. 

이를 통해 사람들은 클라이언트를 중단하지 않고 헤더에서 더 이상 필요하지 않은 #include 문을 제거할 수 있다.

이것은 관련 헤더에도 적용된다. foo.h에 bar.h가 포함되어 있어도 foo.cc에서 심볼을 사용하는 경우 foo.cc에 bar.h가 포함되어야 한다.

 

위의 예제는 다음과 같은 경우이다.

foo.h에 bar.h가 포함이 되어있고 foo.cc에서 bar.h를 사용하는 경우 이미 foo.h에서 bar.h를 include 하고 있지만 foo.cc에서 foo.h만 include 하지 말고 bar.h를 include 해야 된다는 것이다.

//foo.h
#include "bar.h"
class Foo 
{
public:
    Bar bar;
};
//bar.h
class Bar
{
};
//foo.cc
#include "foo.h"
#include "bar.h"
int main()
{
    Foo* foo = new Foo();
    Bar* bar = new Bar();
}

transitive inclusion에 대해 자세히 알아보기 위해 아래 글을 참고해보자.

It depends on whether that transitive inclusion is by necessity (e.g. base class) or because of an implementation detail (private member). 
To clarify, the transitive inclusion is necessary when removing it can only be done after first changing the interfaces declared in the intermediate header. Since that's already a breaking change, any .cpp file using it has to be checked anyway. 
Example: A.h is included by B.h which is used by C.cpp. If B.h used A.h for some implementation detail, then C.cpp shouldn't assume that B.h will continue to do so. But if B.h uses A.h for a base class, then C.cpp may assume that B.h will continue to include the relevant headers for its base classes. 
You see here the actual advantage of NOT duplicating header inclusions. Say that the base class used by B.h really didn't belong in A.h and is refactored into B.h itself. B.h is now a standalone header. If C.cpp redundantly included A.h, it now includes an unnecessary header.

https://softwareengineering.stackexchange.com/questions/262019/is-it-good-practice-to-rely-on-headers-being-included-transitively

전방 선언 (Forward Declarations)

가능하면 전방 선언(forward declaration)을 사용하지 마라. 대신 필요한 헤드를 포함해라. (Include What You Use)

 

정의 : "전방 선언"은 관련 정의가 없는 엔터티의 선언. 식별자를 정의하기 전에 식별자의 존재를 컴파일러에게 미리 알리는 방식

 

아래 예제들이 전방 선언의 예를 보여준다.

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

장점:

  • #include는 컴파일러가 더 많은 파일을 열고 더 많은 입력을 처리하도록 강제하므로 전방 선언은 컴파일 시간을 절약할 수 있습니다.
  • 전방 선언은 불필요한 재컴파일을 절약할 수 있다. #include는 헤더의 관련 없는 변경으로 인해 여러분의 코드를 더 자주 재 컴파일하도록 할 수 있다.

단점:

  • 전방 선언은 종속성을 숨길 수 있으므로, 헤더가 변경될 때 사용자 코드가 필요한 재컴파일을 건너뛸 수 있다.
  • #include 문과 반대되는 전방 선언은 자동 도구가 기호를 정의하는 모듈을 검색하기 어렵게 만든다.
  • 라이브러리에 대한 후속 변경으로 인해 전방 선언이 손상될 수 있다. 함수 및 템플릿의 전방 선언은 헤더 소유자가 매개변수 유형 확장, 기본값이 있는 템플릿 매개변수 추가 또는 새 네임스페이스로의 마이그레이션과 같이 API에 다른 방식으로 호환 가능한 변경을 수행하는 것을 방지할 수 있다.
  • namespace std::에서 전방 선언 기호는 정의되지 않은 동작을 생성한다.
  • 전방 선언이 필요한지 전체 #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); }  // calls f(B*)
  • #include가 B와 D에 대한 전방 선언으로 대체된 경우 test()는 f(void*)를 호출한다.
이 부분이 이해가 안 되어서 찾아보니 나와 같은 질문을 한 사람이 있었다.

B와 D에 대한 전방 선언으로 대체되었다는 의미는 아래와 같이 변경된 것이고 inheritance 관계가 없어졌으므로 f(D*)에 대해 선언된 것이 없기 때문에 f(void*)가 호출되는 것이다.
struct B{};
struct D{};

https://stackoverflow.com/questions/50353757/forward-declaration-vs-include-in-google-c-style-guide
  • 헤더에서 여러 기호를 전방 선언하는 것은 단순히 헤더를 #include 하는 것보다 더 장황할 수 있다.
  • 전방 선언을 가능하게 하는 코드 구조화(예: 객체 멤버 대신 포인터 멤버 사용)는 코드를 더 느리고 더 복잡하게 만들 수 있다.

결정:

 

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


인라인 함수 (Inline Functions)

10줄 이하와 같이 작을 때만 함수를 인라인으로 정의하라

 

정의: 

컴파일러가 일반적인 함수 호출 메커니즘을 통해 호출하는 대신 인라인으로 확장할 수 있는 방식으로 함수를 선언할 수 있다.

 

장점: 

인라인 된 함수가 짧은 경우, 함수를 인라인 하면 보다 효율적인 객체 코드를 생성할 수 있다.

accessor와 mutator, 기타 짧고 성능이 중요한 함수를 자유롭게 인라인 하라.

 

*여기서 accessor와 mutator는 아래와 같은 함수를 말한다.

Accessor and Mutator Functions
Accessor and mutator 함수는 (a.k.a. set and get 함수) private 변수를 직접 변경하거나 액세스 할 수 있는 방법을 제공한다.

단점:

인라인을 과도하게 사용하면 실제로 프로그램이 느려질 수 있다.

함수의 크기에 따라 인라인 하면 코드 크기가 증가하거나 감소할 수 있다.

매우 작은 accessor 함수를 인라인하면 일반적으로 코드 크기가 줄어들고 매우 큰 함수를 인라인하면 코드 크기가 크게 늘어날 수 있다. 

최신 프로세서에서는 명령 캐시를 더 잘 사용하기 때문에 일반적으로 더 작은 코드가 더 빠르게 실행된다.

 

결정:

경험상 10줄 이상인 함수는 인라인 하지 않는 것이 좋다.

암시적 멤버 및 기본 소멸자 호출 때문에 나타나는 것보다 소멸자가 종종 긴 경우 소멸자를 조심하라!

 

또 다른 유용한 경험 법칙: 일반적으로 루프 또는 스위치 문으로 함수를 인라인 하는 것은 비용에 대해서 효율적이지 않다. (일반적인 경우에 loop 또는 switch 문이 절대 실행되지 않은 경우 제외)

 

함수가 일반적으로 선언된 경우에도 항상 인라인 되는 것은 아니라는 점을 아는 것이 중요하다.

예를 들어, 가상 및 재귀 함수는 일반적으로 인라인 되지 않는다.

일반적으로 재귀 함수는 인라인이 아니어야 한다.

가상 함수(virtual funciton)를 인라인으로 만드는 주된 이유는 편의를 위해, accessor 및 mutator와 같은 동작을 문서화하기 위해 해당 정의를 클래스에 배치하는 것이다.


include의 이름과 순서 (Names and Order of Includes)

헤더를 다음 순서로 포함해라:

관련 헤더, C 시스템 헤더, C++ 표준 라이브러리 헤더, 기타 라이브러리 헤더, 프로젝트 헤더

 

프로젝트의 모든 헤더 파일은 UNIX 디렉터리 별칭 .(현재 디렉터리) 또는 ..(상위 디렉터리)를 사용하지 않고 프로젝트 소스 디렉터리의 하위 항목으로 나열되어야 한다. 

예를 들어 google-awesome-project/src/base/logging.h는 다음과 같이 포함되어야 한다.

#include "base/logging.h"

indir2/foo2.h를 구현하거나 테스트하는 것이 주요 목적인 dir/foo.cc 또는 dir/foo_test.cc에서 다음과 같은 순서로 include하라.

  1. dir2/foo2.h.
  2. 빈 라인
  3. C 시스템 헤더 (더 정확하게는: .h 확장자를 <>로 감싼다.), 예 : <unistd.h>, <stdlib.h>
  4. 빈 라인
  5. C++ standard 라이브러리 헤더 (확장자 없이), 예 : <algorithm>, <cstddef>
  6. 빈 라인
  7. 다른 라이브러리의 .h 파일들
  8. 빈 라인
  9. 당신의 프로젝트의 .h파일들

각각의 그룹을 하나의 빈 라인으로 구분한다.

 

선호하는 순서로, 관련 헤더 dir2/foo2.h가 필요한 include들을 생략하면, 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)와 상호 교환할 수 있다.

두 스타일 모두 허용되지만, 기존 코드와의 일관성을 선호한다.

 

각 섹션 내에서 include는 알파벳순으로 정렬되어야 한다. 이전 코드는 이 규칙을 따르지 않을 수 있으므로 편리할 때 수정해야 한다.

 

예를 들어 include 된 ingoogle-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 "base/commandlineflags.h"
#include "foo/server/bar.h"

 

예외: 

 

때로는 시스템 특정 코드에 조건부 포함이 필요한 경우가 있다. 

이러한 코드는 다른 #include 뒤에 조건부 포함을 넣을 수 있다.

물론 시스템 고유의 코드를 작게 유지하고 현지화해야 한다. 

#include "foo/public/fooserver.h"

#include "base/port.h"  // For LANG_CXX11.

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

 

https://google.github.io/styleguide/cppguide.html#Self_contained_Headers
300x250

댓글