본문 바로가기
알아두면 손해 없는 정보 저장소

Google C++ Style Guide 3

by anothel 2021. 7. 10.

Google C++ Sytle Guide를 해석해가며 공부한 후 배운 점과 느낀 점에 대해서 서술하려 한다.

 

C++ Version, Header Files


 

C++ Version


현재, 코드는 C++17 환경이어야 한다(예를 들어, C++ 2x는 안된다.). 이 가이드가 목표로 하는 C++ 버전은 시간이 지남에 따라 (공격적으로) 발전한다.

non-standard extensions을 사용하지 마라.

C++14, C++17에서 나온 기능을 사용하기 전에 다른 환경으로의 이식 가능성을 고려해라.

더보기
더보기

Currently, code should target C++17, i.e., should not use C++2x features. The C++ version targeted by this guide will advance (aggressively) over time.

Do not use non-standard extensions.

Consider portability to other environments before using features from C++14 and C++17 in your project.

 

Header Files


일반적으로, 모든 .cc 파일은 관련된 .h 파일이 있어야 한다. 몇몇 일반적인 예외사항이 있긴 한데, 유닛 테스트 혹은 main() 함수를 포함하고 있는 작은 규모의 .cc 파일이 그렇다.

헤더 파일의 올바른 사용은 코드에서의 성능, 가독성, 크기 등에서 큰 차이를 만들 수 있다.

지금부터 소개하는 규칙은 헤더 파일을 사용함에 있어서 다양한 함정을 통과할 수 있도록 안내한다.

더보기
더보기

In general, every .cc file should have an associated .h file. There are some common exceptions, such as unit tests and small .cc files containing just a main() function.

Correct use of header files can make a huge difference to the readability, size and performance of your code.

The following rules will guide you through the various pitfalls of using header files.

Self-contained Headers


헤더 파일들은 독립적(스스로 컴파일)이어야 하며, .h로 끝나야 한다. 포함을 위한 비 헤더 파일은 .inc로 끝나야 하며 아껴서 사용해야 한다.

모든 헤더 파일들은 독립적이어야 한다. 사용자들과 리팩터링 툴들은 헤더를 포함하기 위한 특별한 상태에 대해서 들러붙을 필요가 없다. 특히, 헤더는 헤더 가드를 갖고 있어야 하며, 필요한 모든 다른 헤더를 포함하고 있어야 한다.

템플릿과 인라인 함수들을 한 파일에 그들의 정의들로써 같은 파일에 놓는 것을 선호해라. 이러한 구성의 정의들은 반드시 그것들을 사용하는 모든 .cc 파일에 포함되어야만 하고, 그렇지 않으면 프로그램은 몇몇 빌드 환경설정에서 링크에 실패할 것이다. 만약 선언과 정의가 다른 파일에 있다면, 선언이 정의를 타동적으로 포함(include) 해야 한다. 이 정의들을 별도로 포함(include)된 헤더 파일들(-inl.h)로 옮기지 마라; 이러한 행위는 과거에는 일반적이었지만, 더 이상 허락되지 않는다.

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

독립적이지 않은 것을 포함하도록 디자인된 파일의 경우는 거의 없다. 이런 파일은 전형적으로 또 다른 파일의 중간에서와 같이 일반적이지 않은 위치를 포함(include)하도록 의도되었다. 아마도 헤더 가드를 사용하지 않았을 것이며, 전제조건(전처리기?)을 포함(include) 하지 않았을 것이다. 확장명이 .inc와 같은 파일의 이름을 지정해라. 아껴서 사용하고, 가능하다면 독립적인 헤더 파일을 선호하라.

더보기
더보기

Header files should be self-contained (compile on their own) and end in .h. Non-header files that are meant for inclusion should end in .inc and be used sparingly.

All header files should be self-contained. Users and refactoring tools should not have to adhere to special conditions to include the header. Specifically, a header should have header guards and include all other headers it needs.

Prefer placing the definitions for template and inline functions in the same file as their declarations. The definitions of these constructs must be included into every .cc file that uses them, or the program may fail to link in some build configurations. If declarations and definitions are in different files, including the former should transitively include the latter. Do not move these definitions to separately included header files (-inl.h); this practice was common in the past, but is no longer allowed.

As an exception, a template that is explicitly instantiated for all relevant sets of template arguments, or that is a private implementation detail of a class, is allowed to be defined in the one and only .cc file that instantiates the template.

There are rare cases where a file designed to be included is not self-contained. These are typically intended to be included at unusual locations, such as the middle of another file. They might not use header guards, and might not include their prerequisites. Name such files with the .inc extension. Use sparingly, and prefer self-contained headers when possible.

The #define Guard


모든 헤더 파일은 다양한 충돌을 방지하지 위해서 #define 가드를 사용해야 한다. 기호 이름은 다음과 같은 형식이어야 한다. <PROJECT>_<PATH>_<FILE>_H_.

유일성을 보장하기 위해서, 헤더 가드는 프로젝트 소스 구조의 전체 경로에 기반해야 한다. 예를 들어, foo/src/bar/baz.h 파일이 프로젝트의 foo 안에 있다면 다음과 같은 헤더 가드를 사용해야 한다.

#ifndef FOO_BAR_BAZ_H_
#define FOO_BAR_BAZ_H_

...

#endif  // FOO_BAR_BAZ_H_
더보기
더보기

All header files should have #define guards to prevent multiple inclusion. The format of the symbol name should be <PROJECT>_<PATH>_<FILE>_H_.

To guarantee uniqueness, they should be based on the full path in a project's source tree. For example, the file foo/src/bar/baz.h in project foo should have the following guard:

#ifndef FOO_BAR_BAZ_H_
#define FOO_BAR_BAZ_H_

...

#endif  // FOO_BAR_BAZ_H_

Include What You Use


소스 파일 혹은 어딘가 다른 곳에 정의된 기호를 참조하는 헤더 파일의 경우, 그 기호의 선언이나 정의를 제공하기 위한 적절한 의도의 헤더 파일을 직접적으로 포함(include) 해야 한다. 다른 이유로 헤더 파일을 포함(include) 하지 말아야 한다.

타동적인 포함에 의존해서는 안된다. 이것은 클라이언트를 방해하지 않고, 더 이상 필요하지 않은 헤더의 #include 문구를 제거할 수 있도록 한다. 이것은 관련된 헤더 파일들에도 적용된다. - 만약 foo.cc에서 사용하는 기호가 bar.h로부터 왔다면, 비록 foo.h가 bar.h를 포함한다 할지라도 foo.cc는 bar.h를 포함(include) 해야 한다.

더보기
더보기

If a source or header file refers to a symbol defined elsewhere, the file should directly include a header file which properly intends to provide a declaration or definition of that symbol. It should not include header files for any other reason.

Do not rely on transitive inclusions. This allows people to remove no-longer-needed #include statements from their headers without breaking clients. This also applies to related headers - foo.cc should include bar.h if it uses a symbol from it even if foo.h includes bar.h.

Forward Declarations


가능하다면 전방 선언 사용을 피해라. 대신에 필요한 헤더 파일을 포함(include) 해라.

정의:

관련된 정의가 없는 독립체의 선언

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

찬성 의견:

  • #includes는 컴파일러가 더 많은 파일들을 열게 하고 더 많은 입력들을 처리하게 만들기 때문에, 전방 선언은 컴파일 시간을 단축시킬 수 있다.
  • #includes는 헤더 파일에 있는 관련되지 않은 변화로 인해서, 코드가 더 자주 재 컴파일되게끔 하기 때문에, 전방 선언은 불필요한 재컴파일을 절약할 수 있다.

반대 의견:

  • 전방 선언은 종속성을 숨길 수 있으며, 헤더 파일들에 변화가 있을 때 필요한 재컴파일을 건너뛸 수 있다.
  • #includ의 상태를 반대하는 전방 선언은 자동화 툴이 기호를 정의하고 있는 모듈을 찾는 것을 어렵게 만든다. 
  • 이후 라이브러리의 변화에 의해 전방 선언은 깨질 수 있다. 함수와 템플릿의 전방 선언은 헤더의 주인이 인자의 타입을 넓히는 것, 기본값으로써 템플릿 인자를 추가하는 것, 새로운 네임스페이스로 이주하는 것 등 다른 호환이 가능한 API로의 변경을 하지 못하도록 막을 수 있다.
  • 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가 전방 선언 BD(으)로 바뀌었다면, test()f(void*)를 호출할 것이다.

  • 헤더 파일에서 여러 기호의 전방 선언은 간단한 #include를 하고 있는 헤더 파일보다 더 장황해질 수 있다.
  • 전방 선언이 사용 가능하게 하는 구조(예를 들어, 객체 멤버 대신에 포인터 멤버를 사용하는 것)를 짜는 것은 코드가 더 느려지고 더 복잡하게끔 만들 수 있다.

결정:

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

더보기
더보기

Avoid using forward declarations where possible. Instead, include the headers you need.

Definition:

A "forward declaration" is a declaration of an entity without an associated definition.

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

Pros:

  • Forward declarations can save compile time, as #includes force the compiler to open more files and process more input.
  • Forward declarations can save on unnecessary recompilation. #includes can force your code to be recompiled more often, due to unrelated changes in the header.

Cons:

  • Forward declarations can hide a dependency, allowing user code to skip necessary recompilation when headers change.
  • A forward declaration as opposed to an #include statement makes it difficult for automatic tooling to discover the module defining the symbol.
  • A forward declaration may be broken by subsequent changes to the library. Forward declarations of functions and templates can prevent the header owners from making otherwise-compatible changes to their APIs, such as widening a parameter type, adding a template parameter with a default value, or migrating to a new namespace.
  • Forward declaring symbols from namespace std:: yields undefined behavior.
  • It can be difficult to determine whether a forward declaration or a full #include is needed. Replacing an #include with a forward declaration can silently change the meaning of code:
// 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*)

      If the #include was replaced with forward decls for B and D, test() would call f(void*).

  • Forward declaring multiple symbols from a header can be more verbose than simply #includeing the header.
  • Structuring code to enable forward declarations (e.g., using pointer members instead of object members) can make the code slower and more complex.

Decision:

Try to avoid forward declarations of entities defined in another project.

Inline Functions


함수가 작거나 10줄 미만일 때, 함수를 인라인으로 정의해라.

정의:

컴파일러가 일반적인 함수 호출 방식으로써 함수를 호출하기보다는 인라인으로 확장할 수 있도록 하는 방법으로 함수를 선언할 수 있다.

찬성 의견:

인라인 된 함수가 작을수록 인라인 함수는 더욱 효과적인 객체 코드를 만들 수 있다. 인라인 접근자, 뮤테이터 그리고 다른 짧은 코드, 성능에 크리티컬 한 함수들을 자유롭게 사용하라.

반대 의견:

인라인 함수를 과하게 사용하는 것은 실제적으로는 프로그램을 느리게 만든다. 함수의 크기에 의존해서 인라인 함수로 사용하는 것은 코드의 크기가 커지거나 작아지는 것을 야기할 수 있다. 매우 큰 함수가 환상적으로 코드의 크기를 증가시킬 수도 있는 반면에, 매우 작은 접근자 함수는 보통 코드의 크기를 감소시킨다. 현대의 프로세서는 명령 캐시를 더 잘 사용하기 때문에 보통 더 작은 크기의 코드를 실행시키고 더 빠르다. 

결정:

함수가 10 라인보다 많으면 인라인화 하지 않는 것이 꽤 괜찮은 경험적 규칙이다. implicit 된 멤버와 기본 소멸자의 호출로 인해 처음 생긴 때보다 더 긴 소멸자와 에 주의해라.

또 다른 괜찮은 경험적 규칙: 반복문과 스위치의 인라인 함수는 전형적으로 비용에 대해서 효과적이지 않다(일반적인 상황에서 반복문이나 스위치가 실행되지 않는 경우).

함수가 비록 일반적으로 선언되었다 할지라도, 항상 인라인 되지는 않다는 것은 중요하다; 예를 들어, 가상 함수 재귀 함수는 보통 인라인 되지 않는다. 보통 재귀 함수는 인라인 되지 않아야 한다. 가상 함수를 인라인 하려는 주된 이유는 정의를 클래스에 놓기 위해서 이거나, 편의를 위해서 혹은 행위를 문서화하려는 이유이다. 예를 들어, 접근자 그리고 뮤테이터가 있다.

더보기
더보기

Define functions inline only when they are small, say, 10 lines or fewer.

Definition:

You can declare functions in a way that allows the compiler to expand them inline rather than calling them through the usual function call mechanism.

Pros:

Inlining a function can generate more efficient object code, as long as the inlined function is small. Feel free to inline accessors and mutators, and other short, performance-critical functions.

Cons:

Overuse of inlining can actually make programs slower. Depending on a function's size, inlining it can cause the code size to increase or decrease. Inlining a very small accessor function will usually decrease code size while inlining a very large function can dramatically increase code size. On modern processors smaller code usually runs faster due to better use of the instruction cache.

Decision:

A decent rule of thumb is to not inline a function if it is more than 10 lines long. Beware of destructors, which are often longer than they appear because of implicit member- and base-destructor calls!

Another useful rule of thumb: it's typically not cost effective to inline functions with loops or switch statements (unless, in the common case, the loop or switch statement is never executed).

It is important to know that functions are not always inlined even if they are declared as such; for example, virtual and recursive functions are not normally inlined. Usually recursive functions should not be inline. The main reason for making a virtual function inline is to place its definition in the class, either for convenience or to document its behavior, e.g., for accessors and mutators.

Names and Order of Includes


헤더 include는 다음과 다음과 같은 순서로 되어야 한다: 관련된 헤더, C 시스템 헤더, C++ Standard 라이브러리 헤더, 기타 라이브러리의 헤더, 프로젝트의 헤더들.

모든 프로젝트의 헤더 파일은 유닉스 디렉터리의 별명자 . (현재 디렉터리) 또는 .. (상위 디렉터리)를 사용하지 않는 프로젝트 소스 디렉터리의 내림차순으로써 정렬되어야 한다. 예를 들어, google-awesome-project/src/base/logging.h는 다음을 반드시 include 해야 한다.

#include "base/logging.h"

dir2/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. 프로젝트의 .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)과 상호 호환될 수 있다는 것을 알아둬라. 두 스타일이 허용되지만, 기존 코드와의 일관성을 선호한다.

각각의 include 섹션은 알파벳 순서로 정렬되어야 한다. 이전 코드는 이 규칙을 수용하지 않을 수 있으며, 적절한 때에 수정되어야 한다는 것을 알아둬라.

예를 들어, google-awesome-project/src/foo/internal/fooserver.cc의 include 방식은 아래와 같이 표현할 수 있을 것이다:

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

예외:

때로는, 시스템별 코드는 조건부의 inlucde가 필요하다. 몇몇 코드는 다른 inlucde 후에 조건부의 include를 넣을 수 있다. 물론, 시스템별 코드는 작고 지역적으로 유지해라. 예:

#include "foo/public/fooserver.h"

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

#ifdef LANG_CXX11
#include <initializer_list>
#endif  // LANG_CXX11
더보기
더보기

Include headers in the following order: Related header, C system headers, C++ standard library headers, other libraries' headers, your project's headers.

All of a project's header files should be listed as descendants of the project's source directory without use of UNIX directory aliases . (the current directory) or .. (the parent directory). For example, google-awesome-project/src/base/logging.h should be included as:

#include "base/logging.h"

In dir/foo.cc or dir/foo_test.cc, whose main purpose is to implement or test the stuff in dir2/foo2.h, order your includes as follows:

  1. dir2/foo2.h.
  2. A blank line
  3. C system headers (more precisely: headers in angle brackets with the .h extension), e.g., <unistd.h>, <stdlib.h>.
  4. A blank line
  5. C++ standard library headers (without file extension), e.g., <algorithm>, <cstddef>.
  6. A blank line
  7. Other libraries' .h files.
  8. Your project's .h files.

Separate each non-empty group with one blank line.

With the preferred ordering, if the related header dir2/foo2.h omits any necessary includes, the build of dir/foo.cc or dir/foo_test.cc will break. Thus, this rule ensures that build breaks show up first for the people working on these files, not for innocent people in other packages.

dir/foo.cc and dir2/foo2.h are usually in the same directory (e.g., base/basictypes_test.cc and base/basictypes.h), but may sometimes be in different directories too.

Note that the C headers such as stddef.h are essentially interchangeable with their C++ counterparts (cstddef). Either style is acceptable, but prefer consistency with existing code.

Within each section the includes should be ordered alphabetically. Note that older code might not conform to this rule and should be fixed when convenient.

For example, the includes in google-awesome-project/src/foo/internal/fooserver.cc might look like this:

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

Exception:

Sometimes, system-specific code needs conditional includes. Such code can put conditional includes after other includes. Of course, keep your system-specific code small and localized. Example:

#include "foo/public/fooserver.h"

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

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

 

중간 마무리


  • 헤더 파일은 독립적이어야 한다: 코드에서 사용하는 내용이 담긴 헤더 파일을 실제로 include 해야 한다.
  • 헤더 파일은 충돌을 피하기 위해서 반드시 헤더가드를 포함해야 한다.
  • 전방 선언을 사용하지 말라.
  • 함수가 10라인 미만일 때만 인라인 함수를 사용해라.
  • 헤더 파일 include는 순서가 정해져 있다.

C++ 개발을 하며 VSCode, cpplint 이 두 개를 사용하면서 사실 아주 자동적으로 지켜지는 내용들이었다. 특히, 헤더 파일 선을 하는 데 있어서 자유롭게 선언 후 lint를 적용하면 헤더 파일 순서를 정렬해줬기 때문에 이에 대해서는 고민을 해본 적이 없었던 것 같다. 아주 당연스럽게 정렬된 것들만 봐와서 그런 것 같다. 아울러, 전방 선언을 처음 접했던 때가 생각난다. include 하고 있지 않지만 빌드 시에 가져와서 사용한다는 게 참 신기했었는데 이 문법은 그냥 신기하다까지만 생각하고 사용하지 않도록 해야겠다.

 

출처: https://google.github.io/styleguide/cppguide.html 및 내 생각 그리고 영어사전

728x90