Google C++ Style Guide(한글 번역)(번역 진행 중)

9 minute read

Disclaimer: 2024-08-17 기준으로 작성된 Google C++ Style Guide을 참고하였으며, 오역 및 오타가 있을 수 있습니다.

Background

C++는 많은 구글 오픈소스 프로젝트에서 사용되는 주요 개발 언어 중 하나입니다. 모든 C++ 프로그래머가 알다시피, 이 언어는 강력한 기능들을 제공하지만, 이러한 강력함은 복잡성을 동반하며, 이는 코드가 버그에 취약해지고 읽기 어렵고 유지보수가 어려워질 수 있다는 단점을 가져옵니다.

이 가이드의 목표는 C++ 코드 작성 시의 복잡성을 관리하는 것입니다. 이를 위해 해야 할 것과 하지 말아야 할 것들을 상세히 설명합니다. 이러한 규칙들은 코드베이스를 관리하기 쉽게 유지하면서도 개발자들이 C++ 언어의 기능을 생산적으로 활용할 수 있도록 하기 위해 존재합니다.

스타일, 또는 가독성이라고도 불리는 것은 우리 C++ 코드에 적용되는 규칙들을 말합니다. 스타일이라는 용어는 다소 오해의 소지가 있는데, 이 규칙들은 단순히 소스 파일의 포맷팅을 넘어서는 것을 포함합니다.

구글에서 개발한 대부분의 오픈소스 프로젝트는 이 가이드의 요구사항을 따릅니다.

참고로, 이 가이드는 C++ 튜토리얼이 아닙니다. 독자가 이미 C++ 언어에 익숙하다는 전제를 두고 작성되었습니다.

Goals of the Style Guide

스타일 가이드의 목표 이 문서가 왜 필요한가요?

이 가이드는 몇 가지 핵심 목표를 염두에 두고 작성되었습니다. 이러한 목표는 각 규칙의 근본적인 이유를 설명하며, 이를 통해 규칙들이 왜 존재하고 특정 결정들이 왜 내려졌는지 더 넓은 커뮤니티가 이해할 수 있도록 돕고자 합니다. 각 규칙이 어떤 목표를 달성하기 위한 것인지 이해한다면, 규칙이 언제 예외적으로 적용될 수 있는지, 그리고 가이드에서 규칙을 변경하기 위해 어떤 논의나 대안이 필요한지가 더 명확해질 것입니다.

현재 우리가 생각하는 스타일 가이드의 목표는 다음과 같습니다:

스타일 규칙은 그 가치를 해야 한다 모든 엔지니어가 기억해야 하는 스타일 규칙은 그만큼의 큰 이점을 제공해야 합니다. 그 이점은 해당 규칙이 없는 코드베이스와 비교하여 측정됩니다. 매우 해로운 관행을 금지하는 규칙이라도 사람들이 원래 하지 않는 행동이라면 그 이점은 적을 수 있습니다. 이 원칙은 우리가 가지지 않은 규칙들을 주로 설명합니다. 예를 들어, goto는 많은 원칙을 위반하지만 이미 거의 사용되지 않으므로 스타일 가이드에서 다루지 않습니다.

작성자보다 읽는 사람을 위해 최적화하세요 우리의 코드베이스(그리고 제출된 대부분의 개별 컴포넌트)는 오랜 기간 동안 유지될 것으로 예상됩니다. 그 결과, 대부분의 코드 작성 시간보다 읽는 데 더 많은 시간이 소요됩니다. 우리는 코드 작성 시의 용이성보다 평균적인 소프트웨어 엔지니어가 코드베이스를 읽고, 유지보수하고, 디버깅하는 경험을 최적화하는 것을 명시적으로 선택합니다. 이 원칙의 하위 항목으로, “독자를 위한 흔적을 남겨라”라는 말이 자주 사용됩니다. 코드 조각에서 예상치 못한 일이나 특이한 일이 발생할 때(예: 포인터 소유권 이전), 사용 시점에 독자를 위한 텍스트 힌트를 남기는 것이 가치가 있습니다 (std::unique_ptr는 호출 지점에서 소유권 이전을 명확하게 나타냅니다).

기존 코드와 일관성을 유지하세요 우리의 코드베이스에서 일관된 스타일을 사용하는 것은 더 중요한 문제에 집중할 수 있게 합니다. 일관성은 자동화도 가능하게 합니다. 코드 포맷팅 도구나 #include를 조정하는 도구는 코드가 도구의 기대에 맞게 일관성 있게 작성되었을 때만 제대로 작동합니다. “일관성을 유지하라”는 규칙은 주로 “그냥 하나를 선택하고 신경 쓰지 마라”는 의미로 귀결됩니다. 이러한 포인트들에 유연성을 허용하는 잠재적 이점은 사람들 간의 논쟁을 줄이는 데 비해 가치를 가지지 않습니다. 그러나 일관성에도 한계가 있으며, 명확한 기술적 논거나 장기적인 방향이 없는 경우에는 좋은 결론을 제공할 수 있습니다. 일관성은 일반적으로 오래된 스타일을 고수할 이유로 사용되어서는 안 되며, 새로운 스타일의 이점이나 코드베이스가 시간이 지남에 따라 새로운 스타일로 수렴하는 경향을 고려해야 합니다.

적절한 경우 C++ 커뮤니티와의 일관성을 유지하세요 다른 조직들이 C++을 사용하는 방식과의 일관성도 동일한 이유로 가치가 있습니다. C++ 표준의 기능이 문제를 해결하거나, 널리 알려지고 받아들여진 관용구가 있다면 그것을 사용하는 것이 좋습니다. 그러나 표준 기능과 관용구가 결함이 있거나, 우리의 코드베이스의 필요를 고려하지 않고 설계된 경우에는 (아래에서 설명된 대로) 표준 기능을 제한하거나 금지하는 것이 적절합니다. 일부 경우에는 C++ 표준 라이브러리보다 자체 제작 또는 서드파티 라이브러리를 선호하는 경우도 있습니다. 이는 표준 인터페이스로 전환할 가치가 없거나, 자체 라이브러리가 우월하다고 인식되기 때문입니다.

놀랍거나 위험한 구조를 피하세요 C++에는 겉보기보다 더 놀랍거나 위험한 기능들이 있습니다. 스타일 가이드에서 이러한 제한을 두는 이유는 이러한 함정에 빠지는 것을 방지하기 위함입니다. 이러한 제한에 대한 예외를 허용하는 것은 프로그램의 정확성을 직접적으로 위협할 수 있기 때문에 매우 엄격한 기준을 요구합니다.

평균적인 C++ 프로그래머가 복잡하거나 유지보수하기 어려운 구조를 피하세요 C++에는 코드에 복잡성을 더하는 기능들이 존재합니다. 널리 사용되는 코드에서는 이러한 복잡한 언어 구조를 사용할 수 있는 경우도 있지만, 이러한 복잡성을 이해하는 데 드는 비용은 코드베이스의 새로운 부분을 작업할 때 다시 지불할 필요가 없다는 이점이 있습니다. 의심스러운 경우, 프로젝트 리더에게 물어봐서 규칙의 예외를 받을 수 있습니다. 이는 특히 코드 소유권과 팀 구성원이 시간이 지남에 따라 변경되는 경우에 중요합니다. 현재 해당 코드를 작업하는 모든 사람이 이해하더라도, 몇 년 후에는 그 이해가 유지될 것이 보장되지 않기 때문입니다.

우리의 규모를 고려하세요 100억 줄 이상의 코드베이스와 수천 명의 엔지니어를 가진 상황에서는 한 엔지니어의 실수나 단순화가 많은 사람들에게 큰 비용을 초래할 수 있습니다. 예를 들어, 글로벌 네임스페이스 오염을 피하는 것이 특히 중요합니다. 수억 줄의 코드베이스에서 이름 충돌이 발생하면 작업하기 어려우며, 모든 사람이 글로벌 네임스페이스에 무언가를 넣으면 이를 피하기가 어려워집니다.

필요한 경우 최적화에 양보하세요 성능 최적화는 이 문서의 다른 원칙들과 충돌할 때조차도 때때로 필요하고 적절할 수 있습니다.

이 문서의 의도는 합리적인 제한 내에서 최대한의 지침을 제공하는 것입니다. 항상 상식과 좋은 판단이 우선되어야 합니다. 여기서 우리는 개인적 선호도나 팀의 선호도가 아니라, 구글 C++ 커뮤니티 전체의 확립된 관습을 의미합니다. 독창적이거나 특이한 구조를 사용하는 것에 대해 회의적이고 주저해야 합니다. 금지되지 않았다는 것이 곧 사용 허가를 의미하는 것은 아닙니다. 판단을 사용하고, 확신이 서지 않으면 프로젝트 리더에게 추가 입력을 요청하는 것을 주저하지 마세요.

C++ Version

C++ 버전 현재, 코드는 C++20을 목표로 해야 하며, C++23 기능을 사용해서는 안 됩니다. 이 가이드에서 목표로 하는 C++ 버전은 시간이 지나면서 점진적으로 발전할 것입니다.

비표준 확장은 사용하지 마세요.

C++17 및 C++20의 기능을 프로젝트에서 사용할 때는 다른 환경으로의 이식성을 고려하세요.

Header Files

헤더 파일 일반적으로, 각 .cc 파일에는 관련된 .h 파일이 있어야 합니다. 단위 테스트와 같이 몇 가지 일반적인 예외가 있으며, main() 함수만 포함된 작은 .cc 파일도 예외입니다.

헤더 파일의 올바른 사용은 코드의 가독성, 크기 및 성능에 큰 차이를 가져올 수 있습니다.

다음 규칙들은 헤더 파일을 사용할 때 발생할 수 있는 다양한 함정들을 안내합니다.

독립적인 헤더 파일

헤더 파일은 독립적으로 컴파일될 수 있어야 하며 .h로 끝나야 합니다. 포함을 목적으로 하는 비헤더 파일은 .inc로 끝나야 하며, 신중하게 사용해야 합니다.

모든 헤더 파일은 독립적이어야 합니다. 사용자나 리팩토링 도구가 헤더를 포함할 때 특별한 조건을 따를 필요가 없어야 합니다. 특히, 헤더는 헤더 가드를 가지고 있어야 하며, 필요한 모든 다른 헤더를 포함해야 합니다.

헤더가 인라인 함수나 템플릿을 선언하고 그 헤더의 클라이언트가 이를 인스턴스화할 경우, 해당 인라인 함수와 템플릿의 정의도 헤더에 포함되어야 합니다. 이러한 정의를 별도의 헤더(-inl.h) 파일로 옮기지 마세요. 과거에는 이러한 방식이 일반적이었지만 이제는 허용되지 않습니다. 템플릿의 모든 인스턴스화가 한 .cc 파일에서 발생하는 경우, 해당 템플릿 정의는 그 파일에 유지될 수 있습니다.

포함을 목적으로 설계된 파일이 독립적이지 않은 경우는 드뭅니다. 이러한 파일들은 보통 다른 파일의 중간에 포함되도록 설계되며, 헤더 가드를 사용하지 않거나 필요한 전제 조건을 포함하지 않을 수 있습니다. 이러한 파일들은 .inc 확장자로 명명해야 합니다. 가능하면 독립적인 헤더를 선호하며 신중하게 사용하세요.

#define 가드

모든 헤더 파일은 다중 포함을 방지하기 위해 #define 가드를 가져야 합니다. 심볼 이름의 형식은 ___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 inclusions)에 의존하지 마세요. 이를 통해 불필요해진 #include 문을 제거해도 클라이언트가 깨지지 않게 할 수 있습니다. 이는 관련 헤더에도 적용됩니다. 예를 들어 foo.cc가 bar.h에서 심볼을 사용하면, foo.h가 bar.h를 포함하더라도 foo.cc는 bar.h를 포함해야 합니다.

전방 선언(Forward Declarations)

가능한 경우 전방 선언을 피하고 필요한 헤더를 포함하세요.

정의:

“전방 선언”이란 연관된 정의 없이 엔티티를 선언하는 것을 말합니다.

// C++ 소스 파일에서:
class B;
void FuncInB();
extern int variable_in_b;
ABSL_DECLARE_FLAG(flag_in_b);

장점:

  • 전방 선언은 컴파일 시간을 절약할 수 있습니다. #include 문은 컴파일러가 더 많은 파일을 열고 더 많은 입력을 처리하도록 강제하기 때문입니다.
  • 전방 선언은 불필요한 재컴파일을 줄일 수 있습니다. #include 문은 헤더에서 관련 없는 변경이 있을 때에도 코드를 더 자주 재컴파일하도록 강제할 수 있습니다.

단점:

  • 전방 선언은 종속성을 숨겨, 헤더가 변경될 때 필요한 재컴파일을 생략할 수 있게 만듭니다.
  • 전방 선언은 자동화 도구가 심볼을 정의하는 모듈을 발견하는 것을 어렵게 만듭니다.
  • 전방 선언은 라이브러리의 이후 변경으로 인해 깨질 수 있습니다. 함수나 템플릿의 전방 선언은 헤더 소유자가 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); }  // B*를 인자로 받는 f 함수가 호출됨
  • 만약 #include 문이 B와 D에 대한 전방 선언으로 대체되었다면, test()는 f(void*)를 호출하게 됩니다.
  • 헤더에서 여러 심볼을 전방 선언하는 것은 단순히 헤더를 #include하는 것보다 더 번거로울 수 있습니다.
  • 전방 선언을 가능하게 하도록 코드 구조를 변경하는 것(예: 객체 멤버 대신 포인터 멤버 사용)은 코드를 더 느리게 하고 복잡하게 만들 수 있습니다.

결정: 다른 프로젝트에서 정의된 엔티티의 전방 선언은 가능한 피하세요.

인라인 함수

함수를 인라인으로 정의하는 것은 작고, 예를 들어 10줄 이하일 때만 하세요.

정의: 인라인 함수란 컴파일러가 함수 호출을 통해 함수가 아닌 인라인으로 확장할 수 있도록 선언된 함수입니다.

장점: 인라인 함수는 작을 때 더 효율적인 객체 코드를 생성할 수 있습니다. 접근자와 변경자(accessors and mutators), 그리고 성능에 중요한 짧은 함수들을 인라인으로 정의하는 것은 좋습니다.

단점: 인라인 함수의 과도한 사용은 실제로 프로그램을 느리게 만들 수 있습니다. 함수의 크기에 따라 인라인화는 코드 크기를 증가시키거나 감소시킬 수 있습니다. 매우 작은 접근자 함수를 인라인화하면 보통 코드 크기가 줄어들지만, 매우 큰 함수를 인라인화하면 코드 크기가 급격히 증가할 수 있습니다. 현대의 프로세서에서는 더 작은 코드가 더 빠르게 실행되는데, 이는 명령어 캐시를 더 잘 활용하기 때문입니다.

결정: 일반적인 규칙으로, 함수가 10줄 이상인 경우에는 인라인으로 정의하지 마세요. 특히 소멸자는 암묵적인 멤버 및 기본 소멸자 호출로 인해 길어질 수 있으므로 주의하세요!

또한, 루프나 switch 문이 있는 함수를 인라인화하는 것은 일반적으로 비용 효율적이지 않습니다(단, 일반적인 경우에 루프나 switch 문이 전혀 실행되지 않는 경우는 예외).

중요한 점은, 함수가 인라인으로 선언되었다고 해서 항상 인라인화되는 것은 아니라는 것입니다. 예를 들어, 가상 함수와 재귀 함수는 일반적으로 인라인화되지 않습니다. 재귀 함수는 일반적으로 인라인화해서는 안 됩니다. 가상 함수를 인라인으로 만드는 주된 이유는 정의를 클래스 안에 배치하거나, 접근자와 변경자에 대한 동작을 문서화하기 위해서입니다.

Includes의 이름 및 순서

헤더 파일은 다음 순서로 포함하세요: 관련 헤더, C 시스템 헤더, C++ 표준 라이브러리 헤더, 기타 라이브러리 헤더, 프로젝트의 헤더.

모든 프로젝트의 헤더 파일은 UNIX 디렉토리 별칭 . (현재 디렉토리) 또는 .. (상위 디렉토리) 없이 프로젝트 소스 디렉토리의 하위 항목으로 나열되어야 합니다. 예를 들어, google-awesome-project/src/base/logging.h는 다음과 같이 포함해야 합니다:

# include "base/logging.h"

헤더는 해당 라이브러리가 요구하는 경우에만 각괄호 경로를 사용하여 포함해야 합니다. 특히, 다음 헤더는 각괄호를 사용해야 합니다:

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

dir/foo.cc 또는 dir/foo_test.cc 파일에서 dir2/foo2.h의 구현이나 테스트를 목적으로 할 경우, 포함 순서는 다음과 같습니다:

  1. dir2/foo2.h
  2. 빈 줄 하나
  3. C 시스템 헤더 및 각괄호와 함께 .h 확장자가 있는 기타 헤더(예: , , )
  4. 빈 줄 하나
  5. C++ 표준 라이브러리 헤더(파일 확장자 없이, 예: , )
  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), 경우에 따라 다른 디렉토리에 있을 수도 있습니다.

C 헤더(예: stddef.h)는 본질적으로 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"  // For LANG_CXX11.

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

Reference

Google C++ Style Guide

Leave a comment