일반적으로, 모든 .cc 파일은 연관된 .h 파일을 가져야만 한다. 몇몇 일반적인 예외는, unit tests와 작은 .cc 파일 그리고 단순히 main() 함수를 포함하는 cc 파일이다.
header 파일의 정확한 사용은 코드의 가독성, 크기, 성능에 큰 차이를 생성할 수 있다.
다음 규칙들은 header 파일을 사용할 때 빠질 수 있는 다양한 함정을 통해 당신을 가이드한다.
Self-contained Headers
Header file은 반드시 self-contained (compile on their own)해야 하며, 확장자는 .h 이어야 한다. 단순히 포함을 목적으로 하지만 정식 헤더가 아닌 경우, Non-header files은 .inc 확장자를 사용하며, 드물게 사용한다.
모든 헤더 파일은 self-contained 해야만 한다. 유저와 리팩토링 툴은 헤더를 포함할 특별한 조건을 준수할 필요가 없어야한다. 특히, 헤더는 header guards를 가져야 하며 필요한 다른 모든 헤더를 포함해야 한다.
헤더가 inline function 또는 인스턴스화할 template를 선언하면, inline function과 templates는 헤더 파일에 직접 정의되어야 한다. 이러한 정의를 별도의 include file (.inc)에 옮겨서는 안된다; 이러한 관습은 과거에 일반적이었으나, 이제는 허용되지 않는다. 만약 template의 모든 instantiation이 하나의 .cc 파일에서 발생한다면, 그것들은 explicit 하거나 해당 .cc 파일에서만 접근 가능하기 때문에 template definition을 해당 파일에 정의할 수 있다.
매우 드물게, self-contained하지 않도록 설계된 파일이 있다. 이러한 파일들은 다른 파일의 중간 위치에서 일반적이지 않은 위치에서 포함되도록 의도된다. 이들은 header guards를 사용하지 않으며 필요한 다른 헤더를 포함하지 않을 수 있다. 이러한 파일의 이름은 .inc 확장자를 사용하며, 매우 제한적으로 사용해야하며, 가능한 self-contained header를 사용한다.
Self-contained 의미
Self-contained Header File이란, 다른 header 파일을 include해야만 컴파일되도록 하지 말라는 의미다. 예를 들어, 다음과 같은 코드는 self-contained header file이 아니다.
// foo.h class Foo { std::string foo_str; };
해당 헤더파일을 foo.c 에서 컴파일 하게되면 컴파일 에러가 발생한다.// foo.cc #include "foo.h" // Error int main() { Foo foo; }
즉 foo.h에 #include <string>이 추가되어야 Self-contained header file이 된다.
Template
C++에서 Template을 사용할 때 주의할 점은 헤더 파일에 선언(declation) 뿐 아니라 정의(definition)까지 작성해야한다. 예를 들어,
// foo.h #include <iostream> template <typename T> void Foo(T t);// foo.cc #include "foo.h" #include <iostream> template<typename T> void Foo(T t) { std::cout << "Foo Function " << t << std::endl; }
위와 같은 코드가 있을 경우, main 함수에서 다음처럼 사용하면 컴파일 에러가 발생한다.// main.cc #include "foo.h" int main() { Foo<int>(); // undefined reference to 'void Foo<int>(int)' }template은 사용 시점에 인스턴스화가 되는데, "Foo.h"에서 정의를 찾을 수 없기 때문에, main.cc를 컴파일 시 심볼을 생성하지 않는다. 이후, 링크 시 컴파일 에러를 일으킨다.
g++ main.cc foo.cc -c # Success g++ main.o foo.o # Error그래서 foo.cc 파일에 정의된 정의는 foo.h 파일에 정의해줘야만 한다.
// foo.h #include <iostream> template <typename T> void Foo<T>(T t); template <typename T> void Foo<T>(T t) { std::cout << "Foo Function " << t << std::endl; }근데 template의 단점은 해당 header file을 include 한 모든 cc 파일에서 컴파일 되기 때문에, 컴파일 시간이 증가하게 된다.
예를 들어, bar.cc, baz.cc 파일에서 foo.h를 include 했다고 가정해보자.// bar.cc #include "foo.h" #include <iostream> void Bar() { std::cout << "Bar Function\n"; Foo<int>(0); }// baz.cc #include "foo.h" void Baz() { std::cout << "Baz Function\n"; Foo<int>(0); }각각 컴파일을 하게 되면, bar.o, baz.o 파일에는 각각 Foo<int> 심볼이 생성되게 된다. 그러면 "나중에 링크 과정에서 같은 심볼이 여러 개 존재하게 되는데 심볼이 충돌하지 않을까?" 하는 의문을 가질 수 있다. 예를 들어
g++ main.o bar.o baz.o # bar.o, baz.o에는 같은 심볼이 존재컴파일러는 template으로 생성된 심볼은 weak symbol로 관리한다. weak symbol로 되어 있으면 중복되어도, 특정 symbol을 사용해 bind하게 된다. 다음은 bar.o 의 심볼 테이블이다.
readelf -s bar.o Symbol table '.symtab' contains 21 entries: Num: Value Size Type Bind Vis Ndx Name 0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND 1: 0000000000000000 0 FILE LOCAL DEFAULT ABS bar.cc 2: 0000000000000000 0 SECTION LOCAL DEFAULT 2 .text 3: 0000000000000000 0 SECTION LOCAL DEFAULT 5 .bss 4: 0000000000000000 1 OBJECT LOCAL DEFAULT 5 _ZStL8__ioinit 5: 0000000000000000 0 SECTION LOCAL DEFAULT 6 .rodata 6: 0000000000000000 0 SECTION LOCAL DEFAULT 7 .text._Z3FooIiEvT_ 7: 000000000000002e 86 FUNC LOCAL DEFAULT 2 _Z41__static_ini[...] 8: 0000000000000084 25 FUNC LOCAL DEFAULT 2 _GLOBAL__sub_I__[...] 9: 0000000000000000 46 FUNC GLOBAL DEFAULT 2 _Z3Barv 10: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND _ZSt4cout 11: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND _ZStlsISt11char_[...] 12: 0000000000000000 77 FUNC WEAK DEFAULT 7 _Z3FooIiEvT_ 13: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND _ZNSolsEi 14: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND _GLOBAL_OFFSET_TABLE_ 15: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND _ZSt4endlIcSt11c[...] 16: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND _ZNSolsEPFRSoS_E 17: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND _ZNSt8ios_base4I[...] 18: 0000000000000000 0 NOTYPE GLOBAL HIDDEN UND __dso_handle 19: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND _ZNSt8ios_base4I[...] 20: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND __cxa_atexit확인해보면
12: 0000000000000000 77 FUNC WEAK DEFAULT 7 _Z3FooIiEvT_
Bind 항목이 Weak인 것을 확인할 수 있다.
The #define Guard
모든 header file은 중복 포함을 방지하기 위해 #define guards를 가져야만 한다. 심볼 이름 포맷은 <PROJECT>_<PATH>_<FILE>_H_이다.
uniqueness를 보장하기 위해, 프로젝트의 소스트리에서 전체 경로에 근거하도록 한다. 예를 들어, 파일이 foo 프로젝트에서 foo/src/bar/baz.h 이면 다음 guard를 가져야만 한다.
#ifndef FOO_BAR_BAZ_H_
#define FOO_BAR_BAZ_H_
// ...
#endif // FOO_BAR_BAZ_H_
Include What You Use (IWYU)
만약 소스 또는 헤더 파일이 다른 곳에 정의된 심볼을 참조한다면, 파일은 심볼의 선언 또는 정의를 제공하는 파일을 직접 include해야만 한다. 다른 이유로 불필요하게 헤더 파일을 include 해서는 안된다.
간접 포함(transitive inclusions)에 의존하지 마라. 이 원칙을 지키면, 나중에 헤더에서 더 이상 필요 없는 include를 제거하더라도 그 헤더를 사용하는 다른 코드에서 컴파일 에러가 발생하지 않게 된다.
이 원칙은 관련된 파일에도 동일하게 적용된다. 예를 들어, foo.cc에서 bar.h에 정의된 심볼을 사용한다면, 비록 foo.h에서 bar.h를 include하더라도, foo.cc도 bar.h를 직접 include해야 한다.
Forward Declarations
가능하다면 전방 선언(forward declarations) 사용을 회피하라. 대신 필요한 headers를 포함하라.
Definition:
"forward declaration"은 연관된 정의(definition) 없이 entity의 선언이다.
// In a C++ source file:
class B;
void FuncInB();
extern int variable_in_b;
ABSL_DECLARE_FLAG(flag_in_b);
Pros:
- 전방 선언은 컴파일 시간을 줄여줄 수 있고, 반면 #includes는 컴파일러가 더 많은 파일을 열고 더 많은 입력을 처리하도록 강제한다.
- 전방 선언은 불필요한 재컴파일을 줄여줄 수 있다. #includes는 헤더에서 연관되지 않은 변경 때문에 코드가 더 자주 재컴파일 되도록 강제한다.
Cons:
- 전방선언은 의존성을 숨길 수 있는데, 유저 코드가 헤더가 변경될 때 필요한 재컴파일을 스킵하도록 한다.
- #include statement와 반대로 전방 선언은 symbol을 정의하는 모듈을 자동 툴이 발견하는 것을 어렵게 한다.
- 라이브러리 쪽 코드가 변경되면 전방 선언이 깨질 가능성이 있다.
- 함수나 템플릿의 전방 선언은 원래 API에 큰 문제 없는 수정(예: 인자 타입 변경, 기본 값이 있는 템플릿 파라미터 추가, 네임스페이스 변경)을 방해할 수 있다.
- std:: 네임스페이스의 심볼을 전방 선언하면 정의되지 않은 동작(undefined behavior)이 발생한다.
- 전방 선언 또는 전체 #include가 필요한지 판단하기 어렵다.
만약 #include가 B, D의 전방선언으로 대체되면, test()는 f(void*)를 호출할 것이다.// 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 하는 것 보다 더 장황하게 만든다.
- 전방 선언을 가능하게 하기 위해 코드 구조는 더 복잡해지고 느려지게 할 수 있다.
Decision:
다른 프로젝트에서 정의된 entities의 전방 선언은 피하자.
Defining Functions in Header Files
header file에서 함수의 정의(definition)을 선언(declaration) 시점에 정의를 작성하는 것은, 그 함수가 매우 짧을 때만 허용한다. 만약 짧지 않지만 반드시 header에 있어야 하는 특별한 이유가 있다면, 그 정의를 헤더 파일 내부에 작성하라. 만약 ODR-safe하는 것이 필요하다면, inline specifier를 추가하라.
Definition:
헤더파일에 정의된 함수는 가끔 "inline functions"으로 불린다. 그러나 이 용어는 몇몇 명확히 구별되지만 overlapping situation을 가리키는 다소 overload된 용어이다.
- 텍스트적으로 inline symbol의 정의는 선언 지점에서 정의가 바로 reader에게 노출되는 것이다.
- 헤더 파일에서 정의된 함수나 변수는 컴파일러가 inline expresion으로 확장할 수 있게 해 보다 효츌적인 object 코드로 만든다.
- ODR-safe entities는 종종 header files에 inline 키워드로 정의된 것으로 "One Definition Rule"을 위반하지 않는다.
이러한 혼동은 주로 함수에서 자주 발생하지만, 변수에도 동일한 규칙이 적용된다.
Pros:
- 텍스트적으로 인라인 함수를 정의하는 것은 accessor 와 mutators 같은 단순한 함수의 boilerplate code를 줄인다.
- 위에 언급한 대로, 헤더 파일에서 함수 정의는 compiler에 의해 inline으로 확장되어 작은 함수의 경우 더 효율적인 object code를 생성할 수 있도록 한다.
- Function templates와 constexpr functions은 일반적으로 그것들을 선언한 header file에 정의될 필요가 있다(하지만 public part일 필요는 없다).
Cons:
- public API에 함수 정의를 삽입하면, API를 훓어보기(skim)하기 어려워지며, 코드를 읽는 사람이 부담을 느낄 수 있다 - 복잡한 함수일 수록 비용은 더 커진다.
- public definition은 불필요한 구현 상세를 노출한다.
Decision:
약 10줄정도의 짧은 경우 함수를 public 선언에 정의한다. 더 긴 함수 바디는 .cc 파일에 두며, 기술적이나 성능 상의 이유로 헤더에 둘 수 있다.
심지어 header에 반드시 정의해야 하는 경우라도 public 영역에 넣을 이유는 없다. 대신 정의는 class의 private section이나, word internal을 포함하는 namespce 내부 또는 아래 같은 header 내부에 존재할 수 있다.
header file에 정의가 있을 경우, inline을 명시해 ODR-safe를 가져야하거나 function template 또는 처음 선언될 때 class body에 정의되는 것으로 암시적으로 명시된 inline이 정의되어야 한다.
template <typename T>
class Foo {
public:
int bar() { return bar_; }
void MethodWidthHugeBody();
private:
int bar_;
};
// Implementation details only below here
template <typename T>
void Foo<T>::MethodWithHugeBode() {
// ...
}
inline 키워드
inline 키워드로 선언된 함수는 컴파일 시 함수로 코드를 그대로 삽입하라는 힌트를 컴파일러에 제공한다. inline은 명시적으로 선언할 수도 있고, 암시적으로 컴파일러가 최적화할 수 있다. 인라인으로 선언된 함수는 ODR-safe하게 컴파일된다. 다음 예시 코드를 살펴보자.
#include <iostream> // 1. free function + inline inline void free_inline() { std::cout << "free inline\n"; } // 2. static + inline function static inline void static_inline() { std::cout << "static inline\n"; } // 3. member function + inline class A { public: inline void member_inline() { std::cout << "member inline\n"; } }; int main() { free_inline(); static_inline(); A a; a.member_inline(); }g++ test_inline.cc -creadelf -s test_inline.o Symbol table '.symtab' contains 22 entries: Num: Value Size Type Bind Vis Ndx Name 0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND 1: 0000000000000000 0 FILE LOCAL DEFAULT ABS test_inline.cc 2: 0000000000000000 0 SECTION LOCAL DEFAULT 3 .text 3: 0000000000000000 0 SECTION LOCAL DEFAULT 6 .bss 4: 0000000000000000 1 OBJECT LOCAL DEFAULT 6 _ZStL8__ioinit 5: 0000000000000000 0 SECTION LOCAL DEFAULT 7 .rodata 6: 0000000000000000 0 SECTION LOCAL DEFAULT 8 .text._Z11free_i[...] 7: 0000000000000000 32 FUNC LOCAL DEFAULT 3 _ZL13static_inlinev 8: 0000000000000000 0 SECTION LOCAL DEFAULT 10 .text._ZN1A13mem[...] 9: 000000000000006c 86 FUNC LOCAL DEFAULT 3 _Z41__static_ini[...] 10: 00000000000000c2 25 FUNC LOCAL DEFAULT 3 _GLOBAL__sub_I_main 11: 0000000000000000 36 FUNC WEAK DEFAULT 8 _Z11free_inlinev 12: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND _ZSt4cout 13: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND _ZStlsISt11char_[...] 14: 0000000000000000 44 FUNC WEAK DEFAULT 10 _ZN1A13member_in[...] 15: 0000000000000020 76 FUNC GLOBAL DEFAULT 3 main 16: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND __stack_chk_fail 17: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND _ZNSt8ios_base4I[...] 18: 0000000000000000 0 NOTYPE GLOBAL HIDDEN UND __dso_handle 19: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND _GLOBAL_OFFSET_TABLE_ 20: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND _ZNSt8ios_base4I[...] 21: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND __cxa_atexit생성된 심볼 테이블을 살펴보면, free_inline 함수, static_inline 함수, member_inline 함수 차이를 확인할 수 있다. 먼저 free_inline 함수는 자유함수인데 inline으로 선언된 함수이다. WEAK 속성으로 컴파일 되었다. static_inline 함수는 LOCAL 속성으로 컴파일되었다. member_inline 함수는 WEAK 속성으로 컴파일 되었다.
이는 inline 키워드의 목적은 컴파일러한테 컴파일 시 함수의 호출 대신 코드를 직접 삽입하라는 힌트를 제공하기도 하지만, 진정한 목적은 심볼의 속성을 WEAK로 생성해 ODR-safe(One Definition Rule)를 달성하기 위함이다.
Names and Order of Includes
다음 순서로 header를 include하라: 연관 header, C system headers, C++ standard library headers, 다른 라이브러리 headers, 프로젝트의 headers.
모든 프로젝트의 헤더는 . (현재 디렉터리), .. (부모 디렉터리) 같은 UNIX 디렉터리 aliases 사용 없이 프로젝트 소스 디렉터리의 하위를 나열해야만 한다. 예를 들어 .google-awsome-project/src/base/logging.h는 다음과 같이 include 되어야만 한다.
#include "base/logging.h"
Headers는 라이브러리에서 필요로 하는 경우에만 angle-bracket으로 된 path를 사용해서 include해야만 한다. 특히 다음 경우는 angle brackets을 필요로 한다:
- C and C++ standard library headers (e.g. <stdlib.h> and <string>).
- POSIX, Linux, and Window system headers (e.g. <unistd.h> and <windows.h>).
- in rare cases, third_party libraries (e.g. <Python.h>).
dir/foo.cc 또는 dir/foo_test.cc에서, 해당 파일의 주요 목적은 구현하는 것 또는 dir2/foo2.h를 테스트 하는 것이면, 다음 순서로 include해야한다.
- dir2/foo2.h
- A blank line
- C system headers, and any other headers in angle brackets with the .h extension, e.g., <unistd.h>, <stdlib.h>, <Python.h>.
- A blank line
- C++ standard library headers (without file extension), e.g., <algorithm>, <cstddef>.
- A blank line
- Other libraries' .h files.
- A blank line
- Your project's .h files
별도의 각 비어있지 않은 그룹은 한 줄 비어둔다.
선호 순서에 따라, 만약 관련 header dir2/foo2.h가 필요한 include를 생략한다면, dir/foo.cc 또는 dir/foo_test.cc의 빌드는 중단될 것이다. 그러므로, 다른 패키지에서 작업하는 무관한 사람들이 아닌 이 파일들에서 작업하는 사람들을 위해 첫 번째로 빌드 중단을 보여준다.
dir/foo.cc와 dir2/foo2.h는 주로 같은 디렉터리에 있으나 (e.g., base/basictypes_test.cc and base/basictypes.h), 다른 디렉터리에 가끔 있을지 모른다.
stddef.h 같은 C headers는 C++ 에 대응되는 (cstddef)로 변경해서 사용할 수 있다. 두 스타일 다 가능하지만, 기존 코드와 일관성을 유지하는 것이 선호된다.
각 세션 내부에서, include는 알파벳 순서여야만 한다. 오래된 코드는 이 규칙을 준수하지 않을 수 있으니, 편리할 때 수정해야만 한다.
예를 들어, google-awesome-project/src/foo/internal/fooserver.cc에서 includes는 다음처럼 보인다:
#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"
Exception:
가끔, system-specific 코드는 조건 include가 필요하다. 이런 코드는 다른 include 이후 조건적으로 include가 존재한다. 물론, system-specific code는 작고 localized로 유지하자.
#include "foo/public/fooserver.h"
#include "base/port.h" // For LANG_CXX11.
#ifdef LANG_CXX11
#include <initializer_list>
#endif // LANG_CXX11
'C++ > Google C++ Style Guide' 카테고리의 다른 글
| Other C++ Features (0) | 2025.05.26 |
|---|---|
| Google-Specific Magic (0) | 2025.05.26 |
| Functions (1) | 2025.05.25 |
| Classes (0) | 2025.05.20 |
| Scoping (0) | 2025.05.15 |