Google C++ Style Guide

리비전 3.188

Benjy Weinberger
Craig Silverstein
Gregory Eitzmann
Mark Mentovai
Tashana Landray

원문: http://google-styleguide.googlecode.com/svn/trunk/cppguide.xml

배경

C++는 구글에서 진행하는 많은 오픈 소스 프로젝트에서 사용하는 주요 개발 언어이다. 모든 C++ 개발자가 알다시피 이 언어에는 많은 강력한 기능이 있으나 또한 복잡해 버그투성이에 읽기도 어렵고 유지보수 하기도 힘든 코드를 만들게 될 수 있다.

이 안내문은 C++ 코드를 만들 때 해야 하는 것과 하지 말아야 하는 것을 상세히 설명해 이런 복잡성을 관리하는 것이 목표다. 이 규칙은 C++ 언어 기능을 생산적으로 사용하면서 코드 기반을 관리하기 쉽게 유지하기 위한 것이다.

가독성이라고도 하는 스타일은 C++ 코드를 지배하는 관례를 말한다. 스타일이라는 용어는 다소 잘못된 것인데 이런 관례는 단지 소스 파일 형식 정도가 아니라 그 이상을 다루기 때문이다.

코드 기반을 관리하기 쉽게 유지하는 한 가지 방법은 일관성을 강제하는 것이다. 어떤 프로그래머라도 다른 이가 만든 코드를 살펴볼 수 있고 재빨리 이해할 수 있다는 것은 매우 중요하다. 통일된 스타일을 유지하고 관례를 따른다는 것은 어떤 다양한 심벌이 있고 그 심벌에 대해 어떤 불변식(invariants)이 참인지 추론하는데 ‘패턴 일치(pattern-matching)’를 더 쉽게 사용할 수 있다는 뜻이다. 공통적이고 정해둔 관용어와 패턴을 만들면 코드를 더욱 쉽게 이해할 수 있다. 때로는 스타일 규칙을 바꿀 좋은 논거가 있을 수도 있지만 그럼에도 불구하고 일관성을 유지하기 위해 규칙 그대로를 유지한다.

이 안내문에서 얘기하는 다른 문제는 C++ 기능이 지나치게 많다는 거다. C++는 많은 고급 기능이 있는 커다란 언어이다. 그러므로 때로는 어떤 기능을 제한하고 심지어 금지하기도 한다. 이는 코드를 단순하게 유지하고 그런 기능 때문에 생길 수 있는 공통적인 다양한 오류와 문제를 피하기 위해서이다. 이 안내문에서는 그런 기능을 나열하고 왜 사용을 제한해야 하는지를 설명한다.

구글에서 개발 중인 오픈 소스 프로젝트에서는 이 안내문을 지켜야 한다.

이 안내문은 C++ 입문서가 아니라는 것에 주의하고, 독자는 이 언어에 익숙하다고 가정한다.

헤더 파일(Header Files)

일반적으로 모든 .cc 파일에는 연관된 .h 파일이 있어야 한다. 단위 테스트와 main() 함수만 있는 작은 .cc 파일인 경우처럼 일부 예외가 있을 수 있다.

헤더 파일을 올바로 사용하면 코드 가독성, 크기, 성능에 큰 차이가 생길 수 있다.

다음 규칙에서는 헤더 파일을 사용하며 할 수 있는 다양한 실수를 통해 안내한다.

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

헤더 파일 의존성(Header File Dependencies)

전방 선언으로 충분하면 #include를 사용하지 않아야 한다.

헤더 파일을 포함하면 의존성을 가지므로 그 헤더 파일이 바뀔 때마다 코드도 새로 컴파일 해야 한다. 자신이 만든 헤더 파일에서 다른 여러 헤더 파일을 포함하면 그 파일들이 바뀔 때마다 자신이 만든 헤더 파일을 포함한 모든 코드를 다시 컴파일 해야 한다. 그러므로 포함을 최소화 하기를 권하며, 헤더 파일에서 다른 헤더 파일을 포함하는 것은 더욱 그렇게 하지 않기를 권한다.

전방 선언을 사용해 포함해야 하는 헤더 파일 수를 상당히 줄일 수 있다. 예를 들어 헤더 파일에서 File 클래스를 사용하지만 이 클래스 선언에 접근하지 않아도 된다면, 헤더 파일에서는 #include “file/base/file.h” 대신 그저 class File; 로 전방 선언을 할 수 있다.

헤더 파일에서 클래스 Foo 정의에 접근하지 않고 어떻게 이 클래스를 사용할 수 있을까?

  • Foo* 또는 Foo& 타입 데이터 멤버를 선언한다.
  • 인자 또는 반환 값이 Foo 타입인 함수를 (정의가 아니라) 선언한다(한 가지 예외가 있는데, 인자 Foo 또는 const Foo&가 explicit가 아니고 인자가 하나인 생성자라면 자동 형 변환을 지원하기 위해 전체 정의가 있어야 한다).
  • Foo 타입 정적 데이터 멤버를 선언한다. 정적 데이터 멤버는 클래스 정의 밖에서 정의하기 때문이다.

바꿔 말해 자신의 클래스를 Foo에서 파생하거나, 클래스에 Foo 타입 데이터 멤버가 있다면 Foo에 대한 헤더 파일을 포함해야 한다.

때로는 객체 멤버 대신 포인터 (또는 더 나은 scoped_ptr) 멤버를 사용하는 게 맞을 수 있다. 하지만 이 때문에 코드 가독성이 나빠지고 성능이 떨어질 수 있으므로, 목적이 헤더 파일 포함을 최소화 하는 것뿐이라면 이런 식으로 변경하지 않도록 한다.

물론 전형적으로 .cc 파일에서는 사용하는 클래스 정의를 필요로 하고 일반적으로 여러 헤더 파일을 포함해야 한다.

참고: 소스 파일에서 Foo 심벌을 사용하면 #include나 전방 선언으로 Foo에 대한 정의를 직접 포함해야 한다. 해당 심벌을 직접 포함하지 않고 헤더 파일을 통해 간접적으로 가져와 의존해서는 안 된다. 한 가지 예외는, myfile.cc에서 Foo를 사용할 때 myfile.cc 대신 myfile.h에서 Foo를 포함(하거나 전방 선언)하는 것은 괜찮다.

인라인 함수(Inline Functions)

함수 내용이 10 줄 이하일 때만 인라인으로 정의한다.

설명:

일반적인 함수 호출 구조를 통해 호출하기보다 컴파일러에서 인라인으로 확장하도록 함수를 선언할 수 있다.

장점:

작은 함수를 인라인으로 만들면 더 효율적인 목적 코드(object code)를 만들 수 있다. 접근자(accessor)와 변경자(mutator), 기타 짧은 함수와 성능이 중요한 함수는 자유롭게 인라인으로 만든다.

단점:

인라인을 과도하게 사용하면 실제로는 프로그램 성능이 떨어질 수 있다. 인라인을 하면 함수 크기에 따라 코드 크기가 커지거나 작아질 수 있다. 매우 작은 접근자 함수를 인라인으로 만들면 보통 코드 크기가 작아지지만 함수가 매우 클 때는 극적으로 커진다. 최근 프로세서에서는 일반적으로 코드 크기가 작을수록 명령 캐시를 더 잘 사용하므로 더 빨리 실행된다.

결론:

가장 적절한 규칙은 10 줄을 넘어가는 함수는 인라인으로 만들지 않는 거다. 소멸자에 대해 알아둬야 할 것은, 암시적인 멤버나 기초 클래스의 소멸자를 호출하기 때문에 종종 실제 보이는 것보다 더 길다는 점이다.

다른 유용한 규칙은 (일반적인 경우에 루프나 switch 문을 전혀 실행하지 않는 것이 아니라면) 루프나 switch 문이 있는 함수를 인라인으로 하면 그리 효과적이지 않다는 것이다.

함수를 인라인으로 선언하더라도 항상 인라인이 되는 건 아니라는 것을 기억해야 한다. 예를 들어, 가상 또는 재귀 함수는 일반적으로 인라인이 되지 않는다. 일반적으로 재귀 함수는 인라인으로 만들지 않아야 한다. 가상 함수를 인라인으로 만드는 주된 이유는 해당 정의를 클래스에 둬 편의성을 높이거나 접근자와 변경자 등과 같은 행위를 문서화 하기 위해서이다.

-inl.h 파일(The -inl.h Files)

필요에 따라 파일 이름에 –inl.h를 접미어로 붙여 복잡한 인라인 함수를 정의하는데 사용할 수 있다.

인라인 함수 정의는 헤더 파일에 있어야 호출하는 곳에서 인라인 하는데 필요한 정의를 컴파일러에서 접근할 수 있다. 하지만 구현 코드는 .cc 파일에 있는 것이 적절하며, 가독성이나 성능에 이점이 없다면 실제 코드를 .h 파일에 많이 두는 것은 그리 좋지 않다.

인라인 함수 정의가 매우 짧으면 .h 파일에 그 코드를 두는 게 좋다. 즉 접근자와 변경자는 반드시 클래스 정의에 둔다. 더 복잡한 인라인 함수 역시 구현과 호출 편의성 때문에 .h 파일에 둘 수도 있지만 이 때문에 .h 파일이 너무 커진다면 그 코드를 별도의 –inl.h 파일에 둘 수도 있다. 이를 통해 구현과 클래스 정의를 나누고 구현 내용을 필요한 곳에서 포함할 수 있다.

-inl.h 파일은 함수 템플릿을 정의하는 데에도 사용해 템플릿 정의를 쉽게 읽을 수 있도록 할 수 있다.

-inl.h 파일에도 다른 헤더 파일처럼 #define 보호문이 있어야 한다.

함수 매개변수 순서(Function Parameter Ordering)

함수를 정의할 때 매개변수 순서는 입력, 출력 순이다.

C/C++ 함수에서 매개변수는 해당 함수에 대한 입력, 출력, 또는 둘 모두이다. 일반적으로 입력 매개변수는 값 또는 const 참조자이고 출력과 입/출력 매개변수는 const가 아닌 포인터이다. 함수 매개변수 순서를 정할 때는 입력 전용 매개변수 모두를 출력 매개변수 앞에 둔다. 특히 새 매개변수는 새로 추가한다는 이유만으로 함수 끝에 넣지 않으며, 새로 추가하는 입력 전용 매개변수는 출력 매개변수 앞에 둔다.

이는 엄격한 규칙은 아니다. 입력과 출력을 모두 하는 매개변수나 관련된 함수와의 일관성 때문에 이 규칙을 어겨야 할 수도 있다[1].

이름과 포함 순서(Names and Order of Includes)

가독성을 높이고 숨은 의존성을 피하기 위해 다음과 같은 표준 순서를 사용한다.

C 라이브러리, C++ 라이브러리, 그 외 라이브러리의 .h, 프로젝트에 속한 .h

프로젝트에 속한 헤더 파일은 UNIX 디렉터리 바로 가기인 .(현재 디렉터리) 또는 ..(부모 디렉터리)를 사용하지 않고 해당 프로젝트의 소스 디렉터리 바로 아래부터 표시한다. 예를 들어 google-awesome-project/src/base/logging.h 파일은 다음처럼 포함해야 한다.

#include "base/logging.h"

dir/foo.cc 또는 dir/foo_test.cc 파일의 주목적이 dir2/foo2.h 내용을 구현하거나 테스트하는 것일 때, 이 파일에서 포함 순서는 다음과 같다.

  1. dir2/foo2.h (우선 위치. 더 자세한 내용은 아래를 본다)
  2. C 시스템 파일
  3. C++ 시스템 파일
  4. 그 외 라이브러리의 .h 파일
  5. 프로젝트에 속한 .h 파일

우선 순위를 사용하면 숨겨진 의존성이 줄어든다. 모든 헤더 파일은 그 자체로 컴파일 할 수 있어야 하는데, 가장 쉬운 방법은 각 .h 파일을 .cc 파일에서 첫 번째로 포함한 파일이 되도록 하면 된다.

dir/foo.cc와 dir2/foo2.h는 (base/basictypes_test.cc와 base/basictypes.h처럼) 같은 디렉터리에 있을 수도 있고 아닐 수도 있다.

각 부분별로는 알파벳 순서로 포함하는 것이 좋다.

예를 들어 google-awesome-project/src/foo/internal/fooserver.cc에서 포함 순서는 다음과 같을 수 있다.

#include "foo/public/fooserver.h"  // Preferred location.

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

#include <hash_map>
#include <vector>

#include "base/basictypes.h"
#include "base/commandlineflags.h"
#include "foo/public/bar.h"

범위(Scoping)

네임스페이스(Namespaces)

.cc 파일에 이름 없는 네임스페이스를 사용하길 권한다. 이름 있는 네임스페이스를 사용할 때는 프로젝트 이름에 기초해 선택하고 경로를 고려할 수도 있다. using 지시자는 사용하지 않는다.

설명:

네임스페이스는 전역 범위를 별개의 이름 공간으로 나누므로 전역 범위에서 이름 충돌이 생기지 않도록 하는데 유용하다.

장점:

네임스페이스는 클래스에서 제공하는 (계층적인) 이름 축에 추가로 (마찬가지로 계층적인) 이름 축을 지원한다.

예를 들어, 서로 다른 두 프로젝트에 전역 범위로 Foo 클래스가 있으면 이 심벌들은 컴파일 또는 실행 중에 충돌할 수 있다. 각 프로젝트에서 project1::Foo와 project2::Foo처럼 네임스페이스에 코드를 두면 각 심벌은 충돌하지 않는다.

단점:

네임스페이스를 사용하면 혼란스러울 수 있다. 클래스에서 제공하는 (계층적인) 이름 축에 추가로 (마찬가지로 계층적인) 이름 축을 제공하기 때문이다.

헤더 파일에서 이름 없는 네임스페이스를 사용하면 C++ 단일 정의 법칙(ODR)을 쉽게 위반할 수 있다.

결론:

다음에서 설명한 정책에 따라 네임스페이스를 사용한다.

이름 없는 네임스페이스

  • 이름 없는 네임스페이스는 실행 중 이름 충돌을 피하기 위해 .cc 파일에서만 사용한다.
namespace {                           // This is in a .cc file.

// The content of a namespace is not indented
enum { kUnused, kEOF, kError };       // Commonly used tokens.
bool AtEof() { return pos_ == kEOF; }  // Uses our namespace's EOF.

}  // namespace

하지만 특정 클래스와 연관된 파일 범위 선언은 이름 없는 네임스페이스의 멤버가 아닌 타입, 정적 데이터 멤버 또는 정적 멤버 함수로 그 클래스 안에 선언할 수도 있다. 이름 없는 네임스페이스는 위에서처럼 // namespace 주석문으로 마친다.

  • 이름 없는 네임스페이스는 .h 파일에서 사용하지 않는다.

이름 있는 네임스페이스

이름 있는 네임스페이스는 다음처럼 사용한다.

  • 포함문, gflags 정의와 선언, 다른 네임스페이스에 있는 클래스에 대한 전방 선언 이후 모든 소스 내용을 네임스페이스로 감싼다.
// In the .h file
namespace mynamespace {

// All declarations are within the namespace scope.
// Notice the lack of indentation.
class MyClass {
 public:
  ...
  void Foo();
};

}  // namespace mynamespace
// In the .cc file
namespace mynamespace {

// Definition of functions is within scope of the namespace.
void MyClass::Foo() {
  ...
}

}  // namespace mynamespace

전형적인 .cc 파일은 다른 네임스페이스에 있는 클래스를 참조하는데 필요한 것을 포함하므로 상세 내용은 더 복잡할 수 있다.

#include "a.h"

DEFINE_bool(someflag, false, "dummy flag");

class C;  // Forward declaration of class C in the global namespace.
namespace a { class A; }  // Forward declaration of a::A.

namespace b {

...code for b...         // Code goes against the left margin.

}  // namespace b
  • std 네임스페이스에는 아무것도 선언하지 않으며, 심지어 표준 라이브러리 클래스에 대한 전방 선언일지라도 마찬가지다. std 네임스페이스에 요소를 선언하는 것은 동작을 보장할 수 없다. 즉 이식성이 없다. 표준 라이브러리에 있는 요소를 선언하려면 적절한 헤더 파일을 포함한다.
  • 네임스페이스에서 모든 이름을 사용하기 위해 using 지시자를 사용하지 않아야 한다.
// Forbidden -- This pollutes the namespace.
using namespace foo;
  • using 선언은 .cc 파일, .h 파일에 있는 함수, 메소드 또는 클래스 등 어디서든 사용해도 좋다.
// OK in .cc files.
// Must be in a function, method or class in .h files.
using ::foo::bar;
  • 네임스페이스 별명은 .cc 파일 내 어디든, 전체 .h 파일을 감싼 이름 있는 네임스페이스 내 어디든, 그리고 함수와 메소드에서 사용할 수 있다.
// Shorten access to some commonly used names in .cc files.
namespace fbz = ::foo::bar::baz;

// Shorten access to some commonly used names (in a .h file).
namespace librarian {
// The following alias is available to all files including
// this header (in namespace librarian):
// alias names should therefore be chosen consistently
// within a project.
namespace pd_s = ::pipeline_diagnostics::sidetable;

inline void my_inline_function() {
  // namespace alias local to a function (or method).
  namespace fbz = ::foo::bar::baz;
  ...
}
}  // namespace librarian

.h 파일에 있는 별명은 (프로젝트 외부에서 사용할 수 있는) 공용 헤더와 그 파일을 간접적으로 포함한 헤더 등 그 파일을 포함하는 모든 곳에서 볼 수 있으며, 일반적으로 공용 API는 가능한 작게 유지한다는 점에서 별명은 피하는 게 좋다.

중첩 클래스(Nested Classes)

인터페이스의 일부분으로 중첩 클래스를 사용할 수 있더라도 전역 범위 밖에서 선언을 유지하기 위해 네임스페이스를 고려한다.

설명:

클래스에서는 그 안에 다른 클래스를 정의할 수 있으며 이를 멤버 클래스라 한다.

class Foo {

 private:
  // Bar is a member class, nested within Foo.
  class Bar {
    ...
  };

};

장점:

중첩 (또는 멤버) 클래스는 그 것을 포함하고 있는 클래스에서만 사용할 수 있다는 점에서 유용하다. 즉 중첩 클래스 이름을 바깥에 드러내지 않으면서 그것을 포함하는 클래스 범위 안에 그 멤버를 둘 수 있다. 중첩 클래스 정의를 포함 클래스(enclosing class) 선언 안에 두지 않기 위해 중첩 클래스를 포함 클래스 안에서 전방 선언한 후 .cc 파일에서 정의할 수 있다. 흔히 중첩 클래스 정의는 구현 내용에만 의미가 있기 때문이다.

단점:

중첩 클래스는 포함 클래스 정의 안에서만 전방 선언할 수 있다. 그러므로 Foo::Bar* 포인터를 다루는 모든 헤더 파일에서는 Foo 클래스 선언 전체를 포함해야 한다.

결론:

일부 메소드에 대한 선택적인 집합을 유지하는 클래스처럼 실제로 인터페이스 일부분이 아니라면 중첩 클래스를 공용으로 하지 않는다.

비멤버, 정적 멤버와 전역 함수(NonMember, Static Member, and Global Functions)

전역 함수보다는 네임스페이스 내 비멤버 함수나 정적 멤버 함수를 우선으로 한다. 전역 함수는 사용하지 않는다.

장점:

비멤버와 정적 멤버 함수는 일부 상황에서 유용할 수 있다. 네임스페이스에 비멤버 함수를 두면 전역 네임스페이스 오염을 피할 수 있다.

단점:

비멤버와 정적 멤버 함수는 새 클래스 멤버로 하는 게 더 합리적일 수 있다. 특히 이들 함수에서 외부 자원에 접근하거나 이들에 중요한 의존성이 있을 때는 더욱 그러하다.

결론:

때때로 클래스 인스턴스와 결합하지 않은 함수를 정의하는 것은 유용하거나 심지어 필요하기까지 하며, 그런 함수는 정적 멤버나 비멤버 함수 중 하나이다. 비멤버 함수는 외부 변수에 의존하지 않아야 하고 거의 항상 네임스페이스 안에 존재해야 한다. 정적 데이터를 공유하지 않는 정적 멤버 함수를 단지 그룹으로 묶기 위해서는 클래스를 만들기보다 네임스페이스를 사용한다.

컴파일 단위가 제품 클래스인 그런 클래스에서 정의한 함수를 다른 컴파일 단위에서 호출할 때 불필요한 결합과 링크 중 의존성이 생길 수 있다. 특히 정적 멤버 함수는 더욱 그렇게 되기 쉽다. 새 클래스로 빼 내거나 그런 함수를 가능하면 별도 라이브러리 내 네임스페이스로 옮긴다.

비멤버 함수를 정의해야 하고 .cc 파일에만 둬도 된다면 이름 없는 네임스페이스나 static 링크(즉 static int Foo() {…})로 범위를 제한한다.

지역 변수(Local Variables)

함수에서 변수는 가능한 가장 좁은 범위로 두고 선언할 때 초기화한다.

C++에서는 함수 어디서나 변수를 선언할 수 있다. 변수는 가능한 지역 범위로, 처음 사용하는 곳에 가까이 선언하길 권한다. 이렇게 하면 그 변수를 선언한 곳을 더 쉽게 찾고 어떤 타입인지, 무엇으로 초기화 했는지 쉽게 알 수 있다. 특히 선언 후 대입 대신 초기화를 사용한다.

int i;
i = f();      // Bad -- initialization separate from declaration.
int j = g();  // Good -- declaration has initialization.

gcc에서는 for (int i = 0; i < 10; ++i)를 올바르게 구현하므로 같은 범위에 있는 다른 for 루프에서 i를 다시 사용할 수 있다는 점에 주의한다. 또한 if와 while 문에서도 범위 선언은 마찬가지다.

while (const char* p = strchr(str, '/')) str = p + 1;

한 가지 단서가 있다. 만약 그 변수가 객체이면 범위에 들어갈 때마다 생성자를 생성해 호출하고 범위를 벗어날 때마다 소멸자를 호출한다.

// Inefficient implementation:
for (int i = 0; i < 1000000; ++i) {
  Foo f;  // My ctor and dtor get called 1000000 times each.
  f.DoSomething(i);
}
&#91;/sourcecode&#93;

루프 안에서 사용하는 변수는 해당 루프 밖에서 선언하는 것이 더 효율적이다.

&#91;sourcecode language="cpp"&#93;
Foo f;  // My ctor and dtor get called once each.
for (int i = 0; i < 1000000; ++i) {
  f.DoSomething(i);
}
&#91;/sourcecode&#93;
<h3></h3>
<h3><strong>정적과 전역 변수(Static and Global Variables)</strong></h3>
클래스 타입인 정적 또는 전역 변수는 금지한다. 생성자와 소멸자의 순서가 정해져 있지 않아 찾기 힘든 버그를 만들 수 있기 때문이다.

전역 변수, 정적 변수, 정적 클래스 멤버 변수 그리고 함수 정적 변수를 포함해 저장 기간이 정적(static)인 객체는 POD(Plain Old Type) 타입이어야 한다. 즉 int, char, float 또는 포인터나 배열, POD 구조체<a id="ref2" href="#2"><sup>[2]</sup></a>여야 한다.

C++에서 클래스 생성자와 정적 변수 초기화식을 호출하는 순서는 부분적으로만 정해져 있을 뿐이며 심지어 빌드 때마다 바뀔 수도 있어 찾기 힘든 버그를 만들 수 있다. 그러므로 클래스 타입 전역 변수 금지와 더불어 getenv()나 getpid()처럼 호출한 함수가 다른 전역 변수에 의존하는 것을 제외하고 정적 POD 변수를 함수 호출 결과로 초기화하는 것도 금지한다.

소멸자 호출 순서는 생성자 호출 순서와 반대로 정의하지만 생성자 순서가 정해져 있지 않으므로 소멸자 역시 마찬가지다. 예를 들어 프로그램을 종료할 때 정적 변수는 소멸한 상태일 수 있지만 코드는 여전히 다른 스레드에서 실행 중이어서 그 변수에 접근하다 실패할 수도 있다. 또는 정적 ‘string’ 배열의 소멸자가 그 string을 참조하는 다른 변수의 소멸자보다 먼저 실행할 수도 있다.

이 때문에 정적 변수에는 POD 데이터만 허용한다. 이 규칙에서는 vector나 string을 전혀 사용할 수 없으며 대신 C 배열이나 const char[]을 사용한다.

클래스 타입 정적 또는 전역 변수을 사용해야 한다면 main() 함수나 pthread_once()에서 절대 해제하지 않을 포인터로 초기화하는 것을 고려해 본다. 단, 이 포인터는 ‘스마트’ 포인터가 아니라 기본형(raw) 포인터여야 한다는 점에 주의한다. 스마트 포인터의 소멸자는 앞에서 설명한 소멸자 순서 문제가 생길 수 있기 때문이다.
<h2></h2>
<h2><strong>클래스(Classes)</strong></h2>
클래스는 C++에서 기본적인 코드 단위이며 당연히 엄청나게 사용한다. 여기서는 클래스를 만들 때 따라야 할 것과 그러지 않아야 할 것을 설명한다.
<h3></h3>
<h3><strong>생성자에서 하는 일(Doing Work in Constructors)</strong></h3>
일반적으로 생성자에서는 멤버 변수를 초기화하는 일만 해야 한다. 모든 복잡한 초기화는 명시적으로 Init() 메소드를 만들어 한다.

<strong>설명:</strong>

생성자 본체에서 초기화를 할 수 있다.

<strong>장점:</strong>

만드는데 편하다. 클래스를 초기화했는지 안 했는지 걱정할 필요가 없다.

<strong>단점:</strong>

생성자에서 무언가 할 때 생기는 문제는 다음과 같다.
<ul>
	<li>생성자에서는 오류를 알리기가 쉽지 않다. 예외 사용은 논외이나 이 역시 금지이다.</li>
	<li>무언가 실패하면 초기화를 마치지 않은 객체가 생기므로 그 상태를 알 수 없다.</li>
	<li>가상 함수를 호출하면 하위 클래스 구현을 불러 오지 못한다. 현재는 파생시키지 않았더라도 나중에 클래스를 고쳤을 때 모르는 사이 이런 문제가 생겨 매우 혼란스러울 수 있다.</li>
	<li>(규칙에 위배되지만) 클래스 타입 전역 변수를 누군가 만들면 그 클래스 생성자는 main() 이전에 호출될 것이고 생성자 코드에서 암시적으로 가정하고 있는 무언가를 어길 수 있다. 예를 들어 <a href="http://code.google.com/p/google-gflags/">gflags</a>는 아직 초기화되지 않았을 것이다.</li>
</ul>
<strong>결론:</strong>

객체에서 뭔가 중요한 초기화를 해야 한다면 Init() 메소드를 명시적으로 만들 것을 고려한다. 특히 생성자에서는 가상 함수를 호출하거나 오류를 발생하거나 초기화하지 않았을 가능성이 있는 전역 변수에 접근하는 등의 일은 하지 말아야 한다.
<h3></h3>
<h3><strong>기본 생성자(Default Constructors)</strong></h3>
클래스에 멤버 변수를 정의했으나 다른 생성자가 없다면 기본 생성자를 정의해야 한다. 그렇지 않으면 컴파일러에서 알아서 처리하나 그리 좋지 않다.

<strong>설명:</strong>

새 클래스 객체를 인자 없이 만들면 기본 생성자를 호출한다. 배열에 대해 new[]를 호출할 때 항상 호출한다.

<strong>장점:</strong>

기본적으로 ‘불가능한’ 값으로 초기화해 디버깅을 훨씬 쉽게 한다.

<strong>단점:</strong>

코드를 만들어야 한다.

<strong>결론:</strong>

클래스에 멤버 변수를 정의했으나 다른 생성자가 없으면 (인자를 취하지 않는) 기본 생성자를 정의해야 하며, 기본 생성자에서는 되도록 객체 상태를 일관되고 유효하게 초기화하는 게 좋다.

다른 생성자가 없을 때 기본 생성자를 정의하지 않으면 컴파일러에서 만든다. 컴파일러에서 만든 생성자는 객체를 적절히 초기화하지 않을 수 있다.

기존 클래스를 상속해 만든 클래스에 새 멤버 변수를 추가하지 않으면 기본 생성자가 없어도 된다.
<h3></h3>
<h3><strong>명시적 생성자(Explicit Constructors)</strong></h3>
인자가 하나인 생성자에는 C++ 키워드인 explicit를 사용한다.

<strong>설명:</strong>

일반적으로 인자가 하나인 생성자는 변환에 사용할 수 있다. 예를 들어 Foo::Foo(string name)을 정의하고 Foo를 인자로 받는 함수에 string을 전달하면, 이 생성자를 호출해 string을 Foo로 변환한 다음 함수에 전달한다. 이는 편리하지만 의도하지 않게 변환하고 새 객체를 생성해 문제를 일으킬 수도 있다. 생성자를 explicit로 선언하면 변환할 때 암시적으로 호출하는 것을 막을 수 있다.

<strong>장점:</strong>

바람직하지 않은 변환을 피한다.

<strong>단점:</strong>

없음.

<strong>결론:</strong>

인자가 하나인 생성자는 모두 명시적으로 만든다. 클래스 정의에서 단일 인자 생성자 앞에 다음처럼 항상 explicit를 사용한다.


explicit Foo(string name);

복사 생성자는 예외인데, 드물지만 사용을 허락할 때는 explicit로 선언하지 않아야 한다. 다른 클래스를 투명하게 감쌀 목적으로 만든 클래스 역시 예외로 한다. 그럴 때는 주석문으로 명확히 설명한다.

복사 생성자(Copy Constructors)

복사 생성자와 대입 연산자는 필요할 때만 사용한다. 사용하지 않을 때는 DISALLOW_COPY_AND_ASSIGN을 사용해 비활성화 한다.

설명:

복사 생성자와 대입 연산자는 객체의 복사본을 만들 때 사용한다. 복사 생성자는 객체를 값으로 전달하는 등 일부 상황에서 컴파일러에서 암시적으로 호출한다.

장점:

복사 생성자는 객체를 쉽게 복사할 수 있게 한다. STL 컨테이너에서는 모든 내용을 복사하고 대입할 수 있어야 한다. 복사 생성자는 CopyFrom() 형식의 방법보다 더 효율적일 수 있다. 복사하면서 생성하고 어떤 상황에서는 컴파일러에서 이 과정을 건너 뛸 수 있으며, 힙을 할당하지 않도록 하는 것도 더 쉽기 때문이다.

단점:

C++에서 객체를 암시적으로 복사하는 것은 많은 버그와 성능 문제를 만들 수 있다. 또한 가독성을 떨어뜨리고 참조에 비해 값으로 전달할 때 어떤 객체를 전달하는지 추적하기 어려워, 객체가 어디서 바뀌는지 알기 어렵다.

결론:

복사할 수 있어야 하는 클래스는 적다. 대부분은 복사 생성자와 대입 연산자가 없어도 된다. 많은 경우에 포인터나 참조는 값을 복사하는 것만큼 잘 동작하고 성능도 더욱 좋다. 예를 들어 함수 매개변수를 값 대신 참조나 포인터로 전달할 수 있고 STL 컨테이너에 객체 대신 포인터를 저장할 수도 있다.

클래스를 복사할 수 있어야 한다면 복사 생성자보다 CopyFrom()이나 Clone()과 같은 복사 메소드를 제공하는 게 더 좋다. 이런 메소드는 암시적으로 호출할 수 없기 때문이다. (성능 문제나 클래스를 STL 컨테이너에 값으로 저장해야 하는 등의 이유로) 복사 메소드가 적합하지 않으면 복사 생성자와 대입 연산자 모두를 제공한다.

복사 생성자나 대입 연산자가 필요 없으면 명시적으로 비활성화 해야 한다. 그렇게 하려면 클래스 private: 구역에 복사 생성자와 대입 연산자 선언을 추가하고 (사용하려 하면 링크 오류가 생기도록) 정의는 하지 않는다.

편리하게 DISALLOW_COPY_AND_ASSIGN 매크로를 사용할 수 있다.

// A macro to disallow the copy constructor and operator= functions
// This should be used in the private: declarations for a class
#define DISALLOW_COPY_AND_ASSIGN(TypeName) \
  TypeName(const TypeName&);               \
  void operator=(const TypeName&)

그런 다음 Foo 클래스에서는 다음처럼 한다.

class Foo {
 public:
  Foo(int f);
  ~Foo();

 private:
  DISALLOW_COPY_AND_ASSIGN(Foo);
};

구조체 대 클래스(Structs vs. Classes)

데이터를 나르는 수동 객체(passive object)일 때만 struct를 사용하고 그 외에는 모두 class를 사용한다.

C++에서 struct와 class 키워드는 거의 동일하므로 각 키워드에 내포된 의미를 추가해 정의하는 데이터 타입에 따라 적절한 키워드를 사용한다.

struct는 데이터를 나르는 수동 객체에 사용하고, 관련 상수는 있을 수 있으나 데이터 멤버를 접근하거나 설정하는 것 외 다른 기능은 없어야 한다. 필드에 접근하거나 설정할 때는 메소드를 호출하지 말고 해당 필드에 직접 접근해 처리한다. 메소드에서는 행위를 제공하지 않아야 하고, 생성자, 소멸자, Initialize(), Reset(), Validate() 등을 예로 든 것처럼 데이터 멤버를 설정하는 데만 써야 한다.

더 많은 기능이 필요하면 class가 더 적절하다. 무엇을 써야 할지 모르겠으면 class로 한다.

STL에 맞춰 함수자와 특성(trait)에 대해 class 대신 struct를 쓸 수 있다.

구조체와 클래스 멤버 변수는 서로 다른 이름 규칙을 사용한다는 점에 주의한다.

상속(Inheritance)

종종 상속보다 포함이 더 낫다. 상속은 public으로 한다.

설명:

하위 클래스에서 기초 클래스를 상속하면 부모인 기초 클래스에서 정의한 모든 데이터와 연산에 대한 정의를 하위 클래스에서 포함한다. 실제로 C++에서 상속은 두 가지 주요한 방법으로 하는데, 실제 코드를 자식에서 상속하는 구현 상속과 메소드 이름만 상속하는 인터페이스 상속이다.

장점:

구현 상속은 기존 타입을 전문화 하듯이, 기초 클래스 코드를 재사용해 코드 크기를 줄인다. 상속은 컴파일 시점 선언이므로 컴파일러에서 연산을 이해하고 오류를 발견할 수 있다. 인터페이스 상속은 클래스에서 특정 API를 드러내도록 프로그램적으로 강제하는데 사용할 수 있다. 이 역시도 API 중 필요한 메소드를 클래스에서 정의하지 않으면 컴파일러에서 오류를 찾을 수 있다.

단점:

구현 상속에서는 하위 클래스를 구현한 코드가 기초와 하위 클래스에 퍼져있으므로 구현 내용을 이해하기 더 어려울 수 있다. 함수가 가상이 아니면 하위 클래스에서 재정의할 수 없으므로 구현 내용을 하위 클래스에서 바꿀 수 없다. The base class may also define some data members, so that specifies physical layout of the base class.

결론:

모든 상속은 public이어야 한다. private 상속을 하려면 그 대신 기초 클래스 인스턴스를 멤버로 포함해야 한다.

구현 상속을 지나치게 사용하지 말아야 한다. 종종 포함이 더 낫다. 상속은 ‘is-a’ 관계일 때로 한정해 사용하도록 한다. 즉 Bar는 Foo의 ‘한 종류이다(is a kind of)’라고 말할 수 있으면 Bar가 Foo를 상속한 것이다.

필요하면 소멸자를 virtual로 한다. 클래스에 가상 메소드가 있으면 소멸자는 가상이어야 한다.

protected는 하위 클래스에서 접근할 수도 있는 멤버 함수로 제한해 사용한다. 데이터 멤버는 private이어야 한다는 점에 주의한다.

상속한 가상 함수를 파생 클래스에서 재정의할 때는 virtual을 명시적으로 선언한다. 이유는 virtual을 생략하면 해당 클래스의 모든 조상을 확인해야 해당 함수가 가상인지 아닌지 알 수 있기 때문이다.

다중 상속(Multiple Inheritance)

다중 상속은 실제로 매우 드문 경우에만 유용하다. 다중 상속은 여러 기초 클래스 중 단 하나에만 구현 내용이 있을 때 허용한다. 그러므로 다른 기초 클래스는 Interface 접미어를 붙인 순수 인터페이스 클래스여야 한다.

설명:

다중 상속으로 하위 클래스에서는 기초 클래스를 하나 이상 상속할 수 있다. 여기서는 순수 인터페이스인 기초 클래스와 구현 내용이 있는 것을 구분한다.

장점:

다중 구현 상속은 단일 상속보다 더 많은 코드를 재사용 할 수 있다(상속 참조).

단점:

다중 구현 상속은 실제로 매우 드문 경우에만 유용하다. 다중 구현 상속이 해결책인 것처럼 보일 때라도 흔히 더 분명하고 명확한 다른 해결책을 찾을 수 있다.

결론:

다중 상속은 모든 상위 클래스가 순수 인터페이스일 때(상위 클래스 중 최초 하나는 예외일 수 있다)만 허용한다. 모두가 순수 인터페이스라는 것을 확인하기 위해 Interface 접미어를 붙여야 한다.

참고: 윈도에서는 이 규칙에 예외가 있다.

인터페이스(Interfaces)

어떤 조건을 만족하는 클래스는 필수는 아니지만 Interface 접미어를 붙일 수 있다.

설명:

다음 조건을 만족하는 클래스는 순수 인터페이스이다.

  • public인 순수 가상(‘= 0’) 메소드와 정적 메소드만 있다.
  • 비정적 데이터 멤버는 없을 수 있다.
  • 생성자는 정의하지 않아도 된다. 생성자가 있으면 인자가 없어야 하고 protected여야 한다.
  • 하위 클래스이면 위 조건을 만족하는 클래스만 상속해야 하고 Interface 접미어를 붙일 수 있다.

인터페이스 클래스는 순수 가상 메소드 때문에 직접 인스턴스를 만들 수 없다. 해당 인터페이스에 대한 구현 내용이 모두 올바르게 소멸하도록 하려면 가상 소멸자도 선언해야 한다. 더 자세한 내용은 스트롭스트룹이 지은 The C++ Programming Language 3판 12.4절을 참조한다.

장점:

클래스에 Interface 접미어를 붙이면 해당 클래스에 구현 메소드나 비정적 데이터 멤버를 추가하지 말아야 한다는 것을 알릴 수 있다. 이는 다중 상속을 할 때 특히 중요하다. 게다가 인터페이스 개념은 자바 프로그래머 사이에 이미 잘 알려져 있다.

단점:

Interface 접미어는 클래스 이름을 길게 하므로 읽고 이해하기 어려울 수 있다. 게다가 인터페이스 속성은 클라이언트에 드러내지 않아야 할 상세 구현 내용으로 간주할 수 있다.

결론:

위 조건에 맞을 때만 클래스 이름 끝에 Interface를 붙일 수 있으나 역은 아니다. 즉 위 조건을 만족하는 클래스 이름이 Interface로 끝나지 않아도 된다[3].

연산자 중복 정의(Operator Overloading)

특별한 경우를 제외하고 연산자를 중복 정의하지 않는다.

설명:

해당 클래스를 내장 타입처럼 연산하는 +와 / 같은 연산자는 중복 정의할 수 있다.

장점:

클래스를 (int 같은) 내장 타입과 같은 방식으로 다루므로 코드를 더 직관적으로 만들 수 있다. 중복 정의한 연산자는 Equals()나 Add() 같이 딱딱해 보이는 이름의 함수에 비해 사용하기 더 좋다. 일부 템플릿 함수를 올바로 동작하게 하려면 연산자를 정의해야 할 수도 있다.

단점:

연산자 중복 정의를 통해 코드를 더 직관적으로 만들 수 있으나 몇 가지 단점이 있다.

  • 비용이 많이 드는 연산을 어리석게도 값싼 내장 연산으로 생각하게 만들 수 있다.
  • 중복 정의한 연산자를 어디서 호출하는지 더 알기 어렵다. 관련 있는 == 호출을 찾는 것보다 Equals()를 찾는 것이 더 쉽다.
  • 일부 연산자는 포인터에도 동작하므로 버그가 생기기 쉽다. Foo + 4와 &Foo + 4는 전혀 다른 일을 하지만 컴파일러에서는 어느 것도 문제 없으므로 디버그 하기 매우 어렵다.

중복 정의 때문에 놀랄만한 결과가 생길 수 있다. 예를 들어 클래스에서 단일 연산자인 operator&를 중복 정의하면 그 클래스를 안전하게 전방 선언할 수 없게 된다.

결론:

일반적으로는 연산자 중복 정의를 사용하지 않는다. 특히 대입 연산자(operator=)는 모르는 사이 문제를 일으킬 수 있으므로 피해야 한다. 이런 것이 필요하면 Equals()와 CopyFrom()과 같은 함수를 정의할 수 있다. 마찬가지로 클래스를 전방 선언해야 할 것 같으면 위험한 단일 연산자인 operator&는 절대로 중복 정의하지 않는다.

드물지만 템플릿이나 로그 기록을 위한 operator<<(ostream&, const T&)처럼 ‘표준’ C++ 클래스와 함께 사용하기 위해 연산자를 중복 정의해야 하는 경우도 있다. 충분히 합당하면 그렇게 할 수 있으나 가능하면 피하는 게 좋다. 특히 STL 컨테이너에서 키로 사용할 수 있는 클래스에서는 operator==나 operator<를 중복 정의하지 않는다. 대신 컨테이너를 선언할 때 동등과 비교 함수자 타입을 만들어야 한다.

일부 STL 알고리즘을 사용하려면 operator==를 중복 정의해야 하므로 그런 경우 이유를 문서화하고 그렇게 할 수 있다.

복사 생성자(Copy Constructors)와 함수 중복 정의(Function Overloading)도 참조한다.

접근 제어(Access Control)

데이터 멤버는 private으로 하고 필요하면 접근자 함수를 통해 접근하도록 한다(구글 테스트를 사용할 때는 기술적인 이유로 테스트 픽스처 클래스 데이터 멤버를 protected로 하는 것을 허용한다). 전형적으로 변수는 foo_, 그에 대한 접근자 함수는 foo()이다. 또한 변경자 함수 set_foo()를 사용할 수도 있다. 예외: (일반적으로 이름이 kFoo인) static const 데이터 멤버는 private일 필요 없다.

접근자 정의는 흔히 헤더 파일에 인라인으로 정의한다.

상속(Inheritance)과 함수 이름(Function Names)도 참조한다.

선언 순서(Declaration Order)

클래스 안에서 선언할 때는 public: 다음에 private:, 메소드 다음에 데이터 멤버(변수) 등과 같은 지정 순서를 유지한다.

클래스를 정의할 때는 public: 구역부터 시작해 protected:와 private: 순서로 하며 비어 있는 구역은 생략한다.

각 구역 내에서 선언은 일반적으로 다음 순서를 따른다.

  • 타입 정의와 열거형
  • 상수 (static const 데이터 멤버)
  • 생성자
  • 소멸자
  • 메소드. 정적 메소드를 포함한다.
  • 데이터 멤버 (static const 데이터 멤버 제외)

프렌드 선언은 항상 private: 구역에 두며 DISALLOW_COPY_AND_ASSIGN 매크로는 private: 구역 마지막에서 호출한다. 즉 클래스에서 가장 마지막 내용이어야 한다. 복사 생성자(Copy Constructors)를 참조한다.

.cc 파일에서 메소드를 정의할 때는 최대한 선언한 순서와 같도록 해야 한다.

메소드 정의가 크면 클래스 정의 안에 인라인 하지 않는다. 일반적으로 간단하거나 성능이 중요하거나 매우 짧을 때만 메소드를 인라인으로 정의한다. 더 자세한 내용은 인라인 함수(Inline Functions)를 참조한다.

함수는 짧게(Write Short Functions)

함수는 짧으면서 한 가지 일에 집중 하도록 한다.

때로는 긴 함수를 써야 할 때도 있다는 걸 알기 때문에 함수 길이를 강하게 제약하진 않는다. 함수 길이가 약 40 줄을 넘으면 프로그램 구조를 망가뜨리지 않으면서 내용을 나눌 수 없는지 생각해 본다.

긴 함수가 지금 잘 동작하고 있더라도 몇 달 내에 누군가가 새 기능을 추가하려고 변경할 수 있고, 결과적으로 찾기 어려운 버그가 생길 수 있다. 함수를 짧고 단순하게 하면 다른 개발자가 그 코드를 더 쉽게 읽고 변경할 수 있다.

어떤 코드로 작업하고 있을 때 길고 복잡한 함수를 만날 수도 있으며 기존 코드를 변경하는데 겁먹을 필요 없다. 그런 함수를 다루거나 오류를 디버그 하는 게 어렵거나 그런 함수의 일부분을 다른 몇 곳에서 사용하고 싶으면 해당 함수를 더 작고 더 다루기 쉬운 조각으로 나누는 것을 고려한다.

구글에서 쓰는 마법(Google-Specific Magic)

구글에서는 C++ 코드에 신뢰성을 높이도록 다양한 기술과 유틸리티를 사용하는데 여기서 사용하는 다양한 방법이 다른 곳에서 본 것과 다를 수도 있다.

스마트 포인터(Smart Pointers)

실제로 포인터가 의미적으로 유효해야 한다면 scoped_ptr이 최고다. 참조 대상이 상수가 아니며 (예를 들어 STL 컨테이너 안에서) 정말로 객체 소유권을 공유해야 하면 std::tr1::shared_ptr만 사용한다. 절대로 auto_ptr은 사용하지 않는다.

설명:

‘스마트’ 포인터는 포인터처럼 동작하는 객체이며 대상 메모리를 자동으로 관리한다.

장점:

스마트 포인터는 메모리 누수를 막는데 매우 유용하며 예외에 안전한 코드를 만드는데 필수이다. 또한 동적으로 할당한 메모리를 정규화하고 문서화한다.

단점:

객체를 설계할 때는 소유자가 하나이며, 소유자 역시 고정하는 것을 우선으로 한다. 스마트 포인터는 소유권을 공유하거나 옮길 수 있어, 마치 소유권을 의미적으로 유효하도록 조심스럽게 설계할 수 있는 유혹적인 대체 방안인 것처럼 보일 수 있다. 하지만 코드를 혼란스럽게 하고 심지어 메모리를 절대 삭제할 수 없는 버그가 생길 수 있다. 스마트 포인터(특히 auto_ptr)의 실제 의미가 불명확하고 혼란스러울 수 있다. 스마트 포인터를 사용할 때 얻는 예외 안전성은 그리 중요하지 않다. 예외를 허용하지 않기 때문이다.

결론:

scoped_ptr

직관적이고 안전하다. 사용할 수 있는 곳은 어디서나 사용한다.

auto_ptr

소유권을 이전하므로 혼란스럽고 버그를 만들기 쉽다. 사용하지 않는다.

shard_ptr

참조 대상이 상수(즉 shared_ptr<const T>)일 때 안전하다. 상수가 아닌 참조 대상에 참조 횟수를 계산하는 포인터를 사용하는 것이 때때로 최상의 설계일 수도 있지만 가능하면 단일 소유자가 되도록 다시 만든다.

cpplint

문법 오류를 확인하는데 cpplint.py를 사용한다.

cpplint.py는 소스 파일을 읽어 많은 스타일 오류를 확인하는 도구이다. 완벽하지 않으며 거짓 양성과 거짓 음성 결과를 모두 줄 수 있지만 여전히 가치 있다. 거짓 양성 결과는 줄 끝에 // NOLINT를 추가해 무시할 수 있다.

프로젝트 자체 툴에서 cpplint.py를 실행하는 방법을 설명한 프로젝트도 일부 있다. 참여하고 있는 프로젝트가 그렇지 않으면 cpplint.py를 따로 받을 수도 있다.

다른 C++ 특징(Other C++ Features)

참조 인자(Reference Arguments)

참조로 전달하는 매개변수에는 모두 const를 붙여야 한다.

설명:

C에서는 함수에서 변수 내용을 바꿔야 하면 int foo(int* pval)처럼 매개변수를 포인터로 해야 한다. C++에서는 다른 방법으로 int foo(int& val)처럼 매개변수를 참조자로 선언할 수 있다.

장점:

매개변수를 참조자로 선언하면 (*pval)++와 같이 깔끔하지 못한 코드를 피할 수 있다. 복사 생성자처럼 어떤 응용 프로그램에서는 필수이다. 포인터와 달리 NULL을 사용할 수 없다는 것을 명확히 할 수 있다.

단점:

참조자는 문법적으로는 값처럼 보이지만 의미적으로는 포인터이므로 혼란스러울 수 있다.

결론:

함수 매개변수에서 모든 참조자는 const여야 한다.

void Foo(const string &in, string *out);

사실 구글 코드에서는 입력 인자를 값 또는 const 참조자로, 출력 인자를 포인터로 사용하는 것이 관례로 매우 강하게 굳어져 있다. 입력 매개변수로 const 포인터를 쓸 수는 있지만 const가 아닌 참조자 매개변수는 절대로 안 된다.

입력 매개변수로 const 포인터를 사용할 수도 있는 경우 중 하나는 그 인자를 복사할 수 없다는 것을 강조하고 싶을 때이다. 그러므로 그 객체는 생명주기 동안 반드시 존재해야 한다. 물론 이런 내용을 주석으로 기록하는 것 역시 좋다. bind2nd와 mem_fun과 같은 STL 어댑터는 참조 매개변수를 허용하지 않으므로 이 때는 포인터를 매개변수로 사용해 함수 선언을 해야 한다.

함수 중복 정의(Function Overloading)

중복 정의한 것 중 어떤 것을 호출하게 될지 생각해 보지 않고도 쉽게 알 수 있을 때만 (생성자를 포함해) 중복 정의 함수를 사용한다.

설명:

const string&를 취하는 함수를 만든 다음 const char*를 취하는 다른 함수를 중복 정의할 수 있다.

class MyClass {
 public:
  void Analyze(const string &text);
  void Analyze(const char *text, size_t textlen);
};

장점:

중복 정의를 사용하면 이름은 같으나 취하는 인자가 다른 함수를 만들 수 있어 코드를 더 직관적으로 만들 수 있다. 중복 정의는 템플릿 코드에서 사용할 수 있고 비지터(visitor) 패턴에서 편리하다.

단점:

인자 타입만으로 함수를 중복 정의했을 때 어떤 일이 생기는지 알려면 복잡한 C++ 일치 규칙을 알아야 할 수 있다. 게다가 어떤 함수의 여러 변형 중 일부만을 파생 클래스에서 재정의(override)해 상속하면 많은 이들이 혼란스러워 한다.

결론:

함수를 중복 정의하고 싶으면 Append()로만 쓰는 대신 AppendString(), AppendInt()처럼 인자에 대한 정보를 이름에 붙이는 것을 고려해 본다.

기본 인자(Default Arguments)

아래에 설명하는 몇 가지 비일반적인 경우를 제외하고는 기본 함수 매개변수는 허용하지 않는다.

장점:

기본값을 많이 사용하는 함수를 종종 사용하면서 때때로 그 기본값을 재정의하고 싶을 수 있다. 기본 매개변수를 사용하면 잘 쓰지 않는 많은 함수를 정의하지 않고도 그런 일을 쉽게 할 수 있다.

단점:

API를 어떻게 사용하는지 이해할 때는 흔히 그것을 사용하는 기존 코드를 살펴본다. 기본 매개변수를 사용하면 복사 후 붙여 넣기 할 때 이전 코드에 모든 매개변수가 나타나지 않을 수 있으므로 유지보수하기 더 어렵다. 기본 인자가 새 코드에 적절하지 않을 때 코드 조각을 복사 후 붙여 넣기 하면 큰 문제가 생길 수 있다.

결론:

아래 설명한 경우를 제외하고 모든 인자는 명시적으로 지정해, 프로그래머가 모르는 사이 기본 값을 적용하기보다 API와 각 인자에 전달하는 값에 주의할 수 있게 강제한다.

특정 예외 한가지는 변수 길이 인자 목록을 시뮬레이션 하는데 기본 인자를 사용할 때이다.

// Support up to 4 params by using a default empty AlphaNum.
string StrCat(const AlphaNum &a,
              const AlphaNum &b = gEmptyAlphaNum,
              const AlphaNum &c = gEmptyAlphaNum,
              const AlphaNum &d = gEmptyAlphaNum);

변수 길이 배열과 alloca()(Variable-Length Arrays and alloca())

변수 길이 배열이나 alloca()는 허용하지 않는다.

장점:

변수 길이 배열은 문법이 자연스러워 보인다. 변수 길이 배열과 alloca()는 모두 매우 효율적이다.

단점:

변수 길이 배열과 alloca()는 표준 C++ 부분이 아니다. 더 중요한 것은, 데이터 크기에 의존해 스택에 공간을 할당하며 메모리를 덮어쓰는 찾기 어려운 버그를 일으킬 수 있다는 점이다. 즉 다음과 같은 상황이다. “내 컴퓨터에서는 잘 동작하는데 실제 제품에서는 이상하게 종료됩니다.”

결론:

대신 scoped_ptr, scoped_array처럼 안전한 할당자를 사용한다.

프렌드(Friends)

friend 클래스와 함수는 이유가 있을 때만 허용한다.

일반적으로 프렌드는 같은 파일에서 정의하므로 다른 파일을 살펴보지 않아도 클래스에서 private 멤버를 사용하는 것을 알 수 있다. friend를 사용하는 일반적인 방법은 FooBuilder 클래스를 Foo의 프렌드로 만들어 Foo의 내부 상태를 밖으로 드러내지 않고도 그 상태를 올바르게 만들 수 있게 하는 것이다. 때로는 단위 테스트 클래스를 테스트할 클래스의 프렌드로 만드는 것이 유용할 수도 있다.

프렌드는 클래스의 캡슐화 범위를 넓히지만 망가뜨리지는 않는다. 어떤 경우에는 멤버를 public으로 하는 것보다 다른 클래스 하나에서만 접근할 수 있게 하는 게 더 좋다. 하지만 대부분의 클래스에서는 public 멤버를 통해서 다른 클래스와 상호작용 하도록 하는 게 좋다.

예외(Exceptions)

C++ 예외는 사용하지 않는다.

장점:

  • 예외를 사용하면 오류 코드를 사용할 때 생길 수 있는 불명확함이나 오류를 만들기 쉬운 문제 없이, 깊게 중첩된 함수에서 ‘생기지 않아야 할’ 실패를 어떻게 처리할지 응용 프로그램의 더 상위 수준에서 결정할 수 있다.
  • 예외는 현대 언어 대부분에서 사용한다. C++에서 사용함으로써 파이썬, 자바와 더 일관성 있게 되고 다른 언어에서도 C++에 더 친숙하게 된다.
  • 일부 서드 파티 C++ 라이브러리에서는 예외를 사용하므로 내부적으로 그 기능을 끄고 통합하기 더 어렵다.
  • 예외는 생성자에서 실패를 알리는 유일한 방법이다. 이를 팩토리 함수나 Init() 메소드로 시뮬레이션 할 수 있지만 이 역시 각각 힙을 할당하거나 새로운 ‘무효(invalid)’ 상태를 만들어야 한다.
  • 예외는 테스트 프레임워크에서 정말 간편하다.

단점:

  • 기존 함수에 throw 문을 추가하면 중간에 호출하는 곳을 모두 살펴봐야 한다. 호출하는 곳에서 적어도 기본 예외 안전성을 반드시 보장하도록 하든지 예외를 잡지 않아 결과적으로 프로그램을 종료하도록 해야 한다. 예를 들어 f()에서 g()를 호출하고 g()에서 h()를 호출할 때, f에서 잡을 수 있는 예외를 h에서 발생하면 g에서는 조심스럽게 처리하든지 적절히 처리하지 못할 수 있다.
  • 더 일반적으로 예외를 사용하면 코드를 살펴볼 때 프로그램 제어 흐름을 알기 어렵게 한다. 생각하지 않은 곳에서 함수를 반환할 수 있기 때문이다. 이 때문에 유지보수성과 디버깅이 어려워진다. 예외를 어디서 어떻게 써야 하는지 규칙을 정해 이런 비용을 최소화할 수 있지만 개발자가 그 규칙을 알고 이해하는데 더 많은 비용이 든다.
  • 예외 안전성은 RAII와 다른 코딩 사례 모두에서 필요하다. 예외를 지원하는 많은 컴퓨터에서 예외 안전한 코드를 올바르고 쉽게 만들 수 있도록 해야 한다. 더욱이 개발자가 전체 호출 경로를 이해하지 않아도 되도록 예외 안전한 코드에서는 로직을 영속적인 상태로 작성하고, ‘발생(commit)’ 단계로 고립시켜야 한다. 이렇게 함으로써 이점과 (어쩌면 발생 단계로 고립시켜 코드를 난해하게 만드는) 비용이 모두 생긴다. 예외를 사용하면 그 것이 가치가 없을지라도 항상 그런 비용을 지불하도록 강제한다.
  • 예외를 켜면 생성한 각 바이너리에 데이터를 추가해 컴파일 시간이 (아마도 약간) 늘어날 것이고 주소 공간에 대한 압박이 증가할 수 있다.
  • 예외를 사용할 수 있으면 예외를 발생하는 것이 적합하지 않거나 예외에서 회복하는 것이 안전하지 않을 때에도 개발자가 그렇게 하도록 할 수 있다. 예를 들어 사용자가 잘못 입력했을 때는 예외를 발생하지 않아야 한다. 이러한 제한에 대해 더 길더라도 스타일 안내서를 문서화해야 한다!

결론:

각자 상황에서는 예외를 사용해 얻는 이점이 비용보다 더 크며 특히 새 프로젝트에서 더 그렇다. 하지만 기존 코드에 예외를 도입하면 의존하는 모든 코드에 영향을 미친다. 새 프로젝트에만 한정해 예외를 쓰는 게 아니라면 예외를 사용하지 않는 기존 코드에 새 프로젝트를 통합하는 것 역시 문제가 된다. 구글에서 만든 대부분의 C++ 코드는 예외를 처리할 수 있도록 되어 있지 않으므로 예외를 발생하는 새 코드를 도입하는 것이 상대적으로 어렵다.

구글에서 만든 기존 코드는 예외와 잘 어울리지 않으므로 예외를 사용하는 비용이 새 프로젝트에서 드는 것보다 다소 더 크다. 또한 변환 과정 역시 느리고 오류도 발생하기 쉽다. 예외는 큰 부담이 생기지만 오류 코드와 단정문 정도로 예외를 대체할 수 있는 방법이 있다고는 생각하지 않는다.

예외 사용에 대해 할 수 있는 조언은 철학이나 도덕에 근거한 것이 아니라 실용주의적이다. 구글에서는 자체적으로 만든 오픈 소스 프로젝트를 사용하길 좋아하는데 그런 프로젝트에서 예외를 사용하면 그렇게 하기 어려우므로 구글 오픈 소스 프로젝트에서도 예외에 대한 조언이 필요하다. 처음부터 다시 모든 것에 대해 그렇게 해 왔다면 아마 좀 달랐을 거다.

윈도 코드에서는 이 규칙에 예외가 있다[4].

실행 중 타입 정보(Run-Time Type Information, RTTI)

실행 중 타입 정보(RTTI)를 사용하지 않는다.

설명:

RTTI를 사용하면 실행 중에 객체의 C++ 클래스 정보를 얻을 수 있다.

장점:

일부 단위 테스트에서 유용하다. 예를 들면 새로 생성한 객체가 기대한 동적 타입인지 검증하는 테스트를 실행하는 팩토리 클래스의 테스트에서 유용하다.

드물게 심지어 테스트 외에도 유용하다.

단점:

실행 중에 타입을 확인해야 한다는 것은 설계에 문제가 있다는 걸 뜻한다. 실행 중에 객체 타입을 알아야 한다면 종종 클래스 설계를 다시 해야 한다는 것을 나타낸다.

결론:

단위 테스트가 아니면 RTTI를 사용하지 않는다. 해당 객체의 클래스에 따라 다르게 동작하는 코드를 만들어야 한다면 타입을 확인하는 다른 방법을 고려한다.

가상 메소드는 특정 하위 클래스 타입에 따라 다른 코드를 실행할 수 있는 더 좋은 방법이다. 이는 그런 동작을 객체 내에 넣는다.

만약 그런 동작이 객체에 속하지 않으나 대신 어떤 처리 코드(processing code)에 속한다면 비지터 디자인 패턴과 같은 이중 호출(double-dispatch) 해법을 고려한다. 이를 사용하면 내장 타입 시스템을 사용하는 클래스 타입을 결정하는데 객체에 속하지 않는 방법을 사용할 수 있다.

이런 아이디어를 정말로 사용할 수 없다고 생각하면 RTTI를 사용할 수도 있다. 하지만 그에 대해서는 두 배로 생각한다. 🙂 그런 다음 또 두 배 더 생각한다. Do not hand-implement an RTTI-like workaround. The arguments against RTTI apply just as much to workarounds like class hierarchies with type tags.

변환(Casting)

static_cast<>()와 같은 C++ 변환을 사용하고 int y = (int)x; 또는 int y = int(x);와 같은 형식은 사용하지 않는다.

설명:

C++에서는 변환 연산자 타입을 구별하는, C와는 다른 변환 시스템을 사용한다.

장점:

C 변환의 문제는 연산이 모호하다는 점이다. 즉 어떤 때는 (int)3.5와 같이 값을 변환(conversion)하고 어떤 때는 (int)”hello”처럼 타입을 변환(cast)한다. C++ 변환에서는 이렇게 하지 않는다. 게다가 C++ 변환은 찾기도 더 쉽다.

단점:

문법이 안 좋다.

결론:

C 형식 변환을 사용하지 말고 C++ 형식 변환을 사용한다.

  • 값을 변환하거나 명시적으로 클래스 포인터를 상위 클래스로 업 캐스팅을 해야 하면 static_cast를 사용한다.
  • const 한정자를 제거할 때는 const_cast를 사용한다(const 참조).
  • 포인터 타입을 정수나 다른 포인터 타입으로 변환하거나 그 반대로 하는, 안전하지 않은 변환을 할 때는 reinterpret_cast를 사용한다. 이 것은 자신이 무엇을 하고 있는지, 관련 문제가 무엇인지 이해할 때만 사용한다.
  • 테스트 코드 외에는 dynamic_cast를 사용하지 않는다. 단위 테스트 외에서 이런 방법으로 실행 중에 타입 정보를 알아야 한다면 설계에 문제가 있을 수 있다(RTTI 참조).

스트림(Streams)

로그를 기록할 때만 스트림을 사용한다.

설명:

스트림은 printf()와 scanf()를 대체한다.

장점:

스트림을 사용하면 출력하는 객체 타입을 몰라도 된다. (비록 gcc에서는 printf에서도 그런 문제가 없지만) 형식 문자열을 인자 목록과 일치시켜야 하는 문제도 없다. 스트림에는 자동으로 관련 파일을 열고 닫는 생성자와 소멸자 있다.

단점:

스트림은 pread()처럼 다루기 어렵다. printf를 사용하지 않고 스트림을 사용해 효율적으로 처리하는 게 불가능하지 않을지라도, 특히 일반적인 형식 문자열 관용구인 %.*s처럼 어떤 형식 문자열은 어렵다. 국제화에 많이 도움되는 %1s 지시자 같은 재정렬 연산자를 스트림에서는 지원하지 않는다.

결론:

로그를 기록하는 인터페이스에서 필요해서 사용할 때를 제외하고 스트림을 사용하지 않는다. 대신 printf와 같은 루틴을 사용한다.

스트림을 사용하는 데에는 다양한 장단점이 있으나 이 역시 다른 많은 경우에서처럼 일관성이 가장 중요하다. 코드에 스트림을 사용하지 말아라.

추가 논의

여기에는 논쟁이 있으므로 그 논거를 더 깊게 설명한다. 단 한 가지 방법을 사용한다는 지침을 떠 올려 보자. 즉 어떤 타입의 I/O를 처리하는 모든 곳에서 코드가 같게 보이도록 하고 싶다. 그러므로 스트림을 사용할지 printf에 읽기/쓰기를 함께 사용할지 사용자가 선택하도록 하고 싶진 않으며, 대신 둘 중 하나를 선택해야 한다. 이는 매우 특수한 경우이며 역사적인 이유로 로그를 기록하는 것은 예외로 했다.

스트림을 옹호하는 쪽에서는 둘 중 스트림을 선택하는 게 명확하다고 주장하지만 이 문제는 사실 그렇게 명확하지 않다. 스트림을 사용할 때 얻는 장점 각각에 대해 단점 역시 모두 있다. 가장 큰 장점은 출력할 객체 타입을 몰라도 된다는 것이다. 이는 대단한 것이다. 하지만 역으로 잘못된 타입을 사용하기 쉽고 컴파일러에서는 그에 대해 경고하지 않는다. 즉 스트림을 사용할 때는 이런 실수를 모르고 하기 쉽다.

cout << this;  // Prints the address
cout << *this;  // Prints the contents
&#91;/sourcecode&#93;

&lt;&lt;를 중복 정의했기 때문에 컴파일러에서는 오류를 발생하지 않는다. 이 때문에 중복 정의를 권하지 않는다.

일부는 prinf 형식 문자열이 보기 안 좋고 읽기도 어렵다고 한다. 하지만 스트림은 때때로 더 안 좋다. 다음 두 코드 조각을 살펴보자. 둘 모두 같은 의미이다. 어느 것이 이해하기 더 쉬울까?

&#91;sourcecode language="cpp"&#93;
cerr << "Error connecting to '" << foo->bar()->hostname.first
     << ":" << foo->bar()->hostname.second << ": " << strerror(errno);

fprintf(stderr, "Error connecting to '%s:%u: %s",
        foo->bar()->hostname.first, foo->bar()->hostname.second,
        strerror(errno));

기타 등등의 다른 문제를 거론할 수도 있다(“올바른 래퍼를 사용하는 게 더 좋습니다.”라고 주장할 수도 있다. 하지만 그게 한 경우에 맞다고 해서 다른 경우에도 그럴까? 게다가 누구간 배워야 할 것을 더 추가하는 게 아니라 이 언어를 더 작게 만드는 게 목적이라는 걸 기억해야 한다).

어느 것이든 서로 다른 장단점이 있으며 명확히 더 우월한 해결책은 없다. 단순주의에 따라 그 중 하나를 선택해야 하며 결정은 printf + read/write 였다.

전위 증가와 전위 감소(Preincrement and Predecrement)

증가와 감소 연산자를 반복자와 다른 템플릿 객체와 함께 쓸 때는 접두어 형식(++i)를 사용한다.

설명:

변수를 증가(++i 또는 i++) 또는 감소(–i 또는 i–)할 때 그 값을 표현식에서 사용하지 않으면 전위 증가(감소) 또는 후위 증가(감소) 중 하나를 선택하면 된다.

장점:

반환 값을 사용하지 않을 때 ‘전위’형(++i)은 ‘후위’형(i++)보다 효율이 떨어지지 않으며 종종 더 효율적이다. 이는 후위 증가(또는 감소)를 할 때는 표현식에서 사용하기 위해 i에 대한 복사본을 만들어야 하기 때문이다. i가 반복자나 스칼라(scalar)가 아닌 타입이면 i를 복사하는 데에 비용이 많이 든다. 두 가지 증가 형식은 그 값을 사용하지 않을 때 똑같이 동작하는데 항상 전위 증가를 사용하지 않을 이유가 있을까?

단점:

전통적으로 C로 개발해 오면서 표현식 값을 사용하지 않을 때는 후위 증가를 사용했으며 특히 for 루프에서 그렇다. 일부는 후위 증가를 사용하는 게 더 읽기 쉽다는 것을 안다. 영어처럼 ‘주어’(i)가 ‘동사’(++)보다 앞에 나오기 때문이다.

결론:

객체가 아닌 단순 스칼라 값일 때는 어느 것을 쓰든 괜찮다. 반복자와 템플릿 타입에 대해서는 전위 증가를 사용한다[5].

const 사용(Use of const)

의미적으로 타당하면 항상 const를 사용하기를 권한다.

설명:

변수와 매개변수를 선언할 때 const int foo처럼 const를 앞에 둬 그 변수가 바뀌지 않는다는 것을 나타낼 수 있다. Class Foo { int Bar(char c) const; };처럼 클래스 함수에 const 한정자를 사용해 그 함수에서 해당 클래스 멤버 변수 상태를 바꾸지 않는다는 것을 나타낼 수 있다.

장점:

변수를 어떻게 사용하는지 더 쉽게 이해할 수 있다. 컴파일러에서 타입 검사를 더 잘 할 수 있으며 더 나은 코드를 만들게 한다. 호출하는 함수에서 변수를 바꾸는데 제한이 있는지 알 수 있으므로 프로그램이 올바른지 파악하는데 도움이 된다. 다중 스레드 프로그램에서 어떤 함수를 잠김 현상 없이 사용할 수 있는지 아는데 도움이 된다.

단점:

const는 바이러스같다. const 변수를 함수에 전달하면 그 함수 역시 원형 선언(prototype)에 const를 사용해야 한다(또는 그 변수에 const_cast를 해야 한다). 이는 라이브러리 함수를 호출할 때 특별한 문제가 될 수 있다.

결론:

const 변수, 데이터 멤버, 메소드, 인자는 컴파일 시점에 타입 검사를 한다. 가능한 빨리 오류를 찾는 게 더 낫기 때문이다. 그러므로 타당하다면 언제나 const를 사용하기를 강하게 권한다.

  • 함수에서 참조나 포인터로 전달한 인자를 변경하지 않으면 그 인자는 const여야 한다.
  • 가능하면 언제나 메소드는 const로 선언한다. 접근자는 거의 언제나 const여야 한다. 다른 메소드인 경우, 데이터 멤버를 전혀 변경하지 않고 const 메소드가 아닌 것은 호출하지 않으며 const가 아닌 포인터나 const가 아닌 참조자를 데이터 멤버에 반환하지 않으면 const여야 한다.
  • 생성 후 변경하지 않아도 되는 데이터 멤버는 const로 만들 것을 고려한다.

하지만 const에 빠져서는 안 된다. const int * const * const x;와 같은 것은 비록 const x를 아주 정확히 표현한 것이라 해도 지나치다. 정말로 알아야 할 것에 집중한다. 이 경우 const int** x면 충분하다.

mutable 키워드도 쓸 수 있으나 스레드에서는 안전하지 않으므로 스레드 안전성을 먼저 조심스레 생각해야 한다.

const를 두는 위치

일부는 const int* foo보다 int const *foo를 지지한다. 이 것이 더 일관성이 있으므로 가독성이 더 좋다고 주장한다. 즉 const는 설명하는 대상을 항상 따른다는 규칙을 유지하기 때문이다. 하지만 일관성을 따르기 위해 해야 하는 대부분을 ‘빠져들지 말라’는 격언으로 해소할 수 있으므로 그 일관성에 관한 논거는 이 경우에 적용되지 않는다. const를 먼저 두는 것은 ‘명사’(int) 앞에 ‘형용사’(const)를 두는 영어 어순을 따르기 때문에 가독성이 더 좋다고 할 수 있다[6].

정수형(Integer Types)

C++ 내장 정수 타입 중에서는 int만 사용한다. 프로그램에서 다른 크기 변수를 사용해야 하면 int16_t와 같이 <stdint.h>에 정의한 크기를 지정한 정수형(precise-width integer type)을 사용한다.

설명:

C++에서는 정수형에 크기를 지정하지 않으며 일반적으로 short는 16비트, int는 32비트, long은 32비트, long long은 64비트로 간주한다.

장점:

선언이 일정하다.

단점:

C++에서 정수형 크기가 컴파일러와 구조에 따라 바뀔 수 있다.

결론:

<stdint.h>에서는 int16_t, uint32_t, int64_t 등을 정의한다. 정수 크기를 보장해야 할 때는 short, unsigned long long 대신 항상 사용한다. C 정수형에서는 int만 사용한다. 적절한 경우에는 size_t와 ptrdiff_t와 같은 표준 타입을 사용해도 좋다.

int는 루프 카운터처럼 너무 커지지 않는 정수에 대해 매우 자주 사용한다. 이런 일반적인 상황에는 평범하고 오래된 int를 사용한다. int는 적어도 32비트라고 가정할 수는 있으나 32비트보다 크다고 가정해서는 안 된다. 64비트 정수형이 필요하면 int64_t 또는 uint64_t를 사용한다.

크다고 할 수 있는 정수에는 int64_t를 사용한다.

표현하려는 내용이 비트 패턴이 아니거나 2의 보수를 정의하는 것이 아니라면 uint32_t와 같은 부호 없는 정수형을 사용해서는 안 된다. 특히 절대 음수가 아닌 수를 표현하는데 부호 없는 정수형을 사용하지 않는다. 대신 이럴 때는 단정문을 사용한다.

부호 없는 정수에 대해

교과서 저자를 포함해 일부는 부호 없는 정수형을 사용해 절대 음수가 아닌 수를 표현하는 것을 권한다. 이는 코드 자체로 문서화를 하기 위한 의도이다. 하지만 C에서 그런 문서화 이점보다 이 때문에 생기는 버그가 더 중요하다. 다음을 보자.

for (unsigned int i = foo.Length()-1; i >= 0; --i) ...

이 코드는 절대 종료하지 않는다. 간혹 gcc에서는 이 버그를 알고 경고하지만 대부분은 그렇지 않다. 이와 같은 버그가 부호와 부호 없는 변수를 비교할 때도 생길 수 있다. 기본적으로 C에서 타입 승격은 부호 없는 정수형을 기대한 것과 다르게 동작하게 한다.
그러므로 변수를 문서화할 때는 단정문을 사용해 음수가 아니라는 것을 나타낸다. 부호 없는 형을 사용하지 않는다.

64비트 이식성(64-bit Portability)

코드는 64비트와 32비트 친화적이어야 한다. 출력, 비교 그리고 구조체 정렬에 대한 문제를 염두에 둔다.

  • 일부 타입에 대한 printf() 지정자는 32비트와 64비트 시스템 사이에 호환이 명확하지 않다. C99에서는 일부 호환하는 형식 지정자를 정의하고 있다. 불행히도 MSVC 7.1에서는 이 지정자 중 일부를 인식하지 않으며 일부 표준이 빠져 있다. 그러므로 어떤 경우에는 보기 안 좋겠지만 스스로 (inttypes.h 표준 포함 파일 형식으로) 정의해야 한다.
// printf macros for size_t, in the style of inttypes.h
#ifdef _LP64
#define __PRIS_PREFIX "z"
#else
#define __PRIS_PREFIX
#endif

// Use these macros after a % in a printf format string
// to get correct 32/64 bit behavior, like this:
// size_t size = records.size();
// printf("%"PRIuS"\n", size);

#define PRIdS __PRIS_PREFIX "d"
#define PRIxS __PRIS_PREFIX "x"
#define PRIuS __PRIS_PREFIX "u"
#define PRIXS __PRIS_PREFIX "X"
#define PRIoS __PRIS_PREFIX "o"
타입
사용하지 않음
사용함
참고
void* (또는 모든 포인터)
%lx
%p
int64_t
%qd, %lld
%”PRId64”
uint64_t
%qu, %llu, %llx
%”PRIu64”, %”PRIx64”
size_t
%u
%”PRIuS”, %”PRIxS”
C99에서는 %zu 지정
ptrdiff_t
%d
%”PRIdS”
C99에서는 %zd 지정

PRI* 매크로는 컴파일러에서 연결하는 독립 문자열로 확장하는 것에 주목한다. 그러므로 상수가 아닌 형식 문자열을 사용하면 그 형식 문자열에 이름이 아닌 매크로 값을 넣어야 한다. 일반적으로 PRI* 매크로를 사용할 때 % 다음에 길이 지정자 등을 포함할 수도 있다. 그러므로 printf(“x = %30”PRIuS”\n”, x)는 32비트 리눅스에서 printf(“x = %30” “u” “\n”, x)로 확장할 것이고 컴파일러에서는 이를 printf(“x = %30u\n”, x)로 처리할 것이다.

  • sizeof(void*) != sizeof(int)라는 것을 기억한다. 포인터 크기인 정수가 필요하면 intptr_t를 사용한다.
  • 구조체 정렬에 유의해야 할 수 있다. 특히 디스크에 구조체를 저장하는 경우라면 더 그렇다. int64_t/uint64_t가 멤버로 있는 모든 클래스와 구조체는 기본적으로 64비트 시스템에서 8바이트 경계로 정렬한다. 이런 구조체를 32비트와 64비트 코드에서 디스크를 통해 공유한다면 두 구조에서 모두 같도록 압축해야 한다. 컴파일러 대부분에서는 구조체 정렬을 바꾸는 방법을 제공한다. gcc라면 __attribute__((packed))를 사용할 수 있다. MSVC에서는 #pragma pack()과 __declspec(align())을 사용한다.
  • 64비트 상수를 만들 때는 LL 또는 ULL 접미어를 사용한다. 예는 다음과 같다.
int64_t my_value = 0x123456789LL;
uint64_t my_mask = 3ULL << 48;
&#91;/sourcecode&#93;
<ul>
	<li>32비트와 64비트 시스템에서 정말로 서로 다른 코드를 써야 한다면 코드가 바뀌는 곳에 #ifdef _LP64를 사용한다(하지만 가능한 이는 피하고 그런 변화는 지역화한다).</li>
</ul>
<h3></h3>
<h3><strong>전처리기 매크로(Preprocessor Macros)</strong></h3>
매크로를 사용할 때는 매우 주의한다. 매크로보다는 인라인 함수, 열거형, const 변수를 우선한다.

매크로는 여러분이 보는 코드와 컴파일러가 보는 코드가 다르다는 것을 뜻한다. 이 때문에 예상하지 못한 결과가 생길 수 있는데 특히 매크가 전역 범위일 때 더욱 그렇다.

다행히 C++에서 매크로는 C에서와 달리 거의 필요하지 않다. 매크로를 사용하는 대신 성능이 중요한 코드에는 인라인 함수를 사용한다. 상수를 저장하는 매크로를 사용하는 대신 const 변수를 사용한다. 긴 변수 이름을 ‘줄이기’ 위해 매크로를 사용하는 대신 참조를 사용한다. 조건 컴파일 코드를 사용하기 위해 매크로를 사용하는 대신, 음… (물론 헤더 파일을 이중으로 포함하는 것을 막기 위해 #define 보호문을 사용하는 것을 제외하고는) 그렇게는 사용하지 않는다. 그렇게 사용하면 테스트하기가 매우 어려워진다.

이들 다른 기술로는 할 수 없는 것을 매크로를 사용해 할 수 있으며 코드 베이스, 특히 저수준 라이브러리에서 볼 수 있을 것이다. (문자열로 만들기, 문자열 결합 등) 매크로로 할 수 있는 특별한 기능 중 몇몇은 언어 기능으로 대체할 수 없다. 하지만 매크로를 사용하기 전에 매크로를 사용하지 않고 같은 결과를 얻을 수 없는지 신중히 고려한다.

다음은 매크로를 사용하며 생기는 많은 문제를 피할 수 있는 사용 방법이다. 그러므로 매크로를 사용한다면 가능한 항상 이를 따른다.
<ul>
	<li>매크로는 .h 파일에 정의하지 않는다.</li>
	<li>사용하기 바로 직전에 #define 매크로를 사용하고 사용한 후 바로 #undef 한다.</li>
	<li>기존 매크로를 자신이 만든 것으로 대체하려고 #undef로 해제하지 않는다. 대신 유일할 것 같은 이름을 사용한다.</li>
	<li>Try not to use macros that expand to unbalanced C++ constructs, or at least document that behavior well.</li>
	<li>##를 사용해 함수, 클래스, 변수 이름을 만들어 내지 않도록 한다.</li>
</ul>
<h3></h3>
<h3><strong>영과 널(0 and NULL)</strong></h3>
정수에는 0, 실수에는 0.0, 포인터에는 NULL, 문자에는 ‘’을 사용한다.

정수에는 0, 실수에는 0.0을 사용하며 이는 논쟁의 여지가 없다.

포인터(주소값)에는 0 또는 NULL을 선택할 수 있다. 비야네 스트롭스트룹은 간소하게 0을 선호한다. 여기서는 NULL을 선호하는데 포인터처럼 보이기 때문이다. 사실 gcc 4.1.0과 같은 일부 C++ 컴파일러에서는 NULL에 대해 특별히 정의해 유용한 경고를 발생할 수 있다. 예를 들면, 특히 sizeof(NULL)이 sizeof(0)과 같지 않은 상황에 그렇다

문자에는 ‘’을 사용한다. 이는 올바른 타입이며 코드 가독성 또한 더 높아진다.
<h3></h3>
<h3><strong>sizeof</strong></h3>
가능하면 언제나 <em>sizeof(</em><em>타입</em><em>)</em> 대신 <em>sizeof(변수이름</em><em>)</em>을 사용한다.

변수 타입을 바꿔도 올바로 갱신되므로 <em>sizeof(변수이름</em><em>)</em>을 사용한다. <em>sizeof(타입</em><em>)</em>은 일부 타당할 수 있으나 변수 타입을 바꾸면 제대로 동작하지 않을 수 있으므로 일반적으로 피해야 한다.


Struct data;
memset(&data, 0, sizeof(data));
memset(&data, 0, sizeof(Struct));

부스트(Boost)

부스트 라이브러리에서 승인한 라이브러리만 사용한다.

설명:

부스트 라이브러리는 많은 이의 검토를 거쳤고 무료이며 인기 있는 오픈 소스 C++ 라이브러리 모음이다.

장점:

부스트 코드는 일반적으로 품질이 매우 높고 호환성이 아주 좋으며 타입 특성(type traits), 더 나은 바인더와 더 나은 스마트 포인터 등과 같이 C++ 표준 라이브러리에 있는 많은 중요한 빈 공간을 메운다. 또한 표준 라이브러리에 TR1 확장 구현도 제공한다.

단점:

메타 프로그래밍과 고급 템플릿 기술, 과도한 ‘함수형’ 프로그래밍과 같은 일부 부스트 라이브러리에서 사용하는 코드 작성 방법은 가독성을 떨어뜨릴 수 있다.

결론:

코드를 읽고 유지보수 하는 모든 기여자에 대해 가독성을 높은 수준으로 유지하기 위해 부스트 기능 중 입증된 일부만 허용한다. 현재는 다음 라이브러리를 허용한다.

  • Call traits (boost/call_traits.hpp)
  • Compressed Pair (boost/compressed_pair.hpp)
  • C++03 표준(ptr_circular_buffer.hpp와 ptr_unordered*)에 없는 직렬화와 래퍼를 제외한 Pointer Container (boost/ptr_container)
  • Array (boost/array.hpp)
  • 직렬화(adj_list_serialize.hpp)와 병렬/분산 알고리즘과 데이터 구조체(boost/graph/parallel/*와 boost/graph/distributed/*)를 제외한 Boost Graph Library(BGL)
  • 병렬/분산 속성 맵(boost/property_map/parallel/*)을 제외한 Property Map (boost/property_map)
  • 반복자를 정의를 처리하는 Iterator 부분 (boost/iterator/iterator_adaptor.hpp, boost/iterator/iterator_facade.hpp, boost/function_output_iterator.hpp)

다른 부스트 기능을 목록에 추가하는 것을 활발히 검토 중이므로 이 규칙은 앞으로 더 유연해 질 수 있다.

C++ 0x

C++ 0x에서 승인한 라이브러리와 언어 확장만 사용한다. 현재는 승인한 것이 없다.

설명:

C++ 0x는 차기 ISO C++ 표준이며 현재 최종 초안(final committee draft) 상태이다. 여기에는 언어와 라이브러리 모두에 큰 변화가 있다.

장점:

C++ 0x가 차기 표준이 되길 바라며 결국에는 C++ 컴파일러 대부분에서 지원하게 될 것이다. 여기에서는 이미 일반적으로 사용하는 C++ 확장을 표준화 하고 일부 연산자 단축 표기를 허용하며 안전성도 일부 향상됐다.

단점:

C++ 0x 표준은 실질적으로 이전보다 더 복잡하고 많은 개발자들도 낯설어 한다. 가독성과 유지보수에 있어 일부 기능이 장기적으로 어떤 영향을 미칠지 알 수 없다. 또한 gcc, icc, clang, Eclipse 등 관심 가질만한 많은 툴에서 다양한 그 기능이 언제 통일되게 구현될지 예상할 수 없다.

부스트와 마찬가지로 일부 C++ 0x 확장에서 사용하는 코드 작성 방법은 가독성을 떨어뜨린다. 기존 구조로 사용할 수 있는 기능과 중복된 것도 있으며 이 때문에 혼란스럽거나 변환 비용이 생길 수 있다.

결론:

C++ 0x 라이브러리와 언어 기능 중 승인한 것만 사용한다. 현재는 어느 기능도 승인하지 않았지만 적절한 때에 기능별로 승인할 것이다.

이름 규칙(Naming)

일관된 규칙 중 가장 중요한 것은 이름 규칙을 정하는 것이다. 이름 스타일을 통해 각 요소가 타입, 변수, 함수, 상수, 매크로 등 어떤 종류인지 선언을 찾지 않고 바로 알 수 있다. 우리 머릿속에 있는 패턴 일치 엔진은 이러한 이름 규칙에 매우 많이 의존한다.

이름 규칙은 꽤 임의적이지만 개인적인 선호보다는 일관성이 더 중요하다고 생각한다. 그러므로 그 것이 합리적이라고 생각하든 그렇지 않든 규칙은 규칙이다.

일반적인 이름 규칙(General Naming Rules)

함수 이름, 변수 이름, 그리고 파일 이름은 설명적이어야 하므로 축약하지 않는다. 타입과 변수는 명사로, 함수는 ‘명령’ 동사로 쓴다.

이름 짓는 방법

이름은 가능한 이유를 담아 설명적이어야 한다. 코드를 즉시 이해하는 게 더 중요하므로 수평 공간을 절약하는 것으로 걱정하지 말라. 다음은 잘 선택한 이름 예이다.

int num_errors;                  // Good.
int num_completed_connections;   // Good.

의미를 담지 않고 모호하게 축약하거나 임의 문자를 이름으로 쓰는 것은 잘못된 선택이다.

int n;                           // Bad - meaningless.
int nerr;                        // Bad - ambiguous abbreviation.
int n_comp_conns;                // Bad - ambiguous abbreviation.

타입과 변수 이름은 FileOpener, num_errors처럼 전형적으로 명사여야 한다.

함수 이름은 OpenFile(), set_num_errors()처럼 전형적으로 명령적이어야 한다(즉 명령이어야 한다). 예외로 접근자는 접근하는 변수 이름과 같아야 한다. 이에 대해서는 함수 이름에서 더 자세히 설명한다.

약어

프로젝트 외에서 잘 알려져 있지 않으면 약어를 사용하지 않는다. 예를 들면 다음과 같다.

// Good
// These show proper names with no abbreviations.
int num_dns_connections;  // Most people know what "DNS" stands for.
int price_count_reader;   // OK, price count. Makes sense.
// Bad!
// Abbreviations can be confusing or ambiguous outside a small group.
int wgc_connections;  // Only your group knows what this stands for.
int pc_reader;        // Lots of things can be abbreviated "pc".

절대로 글자를 빠뜨려 축약하지 않아야 한다.

int error_count;  // Good.
int error_cnt;    // Bad.

파일 이름(File Names)

파일 이름은 모두 소문자여야 하고 밑줄(_)이나 대시(-)를 포함할 수 있다. 어느 것을 사용할지는 프로젝트 관례를 따른다. 항상 따라야 할 형식은 없지만 ‘_’을 선호한다[7].

사용할 수 있는 파일 이름 예는 다음과 같다.

my_useful_class.cc
my-useful-class.cc
myusefulclass.cc
myusefulclass_test.cc // _unittest and _regtest are deprecated.

C++ 파일은 .cc로[8], 헤더 파일은 .h로 마쳐야 한다.

db.h처럼 /usr/include에 이미 있는 파일 이름은 사용하지 않는다.

일반적으로 파일 이름은 매우 한정적으로 만든다. 예를 들어 logs.h보다는 http_server_logs.h를 사용한다. FooBar 클래스를 정의하는데 foo_bar.h와 foo_bar.cc처럼 파일이 쌍으로 있는 경우는 매우 일반적이다.

인라인 함수는 반드시 .h 파일에 있어야 한다. 인라인 함수가 매우 짧으면 .h 파일에 바로 넣는다. 하지만 코드가 많으면 –inl.h로 끝나는 별도 파일에 넣는다. 즉 클래스에 인라인 코드가 많으면 해당 클래스는 다음 세 파일로 구성할 수 있다.

url_table.h      // The class declaration.
url_table.cc     // The class definition.
url_table-inl.h  // Inline functions that include lots of code.

-inl.h 파일 부분도 참조한다.

타입 이름(Type Names)

타입 이름은 대문자로 시작하고 새 단어를 시작할 때마다 대문자를 쓰며, 밑줄은 사용하지 않는다. 예는 다음과 같다. MyExcitingClass, MyExcitingEnum.

클래스, 구조체, 타입 정의, 열거형과 같은 모든 타입 이름은 이름 규칙이 같다. 타입 이름은 대문자로 시작하고 새 단어를 시작할 때마다 대문자를 쓰며, 밑줄은 사용하지 않는다. 예를 보자.

// classes and structs
class UrlTable { ...
class UrlTableTester { ...
struct UrlTableProperties { ...

// typedefs
typedef hash_map PropertiesMap;

// enums
enum UrlTableErrors { ...

변수 이름(Variable Names)

변수 이름은 모두 소문자이며 단어 사이에 밑줄을 쓴다. 클래스 멤버 변수는 마지막에 밑줄을 붙인다. 예를 들면, my_exciting_local_variable, my_exciting_member_variable_.

일반적인 변수 이름

예는 다음과 같다.

string table_name;  // OK - uses underscore.
string tablename;   // OK - all lowercase.
string tableName;   // Bad - mixed case.

클래스 데이터 멤버

데이터 멤버(인스턴스 변수 또는 멤버 변수라고도 한다)는 소문자이며 일반 변수 이름처럼 밑줄을 사용할 수도 있다. 하지만 항상 밑줄을 끝에 붙인다.

string table_name_;  // OK - underscore at end.
string tablename_;   // OK.

구조체 변수

구조체 데이터 멤버 이름 규칙은 일반 변수와 같으나 클래스 데이터 멤버와 달리 끝에 밑줄을 붙이지 않는다.

struct UrlTableProperties {
  string name;
  int num_entries;
}

구조체와 클래스를 각각 언제 사용할지에 대한 논의는 구조체 대 클래스를 참조한다.

전역 변수

전역 변수에 대한 특별한 요구 사항은 없다. 어떤 경우든 사용하지 않아야 하나 만약 사용한다면 g_ 접두어를 붙이거나 지역 변수와 쉽게 구별할 표시를 한다[9].

상수 이름(Constant Names)

k로 시작하고 대소문자를 섞어 쓴다. kDaysInAWeek.

컴파일 시점에 결정되는 모든 상수는 지역 또는 전역으로 선언하든, 클래스 일부분으로 선언하든 관계 없이 다른 변수와 약간 다른 이름 규칙을 따른다. 각 단어는 대문자로 시작하고 k를 가장 앞에 붙인다.

const int kDaysInAWeek = 7;

함수 이름(Function Names)

일반 함수는 대소문자를 섞어 쓰며 접근자와 변경자는 변수 이름과 일치시킨다. 즉 MyExcitingFunction(), MyExcitingMethod(), my_exciting_member_variable(), set_my_exciting_member_variable().

일반 함수

함수는 대문자로 시작하고 새 단어를 시작할 때마다 대문자를 쓴다. 밑줄은 사용하지 않는다.

오류가 발생할 때 함수가 충돌한다면 함수 이름에 OrDie를 붙인다. 이는 제품 코드에 사용할 수 있으나 일반적인 동작 중에 그런 문제가 발생할 것 같은 함수에만 사용한다.

AddTableEntry();
DeleteUrl();
OpenFileOrDie();

접근자와 변경자

접근자와 변경자(get과 set 함수)는 접근하고 설정하는 변수 이름과 일치시킨다. 다음은 인스턴스 변수 이름이 num_entries_일 때 예를 보여준다.

class MyClass {
 public:
  ...
  int num_entries() const { return num_entries_; }
  void set_num_entries(int num_entries) { num_entries_ = num_entries; }

 private:
  int num_entries_;
};

매우 짧은 인라인 함수에는 소문자를 사용할 수도 있다. 예를 들어 매우 값싼 연산을 하므로 루프에서 호출하더라도 그 값을 보관하지 않는 함수라면 소문자를 사용해도 된다.

네임스페이스 이름(Namespace Names)

네임스페이스 이름은 모두 소문자이고 프로젝트 이름에 기초하며 가능하면 디렉터리 구조도 바탕으로 한다. 즉 google_awesome_project.

네임스페이스와 이름 짓는 방법에 대해서는 네임스페이스를 참조한다.

열거형 이름(Enumerator Names)

열거형은 상수 또는 매크로 이름 규칙 중 하나를 선택한다. 즉 kEnumName 또는 ENUM_NAME 중 하나를 선택한다.

열거자 각각은 상수처럼 이름 짓는 것을 선호하지만 매크로처럼 할 수도 있다. 열거형 이름 UrlTableErrors와 AlternateUrlTableErrors는 타입이므로 대소문자를 섞어 쓴다.

enum UrlTableErrors {
  kOK = 0,
  kErrorOutOfMemory,
  kErrorMalformedInput,
};
enum AlternateUrlTableErrors {
  OK = 0,
  OUT_OF_MEMORY = 1,
  MALFORMED_INPUT = 2,
};

2009년 1월까지는 열거 값 이름을 매크로처럼 했는데 열거 값과 매크로에 이름 충돌이 생겼다. 그러므로 상수 이름 규칙을 선호한다. 새 코드에서는 가능하면 상수 이름 규칙을 우선하지만 실제 컴파일 문제가 생기지 않는다면 이전 코드를 상수 이름 규칙으로 바꿀 필요는 없다[10].

매크로 이름(Macro Names)

여러분은 정말로 매크로를 정의하지 않을 거다, 그렇지 않나? 만약 해야 한다면 다음처럼 한다.

MY_MACRO_THAT_SCARES_SMALL_CHILDREN

매크로에 대한 설명을 꼭 보길 바란다. 일반적으로 매크로는 사용하지 않아야 한다. 하지만 절대적으로 필요하다면 이름에는 모두 대문자와 밑줄을 쓴다.

#define ROUND(x) ...
#define PI_ROUNDED 3.0

이름 규칙에 대한 예외(Exceptions to Naming Rules)

기존 C 또는 C++ 요소와 비슷한 무언가에 이름을 짓는다면 기존 이름 규칙을 따를 수 있다.

bigopen()

open() 형식을 따르는 함수 이름

uint

typedef

bigpos

pos 형식을 따르는 struct 또는 class

sparse_hash_map

STL 이름 규칙을 따르는 STL 같은 요소

LONGLONG_MAX

INT_MAX처럼 상수

주석문(Comments)

글 쓰는 게 고통스러울지라도 주석문은 코드 가독성을 좋게 유지하는데 절대적으로 중요하다. 다음 규칙에서는 무엇을, 어디에 적어야 하는지 설명한다. 하지만 기억해 둘 것은, 주석문이 아무리 중요할지라도 좋은 코드는 그 자체로 문서라는 점이다. 타입과 변수에 적절한 이름을 사용하는 것이 불명확한 이름을 사용해 그에 대한 설명을 해야 하는 것보다 더 좋다.

주석문을 적을 때는 청자, 즉 코드를 이해해야 하는 다음 기여자를 생각한다. 관대해져라 – 다음은 자신일 수 있다!

주석문 형식(Comment Style)

// 또는 /* */ 문법 중 하나를 계속해 사용한다.

// 또는 /* */ 문법 중 하나를 사용할 수 있다. 하지만 //가 훨씬 더 일반적이다. 주석문을 적는 방법과 사용하는 형식을 일관되게 유지한다[11].

파일 주석문(File Comments)

각 파일은 저작권 고지로 시작해 파일 내용에 대한 설명을 다음에 적는다.

법적 고지와 저작자 줄

모든 파일에는 다음 항목이 순서대로 있어야 한다.

  • 저작권 문구 (예를 들면 Copyright 2008 Google Inc.)
  • 기반 라이선스. 프로젝트에 사용한 적절한 기반 라이선스를 선택한다. (예를 들면 Apache 2.0, BSD, LGPL, GPL)
  • 해당 파일을 처음 만든 저작자를 표시하는 저작자 줄

누군가가 처음 만든 파일 내용을 자신이 크게 바꾼다면 저작자 줄에 자신을 추가한다. 이는 다른 기여자가 해당 파일에 대해 질문하거나 그 내용에 대해 누구를 만나야 할지 많은 도움이 된다.

파일 내용

모든 파일에는 저작권 고지와 저작자 줄 다음에 파일 내용을 설명하는 주석문이 있어야 한다.

일반적으로 .h 파일에서는 해당 파일에서 선언한 클래스의 목적과 사용 방법에 대한 개략적인 설명과 함께 그 클래스를 기술한다. .cc 파일에는 상세 구현 내용이나 기술적인 알고리즘에 대해 많은 내용을 담아야 한다. 상세 구현 내용이나 알고리즘에 대한 설명이 .h 파일을 읽을 때 유용하다고 생각하면 그곳으로 옮겨도 되지만, 대신 .h 파일에 문서화 내용이 있다는 것을 .cc 파일에 언급해 둔다.

.h와 .cc 파일 양쪽에 같은 주석문을 두지 않는다. 결국에는 서로 달라진다.

클래스 주석문(Class Comments)

모든 클래스 정의에는 해당 클래스의 목적과 사용 방법을 설명하는 관련 주석문이 있어야 한다.

// Iterates over the contents of a GargantuanTable.  Sample usage:
//    GargantuanTableIterator* iter = table->NewIterator();
//    for (iter->Seek("foo"); !iter->done(); iter->Next()) {
//      process(iter->key(), iter->value());
//    }
//    delete iter;
class GargantuanTableIterator {
  ...
};

파일 상단 주석문에서 클래스에 대해 이미 설명을 했다면 ‘자세한 설명은 파일 상단 주석문을 참조한다.’라는 내용으로 자유롭게 간단히 적는다.

클래스를 만들 때 한 동기화 가정이 있으면 문서화한다. 여러 스레드에서 해당 클래스 인스턴스에 접근한다면 다중 스레드 환경에서 규칙과 불변 내용에 대해 주의를 더 기울여 문서화한다.

함수 주석문(Function Comments)

선언 주석문에서는 함수 사용을 설명하고 함수 정의에서 주석문은 연산을 설명한다.

함수 선언

모든 함수 선언에는 바로 앞에 해당 함수의 목적과 사용 방법을 설명하는 주석문이 있어야 한다. 이 주석문는 명령적(‘파일을 열어라’)이기보다 설명적(‘파일을 연다’)이어야 한다. 즉 이 주석문은 함수를 설명하는 것이지 할 일을 함수에 알리는 것이 아니다. 일반적으로 이 주석문은 함수가 일을 어떻게 처리하는지 설명하지 않는다. 그런 내용은 함수 정의에서 사용하는 주석문에 둔다.

다음은 함수 선언에 있는 주석문에서 언급해야 할 것들이다.

  • 입력과 출력은 무엇인지
  • 클래스 멤버 함수에 대한 내용. 즉 메소드 호출 기간을 지나서도 참조 인자를 해당 객체에서 기억해야 하는지 아니면 해제할지 말지 등.
  • 호출하는 쪽에서 꼭 해제해야 하는 메모리를 해당 함수에서 할당하는지
  • 인자 중 어느 것이라도 NULL이 될 수 있는지
  • 함수 사용 방법에 따라 성능에 영향이 있는지
  • 함수에 재진입 할 수 있는지. 동기화 가정은 무엇인지.

다음은 그 예이다.

// Returns an iterator for this table.  It is the client's
// responsibility to delete the iterator when it is done with it,
// and it must not use the iterator once the GargantuanTable object
// on which the iterator was created has been deleted.
//
// The iterator is initially positioned at the beginning of the table.
//
// This method is equivalent to:
//    Iterator* iter = table->NewIterator();
//    iter->Seek("");
//    return iter;
// If you are going to immediately seek to another place in the
// returned iterator, it will be faster to use NewIterator()
// and avoid the extra seek.
Iterator* GetIterator() const;

하지만 필요 이상으로 장황하거나 아주 분명한 것을 적지 않도록 한다. 다음에서는 ‘조건에 맞지 않으면 거짓을 반환한다’는 것을 알 수 있으므로 그런 내용을 적을 필요 없다.

// Returns true if the table cannot hold any more entries.
bool IsTableFull();

생성자와 소멸자에 주석을 달 때는, 생성자와 소멸자가 무엇을 위한 것인지는 알고 있으므로 단순히 ‘이 객체를 소멸한다’라는 식으로 적는 건 쓸모 없다. (예를 들어 인자에서 포인터 소유권을 취한다면) 생성자에서 그 인자로 무엇을 하는지, 소멸자에서 무엇을 제거하는지를 문서화한다. 이런 내용이 중요하지 않으면 문서화하지 않는다. 소멸자 앞에 주석문을 두지 않는 것은 꽤 일반적이다.

함수 정의

각 함수 정의에는 함수에서 일을 처리하는 방법에 관한 기술적인 무언가가 있으면 그에 대해 설명하는 주석문이 있어야 한다. 즉 정의 주석문에서는 사용하는 코딩 기술, 거쳐야 할 단계에 대한 개략적인 내용 또는 다른 방법 대신 왜 그 방법으로 구현했는지 설명할 수 있다. 예를 들어 함수 전반부 동안은 락을 걸어야 하지만 왜 후반부 동안은 그럴 필요가 없는지를 언급할 수 있다.

단순히 .h 파일 등 함수 선언에서 적은 내용을 반복하지 않도록 주의해야 한다. 함수가 하는 일을 간략히 요약하는 것은 괜찮으나 주석문 내용은 어떻게 처리하는지에 초점을 맞춰야 한다.

변수 주석문(Variable Comments)

일반적으로 실제 변수 이름은 해당 변수를 무엇을 위해 사용하는지 잘 알 수 있도록 충분히 설명적이어야 한다. 어떤 경우에는 설명을 더 해야 할 수도 있다.

클래스 데이터 멤버

각 클래스 데이터 멤버(인스턴스 변수 또는 멤버 변수라도고 한다)는 무엇을 위해 사용하는지 설명하는 주석문이 있어야 한다. 만약 변수에서 NULL 또는 -1과 같이 특별한 뜻이 있는 경계값을 취한다면 그 내용을 문서화한다. 예는 다음과 같다.

private:
 // Keeps track of the total number of entries in the table.
 // Used to ensure we do not go over the limit. -1 means
 // that we don't yet know how many entries the table has.
 int num_total_entries_;

전역 변수

데이터 멤버와 마찬가지로 모든 전역 변수에는 그 변수가 무엇이고 무엇을 위해 사용하는지 설명하는 주석문이 있어야 한다. 예를 보자.

// The total number of tests cases that we run through in this regression test.
const int kNumTestCases = 6;

구현 주석문(Implementation Comments)

구현 내용에서 기술적이고 분명하지 않고 흥미롭거나 중요한 부분에는 주석문이 있어야 한다.

클래스 데이터 멤버

기술적이거나 복잡한 코드 내용 앞에는 주석문을 달아야 한다. 예를 보자.

// Divide result by two, taking into account that x
// contains the carry from the add.
for (int i = 0; i < result->size(); i++) {
  x = (x << 8 ) + (*result)&#91;i&#93;;
  (*result)&#91;i&#93; = x >> 1;
  x &= 1;
}

줄 주석문

또한 분명하지 않은 줄에는 해당 줄 끝에 주석문을 달아야 한다. 이 줄 끝 주석문은 코드에서 2 칸을 띄우고 적는다. 예는 다음과 같다.


// If we have enough memory, mmap the data portion too.
mmap_budget = max<int64>(0, mmap_budget - index_->length());
if (mmap_budget >= data_size_ && !MmapData(mmap_chunk_bytes, mlock))
  return;  // Error already logged.

코드가 하는 일을 설명하는 주석문과 함수를 반환할 때 오류를 이미 기록해 뒀다고 설명하는 주석문이 모두 있다는 것에 주목한다.

이어지는 다음 줄에 주석문을 더 적어야 할 때 줄을 맞추면 가독성이 더 좋아진다.

DoSomething();                  // Comment here so the comments line up.
DoSomethingElseThatIsLonger();  // Comment here so there are two spaces between
                                // the code and the comment.
{ // One space before comment when opening a new scope is allowed,
  // thus the comment lines up with the following comments and code.
  DoSomethingElse();  // Two spaces before line comments normally.
}

NULL, true/false, 1, 2, 3…

함수에 NULL, 부울 또는 정수 숫자를 전달할 때는 그에 대해 주석을 추가하거나 상수를 사용해 문서화해야 한다. 다음 예를 보고 비교해 보자.

bool success = CalculateSomething(interesting_value,
                                  10,
                                  false,
                                  NULL);  // What are these arguments??

bool success = CalculateSomething(interesting_value,
                                  10,     // Default base value.
                                  false,  // Not the first time we're calling this.
                                  NULL);  // No callback.

또는 다른 방법으로 상수나 그 자체로 설명적인 변수를 사용한다.

const int kDefaultBaseValue = 10;
const bool kFirstTimeCalling = false;
Callback *null_callback = NULL;
bool success = CalculateSomething(interesting_value,
                                  kDefaultBaseValue,
                                  kFirstTimeCalling,
                                  null_callback);

하지 말아야 할 것

절대 코드 그 자체를 설명하는 일은 하지 않아야 한다. 코드를 읽는 이가 자신이 하려는 일을 잘 모를지라도 자신보다 C++를 더 잘 안다고 가정한다.

// Now go through the b array and make sure that if i occurs,
// the next element is i+1.
...        // Geez.  What a useless comment.

구두점, 맞춤법과 문법(Punctuation, Spelling and Grammer)

구두점, 맞춤법과 문법에 주의한다. 잘 못 쓴 주석문보다는 잘 쓴 것이 더 읽기 쉽다.

일반적으로 주석문은 대문자를 올바로 쓰고 마침표로 마치는 완전한 문장으로 쓴다. 코드 줄 끝에 쓰는 주석문처럼 짧은 것을 쓸 때는 간혹 덜 형식적일 수 있지만 일관된 형식을 유지해야 한다. 완전한 문장은 더 읽기 쉽고 그에 대한 언급을 마쳤으므로 생각이 완전하다는 어떤 확신을 준다.

비록 코드 검수자가 세미콜론를 써야 할 곳에 쉼표를 썼다고 지적해 기가 꺾일지라도 소스 코드의 명확성과 가독성을 높은 수준으로 유지하는 것은 매우 중요하다. 올바른 구두점, 맞춤법 그리고 문법은 이런 목표를 이루는데 도움이 된다.

TODO 주석문(TODO Comments)

임시로 잠시 동안 해결책으로 사용할 코드나 충분히 좋긴 하지만 완전하지 않은 코드에는 TODO 주석문을 사용한다.

TODO 주석문에는 모두 대문자로 쓴 TODO 문자열, TODO 내용에 관련된 문제에 대한 상황을 가장 잘 알려줄 수 있는 이의 이름, 이메일 주소 또는 누군지 식별할 수 있는 다른 내용이 있어야 한다. TODO 형식을 일관되게 유지하는 주된 이유는 요청에 대해 더욱 자세한 내용을 알려줄 수 있는 이를 검색해 찾을 수 있도록 하기 위해서이다. TODO는 언급된 이가 그 문제를 해결할 거라는 공약이 아니다. 따라서 TODO를 만들면 거의 항상 자신의 이름을 적는다.

// TODO([email protected]): Use a "*" here for concatenation operator.
// TODO(Zeke) change this to use relations.

TODO를 ‘미래에 무언가를 할 것’이라는 형식으로 만든다면 정확한 날짜(‘2005년 11월까지 수정’)나 정확한 시기(‘모든 클라이언트가 XML 응답을 처리할 수 있을 때 이 코드를 삭제’) 중 하나를 포함했는지 확인한다.

비추천 주석문(Deprecation Comments)

비추천 인터페이스는 DEPRECATED 주석문으로 표시한다.

모두 대문자로 쓴 DEPRECATED란 단어를 주석문에 포함해 앞으로 비추천 인터페이스로 표시할 수 있다. 이 주석문은 해당 인터페이스 선언 앞 또는 그 선언과 같은 줄에 둔다.

DEPRECATED 다음에는 이름과 이메일 주소 또는 누군지 식별할 수 있는 다른 내용을 괄호 안에 적는다.

비추천 주석문에는 다른 이가 호출 내용을 수정할 수 있도록 간단하고 명확한 지시 내용을 포함해야 한다. C++에서는 새 인터페이스를 호출하는 인라인 함수로 비추천 함수를 구현할 수 있다.

인터페이스 지점을 DEPRECATED로 표시한다고 해서 마법처럼 호출 내용이 바뀌는 것은 아니다. 비추천하는 기능을 실제로 사용하지 않기를 원한다면 호출 내용을 직접 수정하거나 도와줄 팀원을 구해야 할 것이다.

새로 만드는 코드에서는 비추천 인터페이스를 호출하지 않고 대신 새 인터페이스를 사용해야 한다. 지시 내용을 이해할 수 없으면 비추천 내용을 적은 이를 찾아 새 인터페이스를 사용할 수 있도록 도움을 요청한다.

형식(Formatting)

코드 스타일과 형식은 꽤 임의적이지만 모든 이가 같은 스타일을 사용하면 프로젝트는 훨씬 쉬워진다. 모든 이가 모든 형식 규칙에 동의하지 않을 수 있고 규칙 중 일부는 다소 익숙할 수도 있다. 하지만 중요한 것은 프로젝트의 모든 기여자가 같은 스타일을 따라야 다른 이가 만든 코드를 쉽게 읽고 이해할 수 있다는 점이다.

올바른 형식으로 코드를 만드는데 도움이 되도록 이맥스 설정 파일을 만들었다.

줄 길이(Line Length)

코드 각 줄은 최대 80 문자를 넘지 않아야 한다.

이 규칙에 논쟁의 여지가 있다는 건 알지만 많은 기존 코드가 이미 이를 따르고 일관성이 중요하다고 생각한다.

장점:

이 규칙을 지지하는 이들은 창 크기를 조절하도록 강제하고 어느 것도 그 제한을 넘지 않도록 하는 게 간편하다고 주장한다. 일부는 코드 창을 나란히 띄워 사용하므로 창을 더 넓게 배치할 공간이 없다. 사람들은 자신의 작업 환경을 구성할 때 최대 창 폭을 특정 값으로 가정하며 80 열은 전통적으로 표준이다. 바꿀 이유가 있을까?

단점:

바꿀 것을 지지하는 쪽은 줄 폭이 더 넓으면 가독성이 더 좋아진다고 주장한다. 80 열 제한은 완고했던 1960년대 메인프레임 시절로 돌아가는 것이다. 현대 장비에는 더 긴 줄 내용을 넓은 화면에 쉽게 보여줄 수 있는 넓은 화면이 있다.

결론:

80 문자가 최대값이다.

예외: 주석문에서 예로 든 명령이나 URL이 80 문자를 넘으면 쉽게 잘라 붙일 수 있도록 해당 줄은 80 문자를 넘을 수 있다.

예외: 경로가 길면 #include 문은 80 열을 넘을 수 있다. 이런 상황은 가능한 피하도록 한다.

예외: 헤더 보호문은 최대 길이를 넘는 것을 생각할 필요 없다.

비아스키 문자(Non-ASCII Characters)

비아스키 문자는 사용하지 말고 반드시 UTF-8 형식을 사용해야 한다.

사용자가 보는 내용이 영어라 하더라도 소스 코드에 직접 넣지 말아야 한다. 그러므로 비아스키 문자는 극히 사용하지 말아야 한다. 하지만 어떤 경우엔 코드에 그런 단어를 넣어야 할 수도 있다. 예를 들어 외국어로 된 내용에서 데이터를 파싱하는 코드가 있을 때 구분자로 사용할 비아스키 문자를 코드에 직접 입력하는 게 적절할 수도 있다. 더 일반적으로는 (지역화할 필요가 없는) 단위 테스트 코드에 비아스키 문자열이 있을 수도 있다. 그런 경우에는 UTF-8을 사용해야 한다. UTF-8은 아스키 외 문자도 처리할 수 있는 툴 대부분에서 인식하는 인코딩이기 때문이다. 16 진수 인코딩 역시 좋으며 가독성을 높일 수 있을 때는 사용하길 권한다. 예를 들어 “\xEF\xBB\xBF”는 유니코드 BOM 문자로 UTF-8 인코딩 문서에서 그 문자는 보이지 않는다.

공백 문자 대 탭(Spaces vs. Tabs)

공백 문자만 사용하며 2 칸 들여 쓴다.

들여쓰기에는 공백 문자를 쓰며 탭은 사용하지 않는다. 사용하는 편집기에서 탭을 공백 문자로 바꾸도록 설정한다.

함수 선언과 정의(Function Declarations and Definitions)

함수 반환 타입은 함수 이름과 같은 줄에 쓰고 매개변수 역시 공간이 맞으면 그렇게 한다.

함수 모습은 다음과 같다.

ReturnType ClassName::FunctionName(Type par_name1, Type par_name2) {
  DoSomething();
  ...
}

한 줄에 쓸 내용이 너무 많으면

ReturnType ClassName::ReallyLongFunctionName(Type par_name1, Type par_name2,
                                             Type par_name3) {
  DoSomething();
  ...
}

또는

ReturnType LongClassName::ReallyReallyReallyLongFunctionName(
    Type par_name1,  // 4 space indent
    Type par_name2,
    Type par_name3) {
  DoSomething();  // 2 space indent
  ...
}

처럼 한다[12].

몇 가지 주의할 내용은 다음과 같다.

  • 반환 타입은 항상 함수 이름과 같은 줄에 둔다.
  • 여는 괄호는 항상 함수 이름과 같은 줄에 둔다.
  • 함수 이름과 여는 괄호 사이에 절대 빈칸을 두지 않는다.
  • 괄호와 매개변수 사이에 절대 빈칸을 두지 않는다.
  • 여는 중괄호는 항상 마지막 매개변수와 같은 줄 끝에 둔다.
  • 닫는 중괄호는 함수 마지막 줄로 하거나 (다른 규칙에서 허용하면) 여는 중괄호와 같은 줄에 둔다.
  • 닫는 괄호와 여는 중괄호 사이는 한 칸 띄운다.
  • 모든 매개변수에는 이름이 있어야 하고 그 이름은 선언과 구현에서 같아야 한다.
  • 모든 매개변수는 가능하면 줄을 맞춘다.
  • 들여쓰기 기본값은 2 칸이다.
  • 포함한 매개변수는 4 칸 들여쓰기 한다.

함수가 const일 때 이 키워드는 마지막 매개변수와 같은 줄에 둔다.

// Everything in this function signature fits on a single line
ReturnType FunctionName(Type par) const {
  ...
}

// This function signature requires multiple lines, but
// the const keyword is on the line with the last parameter.
ReturnType ReallyLongFunctionName(Type par1,
                                  Type par2) const {
  ...
}

사용하지 않는 매개변수가 일부 있으면 그 변수 이름은 함수 정의에서 주석 처리한다.

// Always have named parameters in interfaces.
class Shape {
 public:
  virtual void Rotate(double radians) = 0;
}

// Always have named parameters in the declaration.
class Circle : public Shape {
 public:
  virtual void Rotate(double radians);
}

// Comment out unused named parameters in definitions.
void Circle::Rotate(double /*radians*/) {}
// Bad - if someone wants to implement later, it's not clear what the
// variable means.
void Circle::Rotate(double) {}

함수 호출(Function Calls)

공간이 맞으면 한 줄에 쓰고 그렇지 않으면 인자를 괄호로 감싼다.

함수 호출 형식은 다음과 같다.

bool retval = DoSomething(argument1, argument2, argument3);

모든 인자를 한 줄에 넣을 수 없으면 여러 줄로 나누며 첫 번째 인자와 줄을 맞춘다[13]. 여는 괄호 다음이나 닫은 괄호 앞에 빈 칸을 두지 않는다.

bool retval = DoSomething(averyveryveryverylongargument1,
                          argument2, argument3);

함수에 인자가 많을 때 줄 당 하나씩 두는 게 가독성이 더 좋으면 그렇게 한다.

bool retval = DoSomething(argument1,
                          argument2,
                          argument3,
                          argument4);

함수 시그니처가 길어 최대 줄 길이 제한 안에 넣을 수 없으면 모든 인자를 각각의 줄로 나눌 수 있다.

if (...) {
  ...
  ...
  if (...) {
    DoSomethingThatRequiresALongFunctionName(
        very_long_argument1,  // 4 space indent
        argument2,
        argument3,
        argument4);
}

조건(Conditionals)

괄호 안에 빈 칸을 두지 않으며 else 키워드는 다음 줄에 쓴다.

기본적인 조건문 형식은 두 가지를 쓸 수 있다. 괄호와 조건 사이에 빈 칸을 쓰는 것과 그렇지 않은 것이다.

가장 일반적인 형식은 빈 칸을 쓰지 않는 것이다. 어느 것이든 괜찮으나 일관되게 한다[14]. 파일을 변경할 때는 기존에 쓰는 형식을 사용하며, 새 코드를 작성할 때는 프로젝트나 그 디렉터리에 있는 다른 파일에서 사용하는 형식을 사용한다[15]. 뭘 써야 할지 잘 모르거나 개인적으로 선호하는 것이 없으면 빈 칸을 두지 않는다.

if (condition) {  // no spaces inside parentheses
  ...  // 2 space indent.
} else {  // The else goes on the same line as the closing brace.
  ...
}

괄호 안에 빈 칸 두는 걸 좋아하면 다음처럼 한다.

if ( condition ) {  // spaces inside parentheses - rare
  ...  // 2 space indent.
} else {  // The else goes on the same line as the closing brace.
  ...
}

어떤 경우든 if와 여는 괄호 사이, 닫는 괄호와 (사용한다면) 중괄호 사이는 한 칸 띄워야 하는 것에 주의한다.

if(condition)     // Bad - space missing after IF.
if (condition){   // Bad - space missing before {.
if(condition){    // Doubly bad.
if (condition) {  // Good - proper space after IF and before {.

짧은 조건문은 한 줄에 쓰는 게 가독성이 좋아지면 그렇게 쓸 수 있다. 이는 내용이 간략하고 else 절을 사용하지 않을 때만 사용할 수 있다.

if (x == kFoo) return new Foo();
if (x == kBar) return new Bar();

조건문에 else를 사용하면 허용하지 않는다.

// Not allowed - IF statement on one line when there is an ELSE clause
if (x) DoThis();
else DoThat();

일반적으로 조건문 다음 문장이 한 줄일 때는 중괄호를 쓰지 않아도 된다. 하지만 쓰고 싶으면 그렇게 해도 된다. 조건문이나 루프문에 조건이 복잡하거나 실행할 구문에 중괄호를 사용해 가독성을 더 높일 수 있다. 일부 프로젝트에서는 if에 중괄호를 항상 사용한다.

if (condition)
  DoSomething();  // 2 space indent.

if (condition) {
  DoSomething();  // 2 space indent.
}

하지만 if-else 문에서 한 부분에 중괄호를 사용하면 다른 부분에서도 써야 한다.

// Not allowed - curly on IF but not ELSE
if (condition) {
  foo;
} else
  bar;

// Not allowed - curly on ELSE but not IF
if (condition)
  foo;
else {
  bar;
}
// Curly braces around both IF and ELSE required because
// one of the clauses used braces.
if (condition) {
  foo;
} else {
  bar;
}

루프와 switch 문(Loops and Switch Statements)

switch 문 각 구역에 괄호를 쓸 수 있다. 루프 본체가 비었을 때는 {} 또는 continue를 사용한다.

switch 문에서 case 구역에는 기호에 따라 중괄호를 쓰거나 그렇지 않을 수 있다. 중괄호를 쓴다면 아래처럼 해야 한다. 열거값에 해당하지 않는 조건이 있을 때는 항상 switch 문에 default를 둬야 한다. default 경우가 절대 실행되지 않아야 한다면 간단히 assert를 사용한다.

switch (var) {
  case 0: {  // 2 space indent
    ...      // 4 space indent
    break;
  }
  case 1: {
    ...
    break;
  }
  default: {
    assert(false);
  }
}

루프 본체가 비었을 때는 {}나 continue를 사용하며 세미콜론 하나만 사용하지 않는다.

while (condition) {
  // Repeat test until it returns false.
}
for (int i = 0; i < kSomeNumber; ++i) {}  // Good - empty body.
while (condition) continue;  // Good - continue indicates no logic.
&#91;/sourcecode&#93;
&#91;sourcecode language="cpp"&#93;
while (condition);  // Bad - looks like part of do/while loop.
&#91;/sourcecode&#93;
<h3></h3>
<h3><strong>포인터와 참조자 표현식(Pointer and Reference Expressions)</strong></h3>
마침표나 화살표 주위에는 빈 칸을 두지 않는다. 포인터 연산자 다음에는 빈 칸을 두지 않는다.

다음은 포인터와 참조자 표현식을 올바른 형식으로 사용한 예이다.


x = *p;
p = &x;
x = r.y;
x = r->y;

다음을 주의한다.

  • 멤버에 접근할 때 마침표나 화살표 주위에 빈 칸을 두지 않는다.
  • 포인터 연산자에서 *와 & 다음에 빈 칸을 두지 않는다.

포인터 변수나 인자를 선언할 때는 *를 타입이나 변수 이름에 붙일 수 있다.

// These are fine, space preceding.
char *c;
const string &str;

// These are fine, space following.
char* c;    // but remember to do "char* c, *d, *e, ...;"!
const string& str;
char * c;  // Bad - spaces on both sides of *
const string & str;  // Bad - spaces on both sides of &

같은 파일 내에서는 일관되게 사용해야 하므로 기존 파일을 수정할 때는 사용하던 형식을 따른다[16].

부울 표현식(Boolean Expressions)

부울 표현식이 표준 줄 길이보다 더 길 때는 일관된 방식으로 줄을 나눈다.

다음 예에서는 논리곱 연산자를 항상 줄 끝에 둔다.

if (this_one_thing > this_other_thing &&
    a_third_thing == a_fourth_thing &&
    yet_another && last_one) {
  ...
}

이 예에서는 모든 논리곱 연산자 &&를 줄 끝에 둔 것에 주목한다. 모든 연산자를 줄 시작에 둘 수도 있지만 구글 코드에서는 줄 끝에 두는 게 더 일반적이다. 괄호는 적절히 사용하면 가독성을 높이는데 많은 도움이 되므로 적절히 자유롭게 추가한다. 또한 and와 compl과 같은 단어 연산자보다 &&와 ~같은 구두점 연산자를 항상 사용하는 것에 주의한다.

반환값(Return Values)

필요 없이 return 문을 괄호로 감싸지 않는다.

return expr;에 괄호를 사용하는데 이 때는 x = expr;인 경우에만 사용한다[17].

return result;                  // No parentheses in the simple case.
return (some_long_condition &&  // Parentheses ok to make a complex
        another_condition);     //     expression more readable.
return (value);                // You wouldn't write var = (value);
return(result);                // return is not a function!

변수와 배열 초기화(Variables and Array Initialization)

= 또는 () 중에 선택한다.

=와 () 사이에 선택할 수 있으며 다음 예는 모두 올바르다.

int x = 3;
int x(3);
string name("Some Name");
string name = "Some Name";

전처리기 지시자(Preprocessor Directives)

전처리기 지시자 시작을 나타내는 해시 표시는 항상 줄 처음에 둔다.

전처리기 지시자가 들여쓰기 한 코드 본체 안에 있더라도 그 지시자는 줄 처음에서 시작해야 한다.

// Good - directives at beginning of line
  if (lopsided_score) {
#if DISASTER_PENDING      // Correct -- Starts at beginning of line
    DropEverything();
# if NOTIFY               // OK but not required -- Spaces after #
    NotifyClient();
# endif
#endif
    BackToNormal();
}
// Bad - indented directives
  if (lopsided_score) {
    #if DISASTER_PENDING  // Wrong!  The "#if" should be at beginning of line
    DropEverything();
    #endif                // Wrong!  Do not indent "#endif"
    BackToNormal();
}

클래스 형식(Class Format)

public, protected와 private 순으로 두며 각각은 한 칸 들여 쓴다.

클래스 선언에 대한 기본 형식은 다음과 같다(주석문은 생략하며 어떤 주석문이 필요한지는 클래스 주석문을 참조한다).

class MyClass : public OtherClass {
 public:      // Note the 1 space indent!
  MyClass();  // Regular 2 space indent.
  explicit MyClass(int var);
  ~MyClass() {}

  void SomeFunction();
  void SomeFunctionThatDoesNothing() {
  }

  void set_some_var(int var) { some_var_ = var; }
  int some_var() const { return some_var_; }

 private:
  bool SomeInternalFunction();

  int some_var_;
  int some_other_var_;
  DISALLOW_COPY_AND_ASSIGN(MyClass);
};

주의할 점은 다음과 같다.

  • 모든 기초 클래스 이름은 하위 클래스 이름과 같은 줄에 두며 80 열 제한에 따른다.
  • public:, protected: 그리고 private: 키워드는 한 칸 들여 쓴다.
  • 첫 번째 것을 제외하고 이 키워드 앞에는 빈 줄을 하나 둔다. 클래스가 작을 때 이 규칙은 선택이다.
  • 이 키워드 다음에 빈 줄은 두지 않는다.
  • public 구역을 첫 번째로 하고 protected를 그 다음, 마지막으로 private 구역을 둔다.

각 구역 안에서 선언 순서는 선언 순서 규칙을 참조한다.

생성자 초기화 목록(constructor Initializer Lists)

생성자 초기화 목록은 한 줄에 모두 쓰거나 다음 줄에 4 칸 들여 쓸 수 있다.

초기화 목록 형식은 두 가지로 쓸 수 있다.

// When it all fits on one line:
MyClass::MyClass(int var) : some_var_(var), some_other_var_(var + 1) {}

또는

// When it requires multiple lines, indent 4 spaces, putting the colon on
// the first initializer line:
MyClass::MyClass(int var)
    : some_var_(var),             // 4 space indent
      some_other_var_(var + 1) {  // lined up
  ...
  DoSomething();
  ...
}

네임스페이스 형식(Namespace Formatting)

네임스페이스 안 내용은 들여쓰기 하지 않는다[18].

네임스페이스는 들여쓰기 수준을 추가하지 않는다. 다음 예처럼 쓴다.

namespace {

void foo() {  // Correct.  No extra indentation within namespace.
  ...
}

}  // namespace

네임스페이스 안 내용에 들여쓰기 하지 않는다.

namespace {

  // Wrong.  Indented when it should not be.
  void foo() {
    ...
  }

}  // namespace

중첩 네임스페이스를 선언할 때는 각 네임스페이스를 한 줄로 배정해 쓴다.

namespace foo {
namespace bar {

수평 빈 칸(Horizontal Whitespace)

수평 빈 칸은 위치에 따라 사용하며 줄 끝에는 절대로 빈 칸을 두지 않는다.

일반

void f(bool b) {  // Open braces should always have a space before them.
  ...
int i = 0;  // Semicolons usually have no space before them.
int x[] = { 0 };  // Spaces inside braces for array initialization are
int x[] = {0};    // optional.  If you use them, put them on both sides!
// Spaces around the colon in inheritance and initializer lists.
class Foo : public Bar {
 public:
  // For inline function implementations, put spaces between the braces
  // and the implementation itself.
  Foo(int b) : Bar(), baz_(b) {}  // No spaces inside empty braces.
  void Reset() { baz_ = 0; }  // Spaces separating braces from implementation.
...

줄 끝에 빈 칸을 두면 다른 이가 그 파일을 편집할 때 일을 추가로 해야 할 수 있고, 병합할 때 끝에 붙인 빈 칸이 없어 질 수도 있다. 그러므로 끝에 빈 칸을 두지 않는다. 이미 그렇게 했으면 제거한다.

루프와 조건

if (b) {          // Space after the keyword in conditions and loops.
} else {          // Spaces around else.
}
while (test) {}   // There is usually no space inside parentheses.
switch (i) {
for (int i = 0; i < 5; ++i) {
switch ( i ) {    // Loops and conditions may have spaces inside
if ( test ) {     // parentheses, but this is rare.  Be consistent.
for ( int i = 0; i < 5; ++i ) {
for ( ; i < 5 ; ++i) {  // For loops always have a space after the
  ...                   // semicolon, and may have a space before the
                        // semicolon.
switch (i) {
  case 1:         // No space before colon in a switch case.
    ...
case 2: break;  // Use a space after a colon if there's code after it.
&#91;/sourcecode&#93;

<strong>연산자</strong>


x = 0;              // Assignment operators always have spaces around
                    // them.
x = -5;             // No spaces separating unary operators and their
++x;                // arguments.
if (x && !y)
  ...
v = w * x + y / z;  // Binary operators usually have spaces around them,
v = w*x + y/z;      // but it's okay to remove spaces around factors.
v = w * (x + z);    // Parentheses should have no spaces inside them.

템플릿과 변환

vector<string> x;           // No spaces inside the angle
y = static_cast<char*>(x);  // brackets (< and >), before
                            // <, or between >( in a cast.
vector<char *> x;           // Spaces between type and pointer are
                            // okay, but be consistent.
set<list<string> > x;       // C++ requires a space in > >.
set< list<string> > x;      // You may optionally use
                            // symmetric spacing in < <.
&#91;/sourcecode&#93;
<h3></h3>
<h3><strong>수직 빈 칸(Vertical Whitespace)</strong></h3>
수직 빈 칸은 최소로 사용한다.

필요 없으면 빈 줄을 사용하지 말라. 이는 규칙이 아니라 원칙이다. 특히 함수 사이에 빈 줄은 하나 또는 둘보다 많이 두지 말고, 함수를 빈 줄로 시작하지 않으며 함수 마지막에 빈 줄을 두지 않는다. 함수 안에서 빈 줄은 잘 판단해 사용한다.

기본 원칙은 코드를 한 화면 안에 나오도록 하면 프로그램 제어 흐름을 따라가고 이해하기 더 쉽다는 것이다. 물론 가독성은 너무 밀집해도 반대로 너무 퍼뜨려도 나빠질 수 있으므로 잘 판단해야 한다. 하지만 일반적으로 수직 빈 칸은 최소로 사용한다.

빈 줄을 유용하게 사용하는데 도움이 되는 중요한 규칙은 다음과 같다.

함수 시작과 끝에 빈 줄을 두면 가독성에 극히 도움이 되지 않는다.

연속으로 있는 if-else 구역 안에 빈 줄을 두면 가독성이 많은 도움이 될 수 있다.
<h2></h2>
<h2><strong>이 규칙에 대한 예외(Exceptions to the Rules)</strong></h2>
위에서 설명한 코드 관례는 중요하다. 하지만 모든 좋은 규칙이 그러하듯 때로는 예외도 있으며 여기서 그 내용을 다룬다.
<h3></h3>
<h3><strong>이 스타일에 일치하지 않는 기존 코드(Existing Non-conformant Code)</strong></h3>
이 스타일 안내문에 일치하지 않는 코드를 다룰 때는 이 규칙에 벗어날 수 있다.

이 안내문에 없는 규칙으로 만든 코드를 변경할 때는 해당 코드의 지역적인 관례를 일관되게 따르기 위해 이 규칙을 벗어날 수 있다. 어떻게 해야 할지 잘 모르겠으면 원저작자나 현재 그 코드를 담당하고 있는 개발자에게 문의한다. 지역적인 일관성을 비롯해 <em>일관성</em>을 기억해야 한다.
<h3></h3>
<h3><strong>윈도 코드(Windows Code)</strong></h3>
윈도 프로그래머는 자체 코딩 규칙을 따라 개발해 왔으며 이는 주로 윈도 헤더나 마이크로소프트에서 만든 다른 코드의 관례에서 파생됐다. 자신이 만든 코드를 다른 이가 쉽게 이해할 수 있게 하길 원하므로 어느 플랫폼에서든 C++로 만드는 모든 이가 따를 안내문을 만들었다.

널리 퍼진 윈도 스타일을 이미 사용하고 있다면 잊었을지도 모를 몇 가지 내용을 다시 살펴보는 것도 가치 있는 일이다.
<ul>
	<li>헝가리 표기법(예를 들면 정수 변수를 iNum으로 이름 붙이는 것)은 사용하지 않으며 소스 파일 이름으로 .cc 확장자를 쓰는 것을 비롯해 구글 명명 규칙을 사용한다<a id="ref19" href="#19"><sup>[19]</sup></a>.</li>
	<li>윈도에서는 DWORD, HANDLE 등과 같이 기본 타입에 많은 동의어를 정의한다. 윈도 API 함수를 호출할 때 이런 타입을 사용하는 것은 전혀 문제 없으며 권장한다. 그렇다고 하더라도 해당 C++ 타입에 가능한 가깝게 유지해야 한다. 예를 들어 LPCTSTR 대신 TCHAR const *를 사용한다.</li>
	<li>마이크로소프트 Visual C++로 컴파일 할 때는 컴파일러 경고 수준을 3 이상으로 하고 모든 경고를 오류로 처리하도록 설정한다.</li>
	<li>#pragma once;를 사용하지 않는다. 대신 표준 구글 포함 보호문을 사용한다. 포함 보호문에서 사용하는 경로는 프로젝트 트리 구조의 최상위를 기준으로 상대적으로 지정한다.</li>
	<li>사실 #pragma와 __declspec 등 비표준 확장은 정말로 사용해야 하는 경우가 아니라면 모두 사용하지 않아야 한다. __declspec(dllimport)와 __declspec(dllexport)는 허용하지만 코드를 공유할 때 이런 확장을 쉽게 비활성 할 수 있도록 DLLIMPORT와 DLLEXPORT 같은 매크로로 정의해 사용해야 한다.</li>
</ul>
하지만 윈도에서는 종종 어겨야 하는 규칙도 몇 가지 있다.
<ul>
	<li>일반적으로 다중 구현 상속은 금지하지만 COM과 일부 ATL/WTL 클래스를 사용할 때는 필요하다. COM 또는 ATL/WTL 클래스와 인터페이스를 구현할 때는 다중 구현 상속을 사용할 수 있다.</li>
	<li>비록 코드에 예외를 사용해서는 안 되지만 ATL과 일부 STL, Visual C++에 포함된 코드 등에서는 예외를 상당히 많이 사용한다. ATL을 사용할 때는 _ATL_NO_EXCEPTIONS를 정의해 예외를 비활성화 해야 한다. 또한 STL에서 예외를 비활성화 할 수 있는지 살펴봐야 하고 그렇게 할 수 없을 때 컴파일러에서 예외를 켜는 것은 괜찮다(이는 STL을 컴파일할 때만이며 여전히 예외 처리 코드를 작성해서는 안 된다).</li>
	<li>미리 컴파일한 헤더로 작업하는 일반적인 방법은 각 소스 파일 가장 위에 헤더 파일을 포함하는 것이다. 전형적으로 그 파일 이름은 StdAfx.h 또는 precompile.h이다. 다른 프로젝트와 코드를 쉽게 공유할 수 있도록 이 파일을 명시적으로 포함하지 말고 (precompile.cc에서 포함하는 것은 제외) 자동으로 포함하도록 /FI 컴파일러 옵션을 사용한다.</li>
	<li>일반적으로 이름이 resource.h이며 매크로만 포함하고 있는 리소스 헤더는 이 스타일 안내문을 따를 필요 없다.</li>
</ul>
<div>
<h2></h2>
<h2><strong>마지막으로(Parting Words)</strong></h2>
상식에 따르고 <em>일관적이어야 한다</em>.

코드를 수정할 때는 주위 내용을 몇 분간 살펴보고 스타일을 결정한다. if 절 주위에서 빈 칸을 사용하면 그렇게 한다. 주석문을 별로 만든 상자로 감쌌으면 마찬가지로 그렇게 한다.

스타일 안내문에서는 코딩할 때 사용하는 공통 단어를 제시하므로 사람들이 말하는 방법이 아닌 무엇을 말하는지에 집중할 수 있다. 여기서는 전역 스타일 규칙을 제시하므로 사람들은 그 단어를 알 수 있다. 하지만 지역 스타일 또한 중요하다. 만약 기존 코드와 매우 다르게 보이는 코드를 추가하면 그런 단절된 내용 때문에 다른 이가 그 것을 읽을 때 리듬을 깨뜨린다.

자 코드를 작성하는 방법에 대해 충분히 썼다. 코드 그 자체는 훨씬 더 흥미롭다. 즐겨라!

</div>

<hr align="left" size="1" width="33%" />

<div>

<a id="1" href="#ref1">[1]</a> 주: 기본적으로 입력 전용, 입출력 겸용, 출력 전용 순서를 유지한다.
<a id="2" href="#ref2">[2]</a> 주: 생성자, 소멸자 등이 없는 C 구조체
<a id="3" href="#ref3">[3]</a> 주: Interface 접미어보다 I를 접두어로 붙인다.
<a id="4" href="#ref4">[4]</a> 주: 예외는 가능한 쓰지 않는다. 하지만 예외를 잘 쓰면 분명 장점도 있다. 다음 글을 잘 참고해 본다.
<ul>
	<li><a href="http://opensw.wikidot.com/cpp-fundamentals-exception-ko">왜 예외를 쓰는 게 좋을까요?</a></li>
	<li><a href="http://yesarang.tistory.com/371">예외가 성능에 미치는 영향</a></li>
	<li><a href="http://yesarang.tistory.com/372">예외 처리와 관련된 고전 글 번역</a></li>
</ul>
<a id="5" href="#ref5">[5]</a> 주: 일관성을 유지하기 위해 후위 연산자를 써야 하는 게 아니라면 전위 연산자를 사용한다.
<a id="6" href="#ref6">[6]</a> 주: const 위치는 위 내용과 달리 const int foo; 형식으로 쓴다. 다음 내용을 참고한다.


int* const foo; // int에 대한 상수 포인터
const int* foo; // 상수 int에 대한 포인터

typedef char* CHARS; 일 때 다음 두 내용은 의미가 같다.

typedef CHARS const CPTR; // char에 대한 상수 포인터
typedef char* const CPTR; // char에 대한 상수 포인터

하지만 다음 두 내용은 의미가 다르다.

typedef const CHARS CPTR; // char에 대한 상수 포인터
typedef const char* CPTR; // 상수 char에 대한 포인터

[7] 주: 밑줄을 사용한다.
[8] 주: C++ 파일은 .cpp를 사용한다.
[9] 주: g_ 접두어를 사용한다.
[10] 주: UrlTableErrors에서 사용한 형식을 따른다.
[11] 주: Doxygen에서 사용하는 방식 중 JavaDoc 형식을 우선으로 한다.
[12] 주: 매개변수를 다음 줄에 4 칸 들여쓰기 한 두 번째 형식을 사용한다.
[13] 주: 줄을 맞추는 방식은 유지보수 할 때 매우 좋지 않으므로 인자는 4 칸 들여쓰기 해서 다음 줄에 쓴다.
[14] 주: 괄호와 조건 사이에 빈 칸을 두지 않도록 한다.
[15] 주: 가능하면 기존 코드도 점차 바꾸도록 한다.
[16] 주: 가능하면 기존 코드도 점차 바꾸도록 한다.
[17] 주: 표현식으로 하나의 결과값을 만들 때만 사용한다.
[18] 주: 네임스페이스 안 내용은 기본값으로 들여쓰기 한다. 중첩 네임스페이스 역시 마찬가지이다.
[19] 주: 소스 파일 이름은 .cpp를 사용한다.

You may also like...

  • Yoonseok Pyo

    안녕하세요. 오로카”oroca.org”에서 활동중인 표윤석이라고 합니다. 저희 커뮤니티에서 구글 스타일가이드를 기반으로 작업을 하려는 중, 모두가 읽기 쉽도록 번역글을 구하고 있었는데 이렇게 멋진 글이 있었네요. 정말 수고하셨습니다.

    “Surpreem”님이 작성하신 번역글을 인용, 수정, 재배포하고 싶은데 가능한지 여쭙고 싶습니다. 물론, 저작자 표시는 필히 하도록 하겠습니다. 그럼, 답변 부탁드립니다.

    • 안녕하세요.
      그렇게 하셔도 됩니다.

      덕분에 이런 로봇 커뮤니티를 알게 되어 반갑습니다. 관심은 항상 있지만 이런저런 핑계로 늘상 해 보지 못하고 있는데 저도 가입해서 틈나는 대로 공부 해야겠습니다.

      • Yoonseok Pyo

        정말 감사합니다. 글 작성이 되면 링크 올리도록 하겠습니다.

  • Lyun

    다른곳으로 퍼가도 될까요??

    • 출처 표시해 주시면 됩니다. 🙂
      그런데 꽤나 옛날 글이라 최근 내용이 많이 빠져 있습니다.