Revision 3.274
Benjy Weinberger만세! 이제 각 문단의 요점을 클릭해서 추가정보를 펼쳐볼 수 있게 되었다. 이 문서 상단에 있는 "모든 추가정보 펼치기"를 사용할 수도 있다.
C++은 구글의 여러 오픈소스 프로젝트에서 사용하는 주요한 개발 언어이다. 모든 C++ 프로그래머가 알고 있듯, C++ 언어는 강력한 기능을 많이 가지고 있으나, 그 강력함은 복잡함을 야기하고 그로 인해 버그가 생기기 쉬운 코드가 되거나 읽고 관리하기 어려운 코드가 될 수 있다.
이 가이드의 목표는 C++ 코드에서 해야 할 것과 하지 말아야 할 것을 자세하게 서술해서 복잡함을 관리하는 것이다. 이러한 규칙들은 코더들이 C++ 언어의 기능들을 계속하여 생산적으로 사용하면서도 기반 코드를 관리가능한 상태로 유지하기 위한 것이다.
'가독성'이라고도 부르는 이 '스타일'은, 우리의 C++ 코드를 지배하는 컨벤션이다. 이러한 컨벤션은 단순한 소스 파일 포맷팅보다 훨씬 많은 것을 담고 있기 때문에 스타일이라는 용어는 다소 그릇된 명칭이다.
우리의 기반 코드를 관리할 수 있게 유지하는 한 가지 방법은 일관성을 강제하는 것이다. 어떤 프로그래머라도 남의 코드를 볼 수 있고 쉽게 이해할 수 있는 것은 매우 중요하다. 일치된 스타일을 유지하고 컨벤션에 따른다는 것은 우리가 더 쉽게 "패턴 매칭"을 사용하여 다양한 기호들이 무엇을 의미하고 어떤 값이 변함없이 참인지를 추측할 수 있다는 것을 의미한다. 모두가 필수적으로 사용할 숙어와 패턴을 만들면 코드를 이해하는 것이 훨씬 쉬워진다. 가끔은 어떤 스타일 규칙을 바꾸자는 바람직한 논쟁이 있을 수 있지만, 그럼에도 불구하고 일관성을 유지하기 위해 규칙을 기존대로 유지한다.
이 가이드가 서술하는 또 다른 이슈는 C++의 기능이 비대해지고 있다는 것이다. C++은 많은 고급 기능이 있는 거대한 언어이다. 어떤 경우에 우리는 어떤 기능의 사용을 제한하거나 심지어 금지한다. 이것은 코드를 간단하게 유지하면서 그 기능들이 흔히 만들 수 있는 문제들과 오류들을 피하기 위해서다. 이 가이드는 이러한 기능들을 나열하고 왜 사용이 제한되었는지 설명한다.
구글에서 개발된 오픈 소스 프로젝트는 이 가이드의 요구사항을 따른다.
이 가이드는 C++ 튜토리얼이 아니다. 우리는 독자가 이 언어에 친숙하다고 가정한다.
기본적으로 모든 .cc
파일은 수반된 .h
파일을 가져야
한다. 흔한 예외로는 유닛 테스트나 main()
함수만을 가진 작은
.cc
파일 등이 있다.
헤더 파일을 바르게 사용하는 것으로 코드의 가독성과 크기와 성능에 큰 차이를 만들 수 있다.
아래 규칙들은 헤더 파일을 사용할 때 피해야 할 함정들에 관한 가이드이다.
#define
가드를
사용해야 한다. 기호의 포맷은
<PROJECT>_<PATH>_<FILE>_H_
으로 한다.
유일성을 보장하기 위해 #define
가드는 프로젝트의 소스
트리의 절대 경로에 기반해야 한다. 예를 들어 프로젝트에
foo/src/bar/baz.h
파일이 있다면 foo
는
아래와 같은 가드를 가져야 한다.
#include
를 피하기 위해 클래스를 전방
선언할 수도 있다.
#include
라인은 종종 전방 선언으로 대체할 수 있다.
#include
는 컴파일러가 더 많은 파일을 열고 더
많은 입력을 처리하도록 만든다.#include
는 헤더 파일 변경 시 코드를
더 자주 다시 컴파일하도록 만든다.#include
가 필요한지 결정하기 힘들 수 있다. 특히 암시적
변환이 개입될 때 그렇다. 극단적인 경우에는 #include
를
전방 선언으로 대체하면 코드의 의미가 조용히 바뀔 수도 있다.#include
하는 것보다 장황해질 수 있다.std::
네임스페이스의 기호를 전방 선언하면 보통
예상할 수 없는 결과를 유발한다.#include
하라.#include
하는 것을 선호하라.#include
하라.#include
를 피하기 위해서 데이터 멤버를
포인터로 대체하지 말라.#include
하라. 다른 헤더를 거쳐 간접적으로
가져온 기호에 의존하지 말라. 하나의 예외로 myfile.cc
파일은 그에 상응하는 myfile.h
헤더에 있는
#include
와 전방 선언에 의존할 수 있다.
괜찮은 판단방법은 길이가 10라인 이상인 함수는 인라인하지 않는 것이다. 소멸자를 주의하라! 소멸자는 함축적으로 멤버와 상위 클래스의 소멸자를 부르기 때문에 보이는 것보다 종종 길다.
또 하나의 괜찮은 판단방법이 있다. 반복문이나 switch 문이 있는 함수 (보통의 경우 절대 수행되지 않는 반복문이나 switch 문인 경우를 제외하고)를 인라인하는 것은 보통 효과적이지 않다.
함수가 인라인으로 선언되었다고 해서 항상 인라인되는 것은 아님을 유의하자. 예를 들면 가상 함수나 재귀 호출 함수는 보통 인라인되지 않는다. 일반적으로 재귀 호출 함수는 인라인되면 안 된다. 가상 함수를 인라인하는 이유는 접근자와 변경자의 경우처럼 주로 클래스 안에 정의를 넣기 위해서이며, 이는 편의 때문이거나, 하는 일을 문서화하기 위해서이다.
-inl.h
접미어가 붙은 파일 이름을 사용할 수 있다.
인라인 함수의 정의는 헤더 파일에 있어야 하며, 이것은 컴파일러가 인라인
함수 호출부분을 컴파일 할 때 인라인 함수의 정의가 필요하기 때문이다.
하지만 구현 코드는 .cc
파일에 포함되는 것이 적절하고,
가독성이나 성능상의 이점이 없는 경우 .h
파일에 실제 코드를
많이 넣는 것을 좋아하지 않는다.
인라인 함수가 짧고 로직이 거의 없는 경우 .h
파일에 넣는
것이 좋다. 예를 들면 접근자나 변경자들은 당연히 클래스 정의에
있어야 한다. 구현이나 호출이 더 편리한 경우 더 복잡한 인라인 함수도
.h
파일에 넣을 수 있으나, 이들이 .h
파일을 너무 다루기 힘들게 만드는 경우엔 대신 별도의 -inl.h
파일에 코드를 넣을 수 있다. 이는 클래스 정의와 인라인 함수의 구현을
분리하면서도 여전히 컴파일러가 필요할 때마다 인라인 함수의 구현을
사용할 수 있게 한다.
-inl.h
파일의 또 다른 사용은 함수 템플릿 정의를 위한
것이다. 이로써 템플릿 정의를 읽기 쉽도록 유지할 수 있다.
-inl.h
파일도 다른 모든 헤더 파일과 마찬가지로
#define
가드가 필요하다는
것을 잊지 말라.
C/C++ 함수의 인자는 입력이거나 출력이거나 둘 다일 수
있다. 입력 인자는 보통 값(value)이거나 const
레퍼런스이고,
출력과 입출력 인자는 const
가 아닌 포인터가 될 것이다. 함수
인자를 순서에 따라 배열할 때, 모든 입력 전용 인자를 출력 인자보다 앞에
두라. 특히 단순히 새롭다는 이유로 새 인자를 함수 마지막에 추가하지 말라.
새로운 입력 전용 인자가 있으면 출력 인자 앞에 두라.
이것은 엄격한 규칙은 아니다. 입력이면서 출력인 인자(종종 클래스와 구조체)는 일을 복잡하게 만든다. 그리고 언제나 그렇듯이 관련된 함수들과의 일관성을 위해 이 규칙을 따르지 않을 수 있다.
.h
,
현재 프로젝트의 .h
.
모든 프로젝트의 헤더 파일은 UNIX의 디렉터리 단축 표시인 .
(현재 디렉터리)이나 ..
(부모 디렉터리)을 사용하지 않고
프로젝트의 소스 디렉터리의 하위 요소로 나열되어야 한다. 예를 들면
google-awesome-project/src/base/logging.h
는 이와 같이
#include
되어야 한다.
주된 목적이 dir2/foo2.h
에 있는 것들을 구현하거나
테스트하기 위한 dir/foo.cc
나
dir/foo_test.cc
에서 include를 아래처럼
순서에 따라 배열하라.
dir2/foo2.h
(아래 설명 참조).
.h
파일.h
파일
이 순서에 따르면 dir2/foo2.h
가 어느
필요한 include들을 빠뜨린 경우에 dir/foo.cc
나
dir/foo_test.cc
의 빌드는 망가진다. 그러므로 이
규칙은 다른 패키지에서 작업하는 무고한 사람들의 빌드 실패보다 이
파일에서 작업하는 사람들의 빌드 실패를 먼저 보여주도록 한다.
dir/foo.cc
와
dir2/foo2.h
는 종종 같은 디렉터리에 있지만,
다른 디렉터리에 있을 수 있다 (예를 들면
base/basictypes_test.cc
와
base/basictypes.h
처럼).
각각의 부분 안에서 include는 알파벳 순서로 나열되어야 한다. 오래된 코드는 이 규칙에 따르지 않을 수 있고 편리할 때 수정되어야 한다는 것에 주목하라.
예를 들자면
google-awesome-project/src/foo/internal/fooserver.cc
의
include들은 이렇게 보일 수 있다.
예외: 가끔 시스템에 특정한 코드들은 조건부 include를 필요로 한다. 이런 코드들은 조건부 include를 다른 include 밑에 둘 수 있다. 당연히, 시스템에 특정한 코드들을 작고 국한되게 유지하라. 예를 들자면,
.cc
파일에 이름 없는 네임스페이스 사용을 권장한다. 이름 있는
네임스페이스는 프로젝트에 기반한, 주로 경로에 기반한 이름을 선택하라.
네임스페이스는 클래스들로 이루어진 (계층적인) 이름 체계에 더해 추가적인 (마찬가지로 계층적인) 이름 체계를 형성한다.
예를 들면 전체 영역(전역)에서 클래스 Foo
를 가진 서로 다른
두 프로젝트가 있다면, 이들 기호들은 컴파일 시점이나 런타임에 충돌할 수
있다. 만약 각각의 프로젝트가 그들의 코드를 네임스페이스 안에 둔다면
project1::Foo
와 project2::Foo
는 이제
충돌하지 않는 서로 다른 기호이다.
인라인 네임스페이스는 자동적으로 자신의 이름들을 자신을 포함하는 네임스페이스에 넣는다. 예로 아래 코드 조각을 보라.
X::Y::foo()
와 X::foo()
라는 표현은 서로 교환
가능하다. 인라인 네임스페이스는 원래 버전간 ABI 호환성을 위해 만들어
졌다.
클래스들로 이루어진 (계층적인) 이름 체계에 추가적으로 (마찬가지로 계층적인) 이름 체계를 만들기 때문에, 네임스페이스는 혼동을 줄 수 있다.
인라인 네임스페이스는 실제로 선언한 이름들이 그 네임스페이스에 제한되지 않기 때문에 특히 혼동을 줄 수 있다. 인라인 네임스페이스는 더 큰 버전 정책의 부분으로서만 유용하다.
헤더 파일에서 이름 없는 네임스페이스의 사용은 C++ One Definition Rule (ODR)을 쉽게 침범할 수 있다.
네임스페이스는 아래에 설명한 정책에 따라 사용하라. 주어진 예제에서 보이는 것처럼 네임스페이스 끝에는 주석을 붙이라.
.cc
파일에서 이름
없는 네임스페이스는 허용되며 심지어 권장된다.
하지만 특정 클래스와 연관이 있는 파일 영역 선언은 이름 없는 네임스페이스의 멤버보다는 그 클래스 내부의 타입 또는 정적 데이터 멤버나 정적 멤버 함수로서 선언될 수 있다.
.h
파일에서 이름 없는 네임스페이스를 사용하지
말라.이름 있는 네임스페이스는 아래와 같이 사용되어야 한다.
전형적인 .cc
파일은 다른 네임스페이스의 클래스를
참조하는 등의 이유로 더 복잡해질 수 있다.
std
네임스페이스에는 아무것도 선언하지 말라.
표준 라이브러리 클래스들의 전방 선언도 하지 말라.
std
네임스페이스에 요소를 선언하는 것은
포팅이 불가능한, 정의되지 않은 행동이다. 표준 라이브러리의
요소를 선언하려면 해당 헤더 파일을 #include
하라.
.cc
파일의
어느 곳에서나 사용할 수 있고, .h
파일의 함수와
메서드와 클래스 안에서 사용할 수 있다.
.cc
파일 어디서나 사용할 수 있고,
.h
파일 전체를 감싸고 있는 이름 있는 네임스페이스 안의
모든 곳과, 함수나 메서드 안에서 사용할 수 있다.
.h
파일에 있는 별칭은 그 파일을
#include
하는 모든 사람에게 보인다는 점에 유의하라.
그러므로 (프로젝트 밖에서 활용할 수 있는) 공개 헤더과
그 공개 헤더들이 #include
하는
헤더들은 가능한 한 공개 API를 작게 유지하려는 목적의 일환으로
별칭들을 정의하는 것을 피해야 한다.
.cc
파일에서 중첩 클래스를 정의할 수 있다.
Foo::Bar*
포인터를 다루고자 하는 모든 헤더 파일은
Foo
클래스의 전체 클래스 선언을 include해야 할 것이다.
가끔은 함수를 클래스 인스턴스에 의존하지 않도록 정의하는 것이 유용하고 때로는 필요하다. 이러한 함수들은 정적 멤버 함수이거나 비멤버 함수일 수 있다. 비멤버 함수는 외부 변수에 의존적이어서는 안 되고, 거의 항상 네임스페이스 안에 있어야 한다. 단순히 정적 데이터를 공유하지 않는 정적 멤버 함수들을 분류하기 위해 새 클래스를 만들기 보다는 네임스페이스를 사용하라.
생성된 클래스로서 같은 컴파일 단위에 정의된 함수들은 다른 컴파일 단위에서 직접 호출될 때 불필요한 커플링이나 링크 시의 종속 관계를 유발할 수 있다. 정적 멤버 함수가 여기에 특별히 민감하다. 이런 함수들을 새 클래스로 뽑아 내거나, 아마 다른 라이브러리로 분리된 네임스페이스 안에 이런 함수들을 두는 것을 고려하라.
만약 비멤버 함수를 정의해야 하고 그것이 해당
.cc
파일에서만 필요하다면, 그 인식 범위를 제한하기 위해
이름 없는 네임스페이스나
static
링크(예로 static int Foo() {...}
)를
사용하라.
C++에서는 함수 어디 곳에서나 변수 선언을 할 수 있지만, 변수를 가능한 한 국한된 범위로 선언하고 최대한 첫 번째 사용처에 가깝게 선언하기를 권장한다. 이는 읽는 사람이 선언을 찾고 변수의 타입과 초기값을 알아내는 것을 쉽게 한다. 특히 선언과 대입 대신 초기화가 사용되어야 한다. 예를 들자면
gcc가 for (int i = 0; i< 10; ++i)
를 올바르게 구현하고
있어서 (i
의 범위는 오로지 for
반복문의 범위),
같은 범위의 다른 for
반복문에서 i
를 재사용할
수 있다는 점에 주목하라. 또한 if
와 while
안의
선언의 범위도 올바르다. 예를 들면
경고할 것이 있다. 만약 변수가 객체이면 그 생성자는 범위에 들어가서 생성되는 때마다 호출되고 그 소멸자는 범위를 빠져나갈 때마다 호출된다.
반복문에서 사용되는 이런 변수를 선언할 때는 반복문 바깥에 하는 것이 성능에 좋을 수 있다.
constexpr
인 경우는 허용한다. 이들은 동적으로 초기화되거나
소멸하지 않는다.
전역 변수, 정적 변수, 정적 클래스 멤버 변수, 함수 정적 변수 등의 정적 저장 기간을 가진 객체들은 반드시 Plain Old Data (POD)여야 한다. 오직 int, char, float, 포인터, 혹은 POD의 배열, POD의 구조체여야 한다.
정적 변수의 클래스 생성자와 초기화 함수가 호출되는 순서는 C++에서
부분적으로만 특정되어 있고, 심지어 각각의 빌드마다 호출 순서가 변경될 수
있어 찾기 힘든 버그를 만들 수 있다. 그러므로 우리는 클래스 타입의 전역
변수를 금지하며, 또한 (getenv()
나
getpid()
처럼) 다른 전역 요소에 전적으로 의존하는 함수가
아닌 한 함수를 사용하여 정적 POD 변수를 초기화할 수 없다.
이와 비슷하게 전역 변수와 정적 변수는 main()
함수에서
리턴하거나 exit()
의 호출 여부에 무관하게 프로그램이
종료될 때 소멸한다. 소멸자의 호출 순서는 생성자의 호출 순서의 반대로
정의된다. 생성자의 호출 순서를 특정할 수 없기 때문에 소멸자의 호출
순서도 마찬가지이다. 예를 들어 프로그램이 종료되는 시점에서 정적 변수는
이미 소멸되었을 수 있고, 하지만 여전히 수행 중인 코드가 (아마도 다른
스레드에 있는) 그것에 접근하려고 시도하고 실패할 수 있다. 혹은 어느 정적
string
변수의 소멸자가 그 문자열을 참조하는 다른 변수에
대한 소멸자보다 먼저 수행될 수도 있다.
소멸자 문제를 완화하기 위해 exit()
대신
quick_exit()
을 불러 프로그램을 종료할 수도 있다.
둘의 차이점은 quick_exit()
는 소멸자를 호출하지 않으며
atexit()
를 호출하여 등록된 핸들러들을 호출하지 않는
것이다. 만약 quick_exit()
를 사용해서 프로그램을 종료할 때
수행해야 하는 핸들러가 있는 경우 (예를 들면 로그를 flush 한다거나),
at_quick_exit()
를 사용하여 등록할 수 있다. (만약
exit()
와 quick_exit()
에 모두 수행해야 하는
핸들러가 있다면 두 곳 모두 등록해야 한다.)
결과적으로 우리는 POD 데이터로 이루어진 가지는 정적 변수만을 허용한다. 이
규칙은 vector
(대신 C 배열을 사용하라)나
string
(대신 const char []
를 사용하라)을
완전히 금지한다.
만약 클래스 타입의 정적 변수나 전역 변수가 필요하면
main()
함수나 pthread_once()
에서 (절대로
해제되지 않는) 포인터를 초기화하는 방법을 고려하라. 이것은 "스마트"한
포인터가 아닌 그냥 포인터여야 하는 데 주의하라. 스마트 포인터의 소멸자는
우리가 피하려 하고 있는 소멸자 순서 문제를 야기할 것이기 때문이다.
main()
보다 먼저 수행될 것이고, 아마도 생성자
코드에 있는 암묵적인 가정을 깨뜨릴 것이다. 예를 들면
gflags들이
아직 초기화되지 않았을 것이다.
Init()
메서드의 사용을 고려하라.
new
했을 때
호출된다. (배열의 경우에) new[]
를 호출할 때 항상 호출된다.
클래스 내 멤버 초기화란 int count_;
나
string name_;
와 반대로 int count_ = 17;
나
string name_{"abc"};
처럼 멤버 변수를 선언하면서
구성하는 것을 뜻한다.
초기화 구문이 제공되지 않았다면 사용자가 정의한 디폴트 생성자가 객체를 초기화한다. 이는 객체가 항상 생성과 동시에 유효하고 사용 가능한 상태임을 보장한다. 또한 디버깅을 지원하기 위해 객체가 최초에는 명백하게 "성립불가능한" 상태로 생성되도록 보장한다.
클래스 내 멤버 초기화는 여러 생성자에 초기화 코드를 중복해서 넣지 않고도 멤버 변수가 적절히 초기화될 것을 보장한다. 이로써 새 멤버 변수를 추가할 때 어떤 생성자에는 초기화 코드를 넣고, 다른 생성자에는 초기화 코드를 넣지 않아서 생기는 버그가 줄어들 수 있다.
디폴트 생성자를 명시적으로 정의하는 것은 코드 작성자에게 추가적인 작업이다.
멤버 변수가 선언 부분에서 초기화된 다음에 다시 생성자에서 초기화된 경우, 생성자의 값이 선언의 값을 재정의하기 때문에 혼동을 줄 가능성이 있다.
간단한 초기화에는, 특히 여러 생성자에서 같은 방식으로 초기화되는 멤버 변수 같은 경우에는, 클래스 내 멤버 초기화를 사용하라.
클래스의 멤버 변수가 클래스 내에서 초기화되지 않고 다른 생성자도 없는 경우, 반드시 디폴트 생성자 (인자 없는 생성자)를 정의해야 한다. 디폴트 생성자는 되도록 내부 상태가 일관성 있고 유효하도록 객체를 초기화해야 한다.
그 이유는 다른 생성자가 없는 상황에서 디폴트 생성자를 정의하지 않으면 컴파일러가 디폴트 생성자를 만들어주기 때문이다. 컴파일러가 만든 생성자는 객체를 올바르게 초기화하지 못할 수 있다.
어떤 클래스가 기존 클래스를 상속했지만 새로운 멤버 변수를 추가하지 않은 경우, 디폴트 생성자를 만들어야 할 필요는 없다.
explicit
을 사용하라.
Foo::Foo(string name)
을 정의하고
Foo
를 기대하는 함수에 문자열을 전달하면, 그 생성자가
호출되어 문자열을 Foo
로 바꾸고 그 Foo
를 그
함수에 전달하게 될 것이다. 이는 편리할 수 있지만 의도하지 않는 객체
변환과 신규 객체 생성이 일어날 때는 골치거리이기도 하다. 생성자를
explicit
으로 선언하는 것은 생성자가 변환에 사용되기 위해
암시적으로 호출되는 것을 막는다.
인자가 하나인 모든 생성자는 explicit
이어야 한다.
인자가 하나인 생성자 정의의 앞에 항상 explicit
를 포함하라.
explicit Foo(string name);
복사 생성자는 극히 일부 경우를 제외하고서는 반드시
explicit
이 아니어야 한다. 다른 클래스에 대한
보이지 않는 래퍼(wrapper)로 의도된 클래스의 경우도 또 다른 예외이다.
이런 예외들은 주석을 통해 명백하게 표시되어야 한다.
마지막으로, 초기화 리스트(initializer_list) 하나만을 취하는 생성자들도
explicit이 아닐 수 있다. 이는 중괄호 초기화 리스트들의 대입 형태를
사용하여 타입을 생성하는 것을 허용하기 위함이다 (예를 들면
MyType m = {1, 2}
).
DISALLOW_COPY_AND_ASSIGN
로 그들을 쓸 수 없게 하라.
CopyFrom()
스타일의 차선책보다 성능이 더 좋을 수 있다.
복사 가능해야 하는 클래스는 거의 없다. 대부분의 클래스는 복사 생성자도 대입 연산자도 가지지 말아야 한다. 많은 경우 값을 복사하는 대신 포인터나 레퍼런스를 써서 더 나은 성능으로 같은 작업을 할 수 있다. 예를 들면 함수 인자를 값 전달하는 대신 레퍼런스나 포인터로 전달할 수 있고, STL컨테이너에 객체를 저장하는 대신 포인터를 저장할 수 있다.
만약 어떤 클래스가 복사 가능해야 한다면, 복사 생성자보다
CopyFrom()
이나 Clone()
과 같이 암시적으로
호출되지 않는 복사 메서드를 선호하라. 만약 복사 메서드로는 충분하지 않은
상황이라면 (예를 들면 성능 상의 문제가 있다거나 클래스가 STL
컨테이너가 값 전달될 필요가 있는 경우) 복사 생성자와
대입 연산자를 둘 다 작성하라.
복사생성자와 대입 연산자가 모두 필요 없는 경우 명시적으로 비활성화해야
한다. 그러기 위해서 클래스의 private:
부분에 기능 없는 복사 생성자와 대입 연산자의 선언을 추가하고 그에
해당하는 정의를 제공하지 말라 (그러므로 이들을 사용하려고 시도하면
링크 오류가 생긴다).
편의상, DISALLOW_COPY_AND_ASSIGN
매크로를
사용할 수 있다.
그렇다면 클래스 class Foo
에서,
생성자 위임과 생성자 상속은 생성자의 코드 중복을 줄이기 위해 모두 C++11에 도입된 기능으로 서로 다른 두 기능이다. 생성자 위임은 초기화 리스트 문법을 통해 그 클래스의 한 생성자에서 다른 생성자로 작업을 넘겨줄 수 있도록 한다. 예를 들면,
생성자 상속은 하위 클래스가 직접 사용 가능한 상위 클래스의 생성자를 다시 선언하지 않고 마치 상위 클래스의 다른 멤버 함수처럼 가질 수 있도록 한다. 이는 상위 클래스가 여러 생성자를 가진 경우에 특히 유용하다. 예를 들면,
이는 Derived
의 생성자가 Base
의 생성자를
호출하는 이외에 어떤 다른 일도 할 필요가 없을 때 특히 유용하다.
생성자 위임과 생성자 상속은 장황하고 획일적인 코드를 줄이고, 이는 가독성을 개선한다.
생성자 위임은 자바 프로그래머에게 친숙하다.
핼퍼 함수를 사용함으로써 생성자 위임의 동작과 비슷한 것을 만들 수 있다.
하위 클래스가 새 멤버 변수를 도입한 경우 상위 클래스의 생성자는 그들을 모르기 때문에 생성자 상속이 혼동을 줄 수 있다.
생성자 위임과 생성자 상속은 획일적인 코드를 줄이고 가독성을 개선할 수 있는 경우에 사용하라. 만약 하위 클래스가 새 멤버 변수를 도입할 경우 상속된 생성자들에 주의하라. 생성자 상속은 하위 클래스의 멤버 변수가 클래스 내 멤버 초기화를 사용할 경우에도 적절할 수 있다.
struct
를 사용하라.
그 외의 모든 경우에는 class
를 쓰라.
C++에서 struct
와 class
키워드는 거의 똑같이
동작한다. 여기서는 각각의 키워드에 고유한 의미를 부여한다. 그에 따라
정의하고자 하는 데이터 타입마다 적절한 키워드를 사용해야 한다.
structs
는 데이터를 나르는 수동적 객체로서 사용되고 연관된
상수들을 포함할 수 있으나, 데이터 멤버의 값을 읽거나 쓰는 것
이외의 어떤 기능도 가져서는 안 된다. 필드의 접근/변경은 메서드 호출이
아닌 직접 필드에 접근하는 방식으로 이루어진다. 메서드는 데이터
멤버를 셋업하는 것 외의 동작을 제공해서는 안 된다. 예를 들면 생성자,
소멸자, Initialize()
, Reset()
,
Validate()
.
만약 더 많은 기능이 필요하다면 class
가 적당하다. 불확실한
경우 class
로 만들라.
STL과의 일관성을 지키기 위해서, functor와 trait에 대해선
class
대신 struct
를 사용할 수 있다.
구조체와 클래스의 멤버 변수들은 서로 다른 이름 규칙을 가지는 데 주목하라.
public
으로 하라.
모든 상속은 public
이어야 한다. private 상속을
사용하지 말고, 대신 상위 클래스의 인스턴스를 멤버로 포함시켜야 한다.
구현 상속을 과용하지 말라. 대부분 컴포지션이 더 적절하다. 상속의
사용을 "동일 관계"의 경우로 제한하려고 노력하라. Bar
가
Foo
의 "종류 중 하나"라고 합당하게 말할 수 있으면
Bar
가 Foo
의 하위 클래스이다.
필요한 경우 소멸자를 virtual
로 하라. 만약 클래스가
가상 메서드를 가지고 있다면 그 소멸자는 반드시
virtual
이어야 한다.
하위 클래스로부터 접근이 필요할 수 있는 멤버 함수들만
protected
로 선언하라.
데이터 멤버는 항상 private여야 하는 데
주의하라.
상속받은 가상 함수를 재정의할 경우 명시적으로 하위 클래스의 선언에
그것을 virtual
로 선언하라. 그 이유는 만약
virtual
이 없으면 읽는 사람이 이 함수가 가상인지 아닌지
궁금한 경우 이 클래스의 모든 조상을 확인해야 하기 때문이다.
Interface
접미어가 달린
순수한 인터페이스 클래스여야 한다.
Interface
라는
접미어로 끝나야 한다.
Interface
라는 접미어로 끝날
수 있다.
아래와 같은 요구를 충족하는 클래스는 순수한 인터페이스이다.
= 0
") 메서드와 정적
메서드만을 가진다 (단 소멸자의 경우는 아래를 보라).
protected
여야 한다.
Interface
라는 접미어로 표시된 클래스로부터
상속받은 클래스여야 한다.
인터페이스는 순수 가상 메서드를 포함하기 때문에 직접 인스턴스화될 수 없다. 또한 해당 인터페이스의 모든 구현체가 바르게 소멸될 수 있는지 보장하기 위해 인터페이스는 가상 소멸자를 반드시 정의해야 한다 (첫 번째 규칙의 예외로 이 가상 소멸자가 순수할 필요는 없다). 자세한 내용은 Stroustrup의 The C++ Programming Language, 3 판, 12.4 섹션을 보라.
Interface
라는 접미어로 표시하는 것은 다른 사람이
이들에 구현이 있는 메서드나 정적이 아닌 데이터 멤버를 추가하면 안 된다는
사실을 알게 한다. 이는 특히 다중 상속의 경우에
특별히 중요하다. 또한 인터페이스 개념은 자바 프로그래머에게 이미 잘
알려져 있다.
Interface
접미어는 클래스 이름을 길게 늘여서 읽고 이해하기
힘들게 만들 수 있다. 또한 인터페이스의 속성은 클라이언트에 노출되지
말아야 할 구현 상세로 간주할 수도 있다.
Interface
로 끝날 수
있다. 하지만 반대의 경우가 필요한 것은 아니다. 즉, 위의 조건을 만족하는
클래스가 꼭 Interface
로 끝나야 하는 것은 아니다.
+
나 /
와 같은 연산자를 정의할 수 있다.
operator""
를 오버로드하여 클래스 타입의 객체를 만드는 데
내장 리터럴 문법을 사용할 수 있다.
연산자 오버로드는 클래스가 내장 타입(int
따위)과 같은
방식으로 동작하기 때문에 코드가 더 직관적으로 보이게 할 수 있다.
오버로드된 연산자는 Equals()
나 Add()
와 같은
단조로운 이름보다는 더 재미있는 함수 이름이다.
어떤 템플릿 함수가 바르게 동작하기 위해 연산자를 정의해야 할 필요가 있을 수 있다.
사용자 정의 리터럴은 사용자 정의 타입의 객체를 생성하는 방법으로 매우 간결한 표시법이다.
Equals()
를 찾는 것은 이에 해당하는 ==
를
찾는 것보다 훨씬 쉽다.
Foo + 4
의 동작과 &Foo + 4
의 동작은
완전히 다르다. 컴파일러는 어느 경우에도 문제삼지 않을 것이므로
디버그는 아주 힘들어진다.
operator&
를 오버로드하면 그 클래스는 안전하게 전방
선언될 수 없다.
일반적인 경우 연산자를 오버로드하지 말라. 특히 대입 연산자
(operator=
)는 위험하기 때문에 피해야 한다.
필요한 경우 Equals()
나 CopyFrom()
과 같은
함수를 정의할 수 있다. 마찬가지로, 전방 선언될 수 있는
가능성이 조금이라도 어떻게 해서라도 위험한 단항
operator&
도 피하라.
operator""
를 오버로드하지 말라. 즉, 사용자 정의
리터럴을 도입하지 말라.
그럼에도 템플릿이나 "표준" C++ 라이브러리 (로깅에 사용하는
operator<<(ostream&, const T&)
따위)
와 연동하기 위해 연산자를 오버로드해야 하는 드문 경우가 있을 수 있다.
완벽히 정당화할 수 있는 경우 허용되지만, 가능한 매 순간마다 이를
피하려고 노력해야 한다. 특히 어떤 클래스가 STL 컨테이너에 키로
쓰일 수 있다는 이유만으로 operator==
와
operator<
를 오버로드하지 말라. 대신 컨테이너를 선언할
때, 항등 functor와 비교 functor type을 만들어야 한다.
어떤 STL 알고리즘은 operator==
을 오버로드할 것을
요구하는데, 이런 경우 그 이유를 설명으로 제공하면서 연산자를
오버로드할 수 있다.
private
으로 만들고, 필요한 경우 접근자 함수를
통해 접근을 제공하라 (기술적인 이유로 우리는
Google Test를 사용할 때
test fixture 클래스의 데이터 멤버가 protected
인 것을
허용한다). foo_
라고 불리는 변수가 있다면
접근자 함수는 전형적으로 foo()
라고 불린다. 또한
set_foo()
라는 변경자 함수가 필요할 수도 있다. 예외:
static const
데이터 멤버(전형적으로 kFoo
라고
불리는)들은 private
여야 할 필요가 없다.
보통 접근자의 정의는 헤더 파일에 인라인된다.
public:
이
private:
보다 먼저이며 메서드가 데이터 멤버 (변수)보다
먼저이며, 기타 등등.
클래스 정의는 반드시 public:
부분으로 시작해야 하며, 그
다음에 protected:
부분이 따라오고 그 다음이
private:
부분이다. 만약 이 부분 중 비어 있는 것이 있다면,
그 부분을 생략한다.
각각의 부분 안에서 선언은 보통 아래와 같은 순서여야 한다.
static const
데이터 멤버)static const
데이터 멤버는 제외)
friend
선언은 항상 private 부분 안에 있어야 하고
DISALLOW_COPY_AND_ASSIGN
매크로 호출은
private:
부분의 마지막에 있어야 한며 클래스의
마지막에 있는 것이어야 한다. 복사 생성자를
참조하라.
해당하는 .cc
파일의 메서드 정의는 가능한 한 선언과 같은
순서여야 한다.
클래스 정의에 너무 많은 인라인 메서드 정의를 넣지 말라. 보통 사소하거나 성능에 중요한, 매우 짧은 메서드만이 인라인으로 정의될 수 있다. 인라인 함수를 참조하라.
가끔은 긴 함수가 적절하다는 것을 알고 있기 때문에 함수의 길이에 관해 아주 강력한 제한을 두진 않는다. 만약 함수가 40 라인을 넘어가면 프로그램의 구조를 해치지 않는 범위에서 이 함수를 나눌 수 있는지 생각하라.
만약 긴 함수가 지금은 완벽하게 동작하고 있다고 하더라도, 누군가 몇 개월 후에 고쳐서 새로운 동작을 넣을 수 있다. 이로 인해 찾기 힘든 버그가 발생할 수 있다. 함수를 짧고 간단하게 유지하는 것은 다른 사람이 코드를 읽고 고치기 쉽게 한다.
작업하다 보면 길고 복잡한 함수를 만날 수 있다. 기존의 코드를 수정하는 데 두려움을 가지지 말라. 그런 함수에서 작업하는 것이 어려운 경우 오류를 디버그하기 힘들어졌다고 깨닫게 되거나 함수가 가진 여러 다른 맥락의 조각을 사용하고 싶어질 것인데 이 때 함수를 더 작고 관리 가능한 조각으로 쪼개는 것을 고려하라.
다른 곳에서 본 것과 다를 수 있는, 구글에서 C++ 코드를 더 튼튼하게 만들기 위해 사용하는 다양한 기법과 장치들이 있다.
"소유권"이란 동적으로 할당된 메모리(와 다른 리소스)를 관리하는 기법이다. 동적으로 할당된 객체의 소유자는 더 이상 필요치 않을 때 객체를 삭제할 것을 보장하는 객체 또는 함수이다. 소유권은 종종 공유될 수 있는데, 이 경우에는 보통 마지막 소유자가 삭제를 책임진다. 공유되지 않는 경우에도 소유권은 코드의 한 곳의 코드에서 다른 곳으로 이전될 수 있다.
"스마트" 포인터는 포인터와 유사하게 동작하는 클래스로,
*
와 ->
연산자를 오버로드하여 만든다.
이러한 스마트 포인터 타입은 소유권 관리를 자동화하기 위해 사용될 수 있다.
C++11에서 도입된 스마트 포인터 타입인
std::unique_ptr
는 동적으로 할당된 객체에 대한
배타적인 소유권을 나타낸다.
std::unique_ptr
이 범위 밖으로 나갈 때 객체가 삭제되며,
복사될 수는 없지만 소유권 이전에 해당하는 이동을 할 수 있다.
shared_ptr
은 복사가 가능한 것으로, 동적으로 할당된 객체에
대한 공유된 소유권을 나타내는 스마트 포인터 타입이다. 객체의 소유권은
모든 복사본에서 공유되며 객체는 마지막 shared_ptr
가
파괴될 때 삭제된다.
std::unique_ptr
는 소유권 이전을 C++11의 이동 문법으로
표현하는 데, 이는 구글 코드에서
일반적으로 금지된 것으로 몇몇
프로그래머들에게는 혼란을 일으킬 수 있다.
동적 할당이 필요한 경우 할당한 코드에서 소유권을 유지하는 것을
선호하라. 다른 코드가 그 객체를 접근해야 할 필요가 있는 경우
소유권을 이전하기보다 복사본을 전달하거나 포인터 또는 레퍼런스를
전달하는 것을 검토하라. 소유권 이전을 명시적으로 하기 위해
std::unique_ptr
를 사용하는 것을 선호하라. 예를 들면,
아주 좋은 근거가 없다면 공유 소유권을 쓰도록 코드를 디자인하지
말라. 그러한 근거로 복사 작업의 비싼 비용을 피할 수 있다는 것을 들 수 있으나,
기반 객체가 변경 불가능하며 성능상 이익이 상당한 경우에만 공유
소유권을 사용해야 한다 (예를 들면
shared_ptr<const Foo>
).
공유 소유권을 사용할 경우에는 shared_ptr
의 사용을
선호하라.
구버전의 C++과 호환할 필요가 없는 새 코드에서는
scoped_ptr
을 사용하지 말라. linked_ptr
이나
std::auto_ptr
을 절대 사용하지 말라. 위 세 가지 경우 모두
std::unique_ptr
을 대신 사용하라.
cpplint.py
를 사용하라.
cpplint.py
는 소스 파일에 있는 여러 스타일 오류를 식별해 주는
도구이다. 완벽하지 않고, 오류가 아닌 것을 오류로 판정하는 문제와 오류를
식별하지 못하는 문제를 모두 가지고 있지만, 여전히 소중한 도구이다.
// NOLINT
를 라인 마지막에 넣으면 오류가 아닌 것을 오류로
식별하는 문제를 무시하게 할 수 있다.
어떤 프로젝트에는 프로젝트 툴에 cpplint.py
를 실행하는 방법에 대한
설명이 포함되어 있다. 그렇지 않은 프로젝트에선 따로
cpplint.py
를 다운로드받을 수 있다.
const
로 수식되어야 한다.
int foo(int *pval)
. C++에서는 함수에서
레퍼런스 인자를 선언할 수 있다. int foo(int &val)
(*pval)++
과 같은 못생긴
코드를 쓰지 않을 수 있으며, 복사생성자 등에서는 필수적으로 레퍼런스를
써야 한다. 또 포인터와 달리 null 값을 가질 수 없는 것이 명확해진다.
함수 인자 리스트에 안의 모든 레퍼런스는 const
여야 한다.
사실 구글 코드에는 입력 인자는 값(value)이거나 const
레퍼런스고, 반면 출력 인자는 포인터라는 강력한 컨벤션이 있다. 입력
인자는 const
포인터일 수 있지만, swap()
처럼
컨벤션으로 정해져 있는 경우를 제외하고는 const
가 아닌
레퍼런스 인자는 절대 허용하지 않는다.
하지만 입력 인자에서 const T&
보다
const T*
를 사용하는 것이 나은 경우가 있다. 예를 들면,
const T&
로 지정될 것이라는
점을 기억하라. 그것 대신 const T*
를 사용하면 입력이 다소
다르게 다루어질 것을 읽는 이가 알 수 있다. 그러므로
만약 const T&
대신 const T*
를 택한다면
분명한 이유가 있을 때여야 한다. 그렇게 하지 않으면 있지도 않는 설명을
찾느라 읽는 사람이 혼란에 빠질 수 있다.
std::forward
,
std::move_iterator
, std::move_if_noexcept
를
사용하지 말라. 복사 불가능한 인자에 대해서만 단일 인자 형식의
std::move
를 사용하라.
void f(string&& s);
는 그 문자열의 rvalue 레퍼런스를
인자로 취하는 함수를 선언한다.
v1
이 vector<string>
인 경우
auto v2(std::move(v1))
는 아마 커다란
양의 데이터를 복사하는 대신 간단하게 포인터만 조작할 것이다. 이 경우
성능이 크게 좋아질 수 있다.
std::move
는 std::unique_ptr
등의 몇몇
표준 라이브러리 타입을 효과적으로 사용하기 위해 필요하다.
rvalue 레퍼런스를 사용하지 말고, std::forward
나
(본질적으로 rvalue 레퍼런스 타입으로 형 변환하는 것에 불과한)
std::move_if_noexcept
유틸리티 함수나
std::move_iterator
를 사용하지 말라. 복사 불가능한 개체
(예를 들면 std::unique_ptr
)에 대해서만, 혹은 복사
불가능해 질 개체에 대한 템플릿 코드 안에서 단일 인자의
std::move
를 사용하라.
const string&
를 취하는 함수를 쓰고 그것을
const char*
를 취하는 다른 함수로 오버로드할 수 있다.
Append()
보다
AppendString()
나 AppendInt()
.
위의 단점이 그렇게 부담이 되는 것은 아니지만, 여전히 함수 오버로딩에 비한 디폴트 인자의 (작은)장점보다 단점이 크다. 그래서 아래에 기술된 것을 제외하고는 모든 인자는 명시적으로 지정되어야 한다.
다른 명시적인 예외는 .cc 파일에서의 정적 함수(혹은 이름 없는 네임스페이스 안의 함수)이다. 이런 경우, 함수의 사용이 지역적이기 때문에 단점이 적용되지 않는다.
또다른 명시적인 예외는 디폴트 인자가 가변길이 인자 목록 리스트로 쓰여질 때이다.
alloca()
를 허용하지 않는다.
alloca()
는 매우 효율적이다.
scoped_ptr
/scoped_array
같은 안전한 할당자를
사용하여라.
friend
클래스와 함수들을 사용한다.
friend
는 코드를 읽는 이가 클래스의 private 멤버의 사용을
다른 파일에서 확인할 필요가 없도록 하기 위해서 일반적으로
같은 파일에서 정의된다. friend
의 일반적인
용례는 Foo
클래스의 내부 상태를 외부에 노출시키지 않고
제대로 구성하기 위해 FooBuilder
클래스가 Foo
클래스의 friend
가 되는 것이다. 유닛테스트 클래스를 테스트
대상 클래스의 friend
로 만드는 것이 때때로 유용하다.
friend
는 클래스의 캡슐화의 경계를 확장하지만 깨뜨리지는
않는다. 이것은 다른 클래스 하나를 위해 멤버를 public으로
두는 것보다 낫다. 그러나 대부분의 클래스는 public 멤버들을
통해서만 다른 클래스들과 상호작용하여야 한다.
Init()
메서드로 흉내낼 수 있지만, 이런 방법들은 각각 힙 할당과
"유효하지 않은 상태"의 도입을 필요로 한다.throw
문을 추가할 때 그 함수를
직간접적으로 호출하는 모든 부분을 반드시 확인해야 한다. 함수는 최소한의
기본 예외안전을 보장해야 하며, 그렇지 않다면 아예 예외를 받지 않아서
프로그램이 잘 종료되도록 해야 한다. 예를 들어 f()
가
g()
를, g()
가 h()
를 호출하되
h
가 던지는 예외를 f
가 받는다고 할 때,
g
는 주의 깊게 작성되어야 하며 그렇지 않으면 리소스가
제대로 정리되지 못할 수 있다.어떤 면에서, 특히 새 프로젝트에서 예외를 쓰는 경우 비용보다 이득이 많다. 그러나 기존의 코드에서 예외를 사용하기 시작하는 것은 모든 의존적인 코드에 영향을 미친다. 만약 예외가 새로운 프로젝트를 넘어서 전파될 수 있다면, 새로운 프로젝트를 예외를 사용하지 않는 기존코드에 통합하는 것이 문제가 된다. 구글에서의 대부분의 기존 C++ 코드는 예외를 다룰 준비가 안되어 있기 때문에, 예외를 만드는 새로운 코드를 적용하는 것은 상대적으로 어렵다.
구글의 기존 코드가 예외에 내성이 없다는 점을 감안하면, 예외를 사용하는 비용은 새로운 프로젝트에서의 비용보다 다소 크다. 전환 작업은 느리고 에러의 소지가 클 것이다. 에러코드 및 assert처럼 예외 대신 쓸 수 있는 것들이 크게 부담될 것이라 생각되지 않는다.
예외를 쓰지 말라는 충고는 철학적·윤리적인 배경이 아니라 실질적인 이유에 기반한다. 우리는 구글의 오픈소스 프로젝트들을 사용하기를 원하는데, 이런 프로젝트들이 예외를 사용한다면 그렇게 하는 것이 어렵기 때문에 구글의 오픈소스 프로젝트들 또한 예외를 사용하지 않기를 권한다. 처음부터 예외를 전체적으로 사용하기로 했었다면 아마 상황이 다를 것이다.
이 제한은 또한 noexcept
, std::exception_ptr
,
std::nested_exception
처럼 C++11 에서 추가된 예외에
관련된 기능들에도 적용된다.
윈도우 코드에는 이 규칙의 예외사항이 있다.
typeid
나 dynamic_cast
를 통해서
이루어진다.
실행시간(run-time)에 객체의 타입을 자주 질의하는 것은 설계에 문제가 있다는 뜻이다. 실행시간에 객체의 타입을 알아야 하는 것은 클래스 계층 설계가 가진 결함의 징후인 경우가 종종 있다.
RTTI 의 무분별한 사용은 코드를 유지보수하기 어렵게 만든다. 그리고 타입
기반의 의사 결정 트리나 코드에 흩어져있는 switch
문을
만든다. 이런 모든 것들은 나중에 코드를 변경할 때 반드시 검사되어야
한다.
아래에 기술된 RTTI의 표준적인 대안을 적용하려면 해당 클래스의 계층을 변경하거나 재설계해야 한다. 가끔 이런 변경은 불가능하거나 바람직하지 않으며, 특히 널리 사용되거나 원숙한 코드에서 더욱 그렇다.
RTTI는 몇몇 유닛테스트들에서 유용하다. 예를 들어, 팩토리 클래스에서 새롭게 만들어진 객체가 기대한 동적 타입을 가지는지 검증하는 테스트에서 유용하다. 또한 객체들과 그들의 모형(mock)간의 관계를 관리하는데 유용하다.
RTTI는 여러 개의 추상 객체들을 고려할 때 유용하다.
아래와 같은 방법을 고려하라.
RTTI 는 적법하지만 남용될 수 있으므로 반드시 주의깊게 사용해야 한다. 유닛테스트에서는 자유롭게 사용해도 되지만, 다른 코드에서는 가능하면 피하라. 특히 새로운 코드에서 RTTI를 사용할 때는 다시 한 번 생각하라. 만약 객체의 클래스에 따라 다르게 거동하는 코드를 작성할 필요가 있다면, 타입을 질의하는 아래의 대안 중 하나를 고려하라.
프로그램의 로직에서 어떤 상위 클래스의 인스턴스가 특정 하위 클래스의
인스턴스라는 것을 보장한다면, dynamic_cast
는 그 객체에
대해 자유롭게 사용할 수 있다.
일반적으로 그런 상황에서 static_cast
를 대신 사용할 수
있다.
타입을 기반으로 한 결정 트리는 코드가 잘못되고 있다는 징후이다.
RTTI 와 비슷한 회피 방법을 직접 구현하지 말라. RTTI 에 대한 논의는 타입 태그를 이용한 클래스 계층같은 회피 방법들에도 반드시 적용된다. 게다가, 회피 방법들은 실제 의도를 감춘다.
static_cast<>()
같은 C++ 형 변환을 사용하라.
int y = (int)x;
나 int y = int(x);
같은
다른 형 변환 형식을 사용하지 말라.
(int)3.5
와 같은) conversion 을 하고
가끔은 ((int)"hello"
와 같은) cast 을 한다.
C++ 형 변환은 이것을 방지한다. 게다가 C++ 형 변환은 검색하기도
좋다.
C 스타일의 형 변환을 사용하지 말라. 대신에 C++ 스타일의 형 변환을 사용하라.
static_cast
를 사용하라.
const
수식어를 제거하기 위해서
const_cast
를 사용하라.
(const 참조)
reinterpret_cast
를 사용하라.
지금 무엇을 하고 있는지 알고 있으며
aliasing 문제를 이해할 때에만 이것을 사용하라.
dynamic_cast
의 사용지침은
RTTI 를 참조하라.
printf()
과 scanf()
를 대체한다.
printf
도 그런 문제가 없다.)
스트림은 연관된 파일들을 열고 닫는 자동적인
생성자와 소멸자를 가진다.
pread()
같은 기능을 만들기 어렵다.
특히, %.*s
와 같은 문자열 포매팅은 printf
와
비슷한 교묘한 방법(hack)없이 스트림으로 효과적으로 처리하기가
불가능하거나 어렵다. 스트림은 국제화에 유용한 (%1s
지시자
같은) 연산자 재정렬을 지원하지 않는다.
로그를 위한 인터페이스에 필요할 때가 아니면 스트림을 사용하지 말라.
대신 printf
같은 것들을 사용하라.
스트림을 사용하는 데 다양한 장단점들이 있지만, 다른 많은 경우들과 같이 논의보다 일관성이 중요하다. 코드에서 스트림을 쓰지 말라.
이 문제에 대해서 논쟁이 있어 왔다. 그래서 좀 더 깊게 이유를
설명하겠다. "단 하나의 방법 가이드 원칙"을 기억하자. 우리는,
어떤 형태의 I/O 를 할 때, 모든 곳에서 같은 형태의 코드를 보길
원한다. 이 때문에 사용자가 스트림이나 printf
와
read
/write
중 하나를 선택하는 것을
허용하고 싶지 않았다. 대신 우리는 이것이나 저것 중 하나로 정해야
했다. 로그는 꽤 특화된 어플리케이션이고 역사적인 이유가 있기에
예외로 하였다.
스트림의 추종자들은 둘 중 스트림이 명백히 낫다고 주장하지만, 사실 이 문제는 그리 명백하지 않다. 그들이 지적하는 스트림의 모든 장점들은 동등한 단점을 가지고 있다. 가장 큰 장점은 출력할 객체의 타입을 몰라도 된다는 점이다. 이것은 타당하다. 하지만 잘못된 타입을 쉽게 사용할 수 있고, 컴파일러가 경고를 하지 않는다는 단점도 있다. 스트림을 사용할 때 자신도 모르게 이런 종류의 실수를 만들기 쉽다.
<<
가 오버로드되었기 때문에, 컴파일러는
오류를 내지 않는다. 정확히 이런 이유로 오버로드를 권하지
않는다.
누군가는 printf
포맷팅이 못생기고 읽기 어렵다고 하지만,
스트림도 더 나을 것이 없다. 아래 코드를 보자. 둘다 오타가 있다.
어느 것이 더 발견하기 쉬운가?
추가적인 논쟁이 계속 나올 수 있다. ("적절한 래퍼(wrapper)로 더 나아지지 않을까"라고 주장할 수도 있을 것이다. 그러나 그것은 두 경우 모두 마찬가지가 아닐까? 또한, 다른 사람이 배워야 할 문법을 추가하는 것이 아니라 언어를 더 작게 만드는 것이 목표임을 기억하라.)
두 경우 모두 각각 다른 장단점을 가지고 있고, 명백히 더 나은
해결법이 없다. 간단함의 원칙은 이들 중 하나를 택해야 한다는 점이다.
그리고 다수의 결정은 printf
+
read
/write
이다.
++i
) 를 사용하라.
++i
또는 i++
),
감소하며(--i
또는 i--
), 표현식의 결과값이
쓰이지 않는다면, 전위 증가 (감소) 방식과 후위 증가 (감소) 방식 중에
하나를 선택해야 한다.
++i
)는 "접미 형태"
(i++
)에 비해 비효율적일 리가 없다. 이것은
후위 증가 (혹은 감소) 방식은 만들어질 i
의 복사본을
요구하기 때문이다. 만약 i
가 이터레이터이거나
스칼라 타입이 아닌 타입이라면, i
의 복사는 비용이 비쌀 수
있다. 값이 무시되는 경우 두 증가 방식 모두 같은 거동이라면 그냥 항상
전위 증가 방식만 사용하는 것이 좋지 않을까?
for
루프에서) 전통적으로 후위
증가 방식이 사용되어 왔다.
영어와 같이 주어(i
)가 동사(++
)보다 먼저
나오기 때문에 후위 증가 방식이 읽기 편하다는 사람도 있다.
const
를 사용하라.
C++11 에서는 경우에 따라 const 보다 constexpr
을 쓰는 편이
낫다.
const
가 올 수 있다
(예를 들면 const int foo
).
클래스 함수들은 클래스 멤버 변수들의 상태를 변경하지
않음을 알리기 위해 const
수식어를 가질 수 있다
(예를 들면 class Foo { int Bar(char c) const;};
).
const
는 전염성이 강하다. 만약 const
변수
하나를 함수에 넘겨준다면, 그 함수는 반드시 그 프로토타입에
const
를 가져야 한다. (그렇지 않다면 그 변수에는
const_cast
가 필요할 것이다.) 이것은 라이브러리 함수들을
호출할 때 특히 문제가 될 수 있다.
const
변수, 데이터 멤버, 메서드, 인자는 컴파일 시에 타입
체크 단계를 추가한다. 이것은 오류를 가능한 한 빨리 찾기 위해서 더
좋다. 그러므로 const
를 합당한 곳마다 사용하기를 강력히
권장한다.
const
여야 한다.
const
로 메서드를 선언하라.
접근자는 거의 항상 const
여야 한다.
그 외 다른 메서드들의 경우 데이터 멤버들을 수정하지 않고,
const
가 아닌 메서드들을 호출하지 않으며,
데이터 멤버에 const
가 아닌 포인터나 레퍼런스를
반환하지 않는다면 const여야 한다.
const
로 만드는 것을 고려하라
mutable
키워드는 허용되지만, 쓰레드와 함께
사용될 때 안전하지 않다. 그래서 쓰레드 안전을 신중히 고려해야 한다.
어떤 사람들은 const int* foo
보다
int const *foo
형태를 선호한다.
그들은 이 형태가 더 일관성이 있기 때문에 더 읽기 쉽다고 주장한다.
이 형태는 const
가 항상 기술하는 객체 뒤에 있는 규칙을
따른다.
그러나 일관성에 근거한 이러한 주장은, 간혹 볼 수 있는 깊게 중첩된
포인터 표현식에 적용되지 않는다. 왜냐하면 const
표현식의
대부분은 단 하나의 const
를 가지고 있고,
이 const
는 기저의 값에 적용되기 때문이다. 이런 경우들에
유지할 일관성은 없다. (const
와 같은) "형용사"가
(int
와 같은) "명사"보다 앞에 있는 것이 영문법에 맞기
때문에 const
를 앞에 두는 것이 거의 틀림없이 가독성이 더
좋을 것이다.
그러므로 const
를 앞에 두는 것을 권장하지만, 강제하지는
않는다. 하지만 주변 코드와 일관성을 유지하라.
constexpr
를 사용하라.
constexpr
로 선언될 수 있다.
어떤 함수와 생성자들은 constexpr
로 선언되어,
constexpr
변수들을 정의할 수 있게 된다.
constexpr
를 사용하여 정의할 수
있다.
constexpr
를 통해 인터페이스의 상수 부분을 더 견고하게
명세할 수 있게 된다.
constexpr의 정의에 부합하는 진짜 상수와 함수들에
constexpr
를 사용하라.
constexpr
를 사용하기 위해 함수 정의를 복잡하게 하지 말라.
inline 을 강제하기 위해 constexpr
를 사용하지 말라.
int
다.
만약 다른 크기의 변수들이 필요하면 int16_t
처럼
<stdint.h>
에 있는 정확하게 크기가 정해진 정수 타입을
사용하라. 만약 변수가 2^31 (2GiB) 보다 크거나 같은 값을 표현한다면,
int64_t
같은 64비트 타입을 사용하라.
값이 int
에 담기 너무 크지 않더라도 계산의 과정에서 더 큰
타입이 필요할 수 있다는 점을 명심하라.
의심되는 경우에는 더 큰 타입을 사용하라.
short
는 16비트, int
는 32비트,
long
이 32비트, long long
이 64비트라고
가정된다.
<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^N 오버플러우의 나머지 연산을
정의한다거나 하는 분명한 목적이 없다면 uint32_t
처럼
부호 없는 정수 타입을 사용하지 말라. 특히 숫자가 절대 음수가 되지 않을
것을 말하기 위해 부호 없는 타입들을 사용하지 말라.
대신 assertion을 사용해서 이를 보장하라.
크기를 반환하는 컨테이너라면, 컨테이너의 가능한 모든 사용을 수용할 수 있는 타입을 사용하라. 의심된다면 작은 타입 대신 더 큰 타입을 사용하라.
정수 타입들을 변환할 때 조심하라. 정수의 변환과 승격(promotion)은 직관적이지 않은 행동을 유발할 수 있다.
몇몇 교과서 저자들을 비롯해서 어떤 사람들은 절대 음수가 될 수 없는 숫자를 표현하기 위해서 부호없는 타입들을 사용하는 것을 권장한다. 이것은 자기 문서화를 하기 위한 형태이다. 그러나 C에서는 그런 문서화의 장점보다 그것이 만들어 낼 수 있는 실제 버그가 더 중요하다. 아래의 코드를 생각해보자.
이 코드는 절대 종료하지 않는다! 가끔 gcc 는 이 버그를 경고하지만, 경고하지 않는 경우도 많다. 마찬가지로 부호 있는 타입의 변수들과 부호 없는 타입의 변수들을 비교할 때 나쁜 버그들이 발생할 수 있다. 근본적으로 C의 자동 형 변환 방식은 부호 없는 타입들이 기대와 다르게 거동하는 원인이 된다.
따라서 assertion을 사용해서 변수가 음수가 아니라는 것을 문서화하라. 부호없는 타입을 사용하지 말라.
printf()
포맷 지정자 중에서 몇 가지 타입은 64비트와
32비트에 대해 완전한 이식성을 가지지 않는다. C99는 이식 가능한 포맷
지정자를 정의한다. 불행히도 MSVC 7.1에서 이 중 몇 가지가 동작하지
않고 표준 중에서 몇 가지가 빠졌다.
그래서 몇 가지 경우에 대한 못 생긴 버전을 정의했다.
(표준 인클루드 파일인 inttypes.h
의 스타일에 맞추어)
Type | DO NOT use | DO use | Notes |
---|---|---|---|
void * (or any pointer) |
%lx |
%p |
|
int64_t |
%qd ,
%lld |
%"PRId64" |
|
uint64_t |
%qu ,
%llu ,
%llx |
%"PRIu64" ,
%"PRIx64" |
|
size_t |
%u |
%"PRIuS" ,
%"PRIxS" |
C99 specifies %zu |
ptrdiff_t |
%d |
%"PRIdS" |
C99 specifies %td |
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())
을
사용할 수 있다.
LL
이나 ULL
접미사를 사용하라. 예를 들면
#ifdef _LP64
를 사용하라(가능하면 사용하지 않는 것이
최선이지만).
const
변수를 선호하라.
매크로는 프로그래머가 보는 코드와 컴파일러가 보는 코드를 다르게 만든다. 특히, 매크로는 전역 범위(scope)를 가지기 때문에, 예상치 못한 결과를 초래할 수 있다.
다행스럽게도 C++에서는 C와는 달리 매크로의 사용이 필수가 아니다.
성능이 민감한 코드에는 매크로 대신 인라인 함수를 사용하라.
상수를 저장하는 용도로 매크로 대신에 const
변수를 사용하라.
긴 변수 이름을 축약하는 용도로 매크로 대신에 레퍼런스를 사용하라.
조건부로 코드를 컴파일하는 용도로는, 글쎄, 아예 그렇게 하지 말라.
(예외적으로 헤더파일 이중 include를 막는
#define
가드 용도로는 사용해도 됨). 테스트를 훨씬 어렵게
만든다.
매크로는 다른 테크닉으로는 할 수 없는 것들을 가능케 하고, 하부 레벨의 라이브러리 코드베이스에서 종종 볼 수 있다.그리고 문자열로 변환하는 기능이나 문자열을 연결하는 기능과 같은 고유한 기능이 있다. 하지만 매크로를 사용하기 전에 매크로를 사용하지 않고도 같은 결과를 얻을 수 있는 다른 방법을 신중하게 고려하라.
아래 사용패턴을 따르면 매크로의 문제점들을 회피할 수 있다. 매크로를 쓰려면 되도록 이를 따르자.
#define
하고,
사용 후에는 바로 #undef
하라.
#undef
를
사용하지 말고, 다른 유일한 이름의 매크로를 사용하라.
##
매크로를
사용하지 말라.
0
을, 실수는 0.0
을,
포인터는 nullptr
(혹은 NULL
) 을,
char는 '\0'
을 사용하라.
정수는 0
을, 실수는 0.0
을 사용하는 것은
명백하므로 논란이 없다.
포인터(변수를 가리키는 용도)에는 0
, NULL
,
nullptr
을 선택적으로 사용할 수 있다. C++11을 허용하는
프로젝트에서는 nullptr
을 사용하라. C++03 프로젝트에는 NULL이
포인터처럼 보이기 때문에 NULL을 권장한다. 사실 어떤 C++ 컴파일러는
유용한 경고메세지를 보여주는 것을 가능케 하는(예를 들면 sizeof(NULL)과
sizeof(0)이 같지 않은 상황) NULL에 대한 특별한 정의가 있다.
char에는 '\0'을 사용하라. 알맞은 타입이고 코드 가독성도 향상된다.
sizeof(type)
보다
sizeof(varname)
사용을 권장한다.
특정 변수의 사이즈가 필요할 때 sizeof(varname)
을
사용하라. sizeof(varname)
는 누군가가 그 변수의
타입을 변경했을 때에도 문제가 없다.
sizeof(type)
은 특정 변수의 사이즈와 관련 없는 곳
(예를 들면 외부나 내부의 데이터 포맷을 관리하는 것)에서 사용될 수 있다.
auto
를 사용하라.
코드 가독성을 향상시켜주는 곳에는 명시적인 타입 선언을 사용하고,
auto
는 지역 변수에만 사용하라.
auto
타입의 변수 표현식에서 변수 초기화에
사용된다. 복사를 통해 변수를 초기화 하거나 레퍼런스를 생성하는 데에
auto
를 사용할 수 있다.
C++의 타입네임은 템플릿이나 네임스페이스와 연관되어 있을 때에는
길고 복잡해진다. 예를 들면 아래 코드에서
auto
가 없으면 한 구문에서 별 의미도 없이
타입네임을 두 번 써야 하는 경우도 있다.
auto
를 사용하면 타입을 명시적으로 써야하는 부담을 덜고
적절한 변수를 바로 사용하는 것이 쉬워진다.
어떤 변수의 초기화가 멀리 선언되어 있는 다른 변수와 관련되어 있을
때에는 명시적인 타입을 사용하는 것이 명확할 때가 있다. 아래 구문에서
프로그래머가 auto
와 const auto&
의 차이를
잘 알지 못하면 원치 않게 복사된 값을 얻게 될 수 있다.
auto
와 C++11의 "중괄호 리스트 초기화"의 상호작용은
혼란스러울 수 있다. 아래 두 선언은 서로 다른 의미를 가진다.
x
는 int이고, y
는 중괄호 초기화 리스트이다.
invisible proxy도 마찬가지이다.
auto
변수가 인터페이스에 선언되어 있다면(예를 들어 헤더의
상수로), 프로그래머가 그 값을 바꾸려 했는데 타입이 바뀌어 버리면서
API가 근본적으로 변해버릴 수 있다.
auto
는 지역변수로만 사용해야 한다. 파일 범위나
네임스페이스 범위에서 사용하면 안 되고, 클래스 멤버로도 사용해서는
안 된다. auto
타입 변수에 중괄호를 사용한 초기화 구문을
사용해도 안 된다.
auto
키워드는 C++11 의 기능 중 하나인 리턴 타입을
함수 뒤에 쓰는 문법에도 사용된다. 이 문법은 사용하지 않는다.
C++03에서 (배열, 생성자 없는 구조체와 같은) aggregate 타입은 중괄호를
사용해서 초기화 될 수 있다.
C++11에서는 이 문법이 모든 데이터타입으로 확장되었다.
이런 중괄호를 사용한 초기화 형태를 "중괄호 리스트 초기화"라고 부른다.
아래에 몇가지 예제가 있다.
사용자 정의 타입 또한 중괄호 초기화 리스트로부터 자동 생성되는
initializer_list
를 받는 생성자를 정의할 수 있다.
마지막으로, 중괄호를 사용한 초기화는 initializer_list
생성자를 가지지 않는 일반적인 생성자를 호출할 수 있다.
중괄호 초기화 리스트를 auto 지역 변수에 할당하지 말라.
요소가 하나인 경우에 혼동스러울 수 있다.
std::function
혹은
std::bind
유틸리티를 사용하지 말라.
std::sort(v.begin(),
v.end(), [](string x, string y) { return x[1] < y[1]; });
. 람다는 C++11에서 다형성 래퍼(wrapper)인
std::function
을 비롯한 함수객체를 위한 유틸리티들과 함께
소개되었다.
std::function
, std::bind
는
일반적인 콜백 방식에 함께 사용될 수 있다. 함수를 인자로 쉽게
사용할 수 있게 해 준다.
람다 표현식, std::function
, std::bind
는
사용하지 말라.
boost/call_traits.hpp
의
Call Traits
boost/compressed_pair.hpp
의
Compressed Pair
boost/graph
의
The Boost Graph Library (BGL),
예외로 직렬화 (adj_list_serialize.hpp
),
병렬/분산 알고리즘과 데이터 구조
(boost/graph/parallel/*
,
boost/graph/distributed/*
)는 사용하지 말 것.
boost/property_map
의
프로퍼티 맵
, 예외로 병렬/분산 프로퍼티 맵은 사용하지 말 것
(boost/property_map/parallel/*
).
boost/iterator/iterator_adaptor.hpp
,
boost/iterator/iterator_facade.hpp
,
boost/function_output_iterator.hpp
boost/polygon/voronoi_builder.hpp
,
boost/polygon/voronoi_diagram.hpp
,
boost/polygon/voronoi_geometry_type.hpp
boost/bimap
의
비트맵
boost/math/distributions
의
통계적 분포와 함수
아래 라이브러리도 사용이 가능하지만, C++ 11 확장 라이브러리로 대체되었다.
boost/array.hpp
의
배열은
std::array
를 대신 사용하라.
boost/ptr_container
의
포인터
컨테이너 는
std::unique_ptr
의 컨테이너를 대신 사용하라.
C++11 표준은 이전 표준에 비해 상당히 복잡하고(1,300 페이지 대 800 페이지), 많은 개발자들에게 생소하다. 몇 가지 기능에 대한 가독성과 유지 보수에 대한 장기적인 효과도 알 수 없다. 특히 예전 버전의 툴을 사용하도록 강제되는 프로젝트의 경우, 우리는 C++11의 다양한 기능이 언제 모두 구현될 지 예측할 수 없다.
Boost 와 마찬가지로, 어떤 C++11 확장은 코드 가독성을 해치는 코드를 만든다. 예를 들자면 코드를 읽는 사람에게 유용할 수 있는 (타입 이름과 같은) 반복 코드를 제거할 수 있는 기능과 템플릿 메타프로그래밍을 장려하는 것 등이 있다. 어떤 확장은 기존 라이브러리가 제공하는 기능을 중복 제공해서 혼란과 변환 비용을 야기한다.
아래 명시된 기능 외에는 사용해도 된다. 사용이 금지된 기능을 이 문서의 다른 부분에서 추가적으로 언급할 수도 있다.
int foo();
대신에
auto foo() -> int;
로 쓰는 것. 기존의 많은 함수
선언과 일관성을 유지하고 싶기 때문이다.
<ratio>
).
비대한 템플릿 인터페이스 스타일과 엮일 수 있는 우려 때문이다.
<cfenv>
와 <fenv.h>
헤더.
많은 컴파일러가 안정적으로 지원하지 않기 때문이다.
std::function
,
std::bind
유틸리티.
일관성을 위한 가장 중요한 규칙은 이름 규칙을 통제하는 것이다. 이름의 스타일을 통해 요소의 선언을 찾지 않고도 해당 요소가 타입인지, 변수인지, 함수인지, 상수인지, 혹은 매크로인지 바로 알 수 있다. 우리 머리 속의 패턴매칭 엔진은 이러한 이름 규칙에 상당히 의존한다.
이름 규칙은 상당히 모호하지만 이 영역에서 개인의 선호도보다 일관성이 더 중요하다고 본다. 합리적이라고 생각하든 아니든 규칙은 지켜져야 한다.
가능하면 상세한 이름을 사용하라. 새로 읽는 사람이 즉시 이해할 수 있는 것이 글자 길이를 줄이는 것보다 훨씬 중요하다. 프로젝트에 관계되지 않은 사람이 익숙하지 않은 약어를 사용하지 말고 중간 글자를 지워서 축약하지 말라.
_
)
혹은 대쉬 (-
)를 포함할 수 있다. 반드시 언더스코어를
사용할 필요는 없고 프로젝트에서 사용하는 관례를 따른다.
사용 가능한 파일 이름:
my_useful_class.cc
my-useful-class.cc
myusefulclass.cc
myusefulclass_test.cc // _unittest와 _regtest는 deprecate되었다.
C++ 파일은 .cc
으로 끝나고 헤더 파일은 .h
로
끝난다.
db.h
와 같이 /usr/include
에 이미 존재하는
파일 이름은 사용하지 말라.
일반적으로 상세하게 파일 이름을 지으라. 예를 들면
http_server_logs.h
가 logs.h
보다 좋다.
FooBar
클래스를 위한 파일 이름은 foo_bar.h
,
foo_bar.cc
이다.
인라인 함수는 .h
에 있어야 한다. 인라인 함수의 코드가
짧으면 .h
안에 들어가고, 길다면 -inl.h
로
가야 한다. 클래스 안에 많은 인라인 코드가 있다면 3개의 파일로 분리한다.
-inl.h 파일 섹션 참조.
MyExcitingClass
,
MyExcitingEnum
.
클래스, 구조체, typedef, 열거형을 포함한 모든 타입에 대해 같은 규칙이 적용된다. 다음 예시처럼 타입 이름은 단어마다 대문자로 시작하며 언더스코어를 사용하지 않는다.
my_exciting_local_variable
,
my_exciting_member_variable_
.
예시:
데이터 멤버(인스턴스 변수 또는 멤버 변수)의 이름은 보통 변수처럼 소문자와 선택적인 언더스코어로 작성하지만, 항상 끝에 언더스코어를 붙인다.
구조체에 안에 있는 데이터 멤버는 클래스에 있는 데이터 멤버와 다르게 끝에 언더스코어를 붙이지 않고 보통 변수처럼 이름짓는다.
어떤 경우에 클래스와 구조체를 써야 할 지에 대해서는 구조체 대 클래스를 참고하라.
제한적으로 사용되어야 하는 전역 변수의 이름에 대한 특별한 규칙은 없다.
하지만 전역 변수를 사용할 때에는 g_
와 같이 로컬 변수와
쉽게 구분할 수 있는 접두어를 사용하는 것이 좋다.
k
로 시작하는 대소문자가 섞인 이름을 사용한다. 예를 들면
kDaysInAWeek
.
지역변수인지, 전역변수인지, 클래스의 일부인지와 상관 없이 모든 컴파일
시점 상수들은 다른 변수들과 조금 다른 이름 규칙을 사용한다.
k
로 시작하여 매 단어의 첫 글자를 대문자로 쓴다.
MyExcitingFunction()
,
MyExcitingMethod()
,
my_exciting_member_variable()
,
set_my_exciting_member_variable()
.
함수 이름은 대문자로 시작하여 각 단어의 첫 글자를 대문자로 쓰고, 언더스코어는 사용하지 않는다.
함수의 실행 중 크래시가 발생할 수 있다면 함수의 이름 뒤에 OrDie 를 붙인다. 이 규칙은 프로덕션 코드에서도 에러가 발생할 가능성이 어느 정도 있는 함수에 한해 적용한다.
접근자와 변경자 (get 과 set 함수)는 접근 또는 변경을 하려는 변수의
이름과 일치하는 이름을 사용한다. 다음 예제는
num_entries_
라는 인스턴스 변수가 포함된 클래스의
일부분이다.
아주 짧은 인라인 함수에선 소문자를 사용할 수 있다. 예를 들어 리턴값을 캐시하지 않고 루프 안에서 호출할 정도로 가벼운 함수라면 소문자 이름을 사용할 수 있다.
google_awesome_project
.
네임스페이스와 네임스페이스의 이름을 짓는 방식에 대해선 네임스페이스를 참고하라.
kEnumName
또는 ENUM_NAME
.
각각의 열거형 값은 상수처럼 이름짓는
것이 선호되지만 매크로처럼 이름짓는
것도 허용한다. 열거형의 이름 (예: UrlTableErrors
,
AlternateUrlTableErrors
)은 타입이므로 대소문자를 섞어서
사용한다.
2009년 1월까지 열거형 값의 스타일은 매크로 방식이었다. 이 방식은 매크로와 열거형 값의 충돌을 일으켰고, 상수 스타일의 이름을 선호하는 방식으로 규칙이 변경되었다. 새로 작성되는 코드는 상수 방식의 이름을 사용하는 것이 좋으나, 이전 방식의 이름이 실제로 컴파일 오류를 발생시키는 것이 아니라면 이전의 코드를 상수 스타일 이름으로 수정할 필요는 없다.
MY_MACRO_THAT_SCARES_SMALL_CHILDREN
.
전처리기 매크로를 참고하라. 일반적으로 매크로는 사용하지 않는 것이 좋다. 하지만 절대적으로 필요하다면 대문자와 언더스코어로만 이름을 짓는다.
bigopen()
open()
의 이름에 기반한 함수 uint
typedef
bigpos
pos
와 같은 형식의 struct
또는
class
sparse_hash_map
LONGLONG_MAX
INT_MAX
와 비슷한 상수 작성하는 것이 쉽지 않지만, 주석은 코드의 가독성을 유지하는 데 절대적으로 중요한 역할을 한다. 다음 규칙은 어디에 무엇을 주석으로 달아야 할 지를 설명한다. 하지만 기억할 점은 주석은 매우 중요하지만, 가장 좋은 코드는 스스로에 대해 설명을 할 수 있는 코드라는 것이다. 타입과 변수에 이해할 수 있는 이름을 짓는 것이 이상한 이름을 짓고 주석으로 설명하는 것보다 훨씬 좋다.
주석을 작성할 때에는 주석을 읽는 이를 위해서, 즉 그 코드를 보고 이해해야 하는 다음 작업자를 위해 작성하라. 관대함을 가지라 - 다음 작업자는 본인일 수도 있다!
//
또는 /* */
문법을 사용한다.
//
와 /* */
의 사용이 모두 허용되지만,
//
이 아주 많이 사용된다.
주석의 내용과 위치, 작성 방식에 일관성이 있어야 한다.
모든 파일은 라이선스 문안을 포함해야 한다. 프로젝트에서 사용하는 적절한 라이선스 문안을 선택하라. (예: Apache 2.0, BSD, LGPL, GPL).
만약 작성자 목록이 있는 파일에 상당한 변경을 한 경우, 작성자 목록을 삭제할 것을 고려하라.
모든 파일의 위쪽에는 파일의 내용을 설명하는 주석이 포함되어야 한다.
일반적으로 .h
파일은 그 파일 안에 정의된
클래스에 대한 설명와 그 클래스가 어떻게 사용되는지에 대한
설명을 포함해야 한다. .cc
파일은 구현에 대한
세부사항이나 다루기 힘든 알고리즘에 대한 논의를 담아야 한다.
구현 세부사항이나 알고리즘에 대한 논의가 .h
파일을
읽는 사람에게도 필요하다고 생각하면 헤더 파일에 넣을 수도 있으나,
.cc
에도 이에 대한 문서가 .h
파일에 있다는
것을 명시해야 한다.
.h
와 .cc
에 중복하여 주석을 사용하지 말라.
중복된 주석은 서로 다르게 변화할 것이다.
이미 파일 상단의 주석에 클래스에 대한 상세한 설명을 두었다면, "전체 설명을 보기 위해서 파일 상단을 보라"라고 부담 없이 적어도 된다. 단 어떤 형태로든 주석이 있어야 한다.
클래스가 가정하고 있는 동기화 가정이 있는 경우 그것을 설명하라. 만약 클래스의 인스턴스에 여러 개의 스레드에서 접근할 수 있는 경우, 멀티 스레드 사용을 둘러싼 규칙과 변하지 않는 사항을 설명하는 데 특별히 주의를 기울이라.
모든 함수 선언은 그 함수가 무엇이고 어떻게 사용하는지 설명하는, 바로 직전에 위치한 주석을 가져야 한다. 이 주석은 명령문("파일을 열어라")이 아니라 설명문("파일을 연다")이어야 한다. 주석은 함수를 설명하는 것이지, 함수에게 무엇을 하라고 말하는 것이 아니다. 보통 이 주석은 함수가 어떻게 그 작업을 수행하는지 설명하지 않는다. 대신 이 내용은 함수 정의의 주석에게 넘겨져야 한다.
함수 선언의 주석에서 언급되어야 하는 것들의 유형은
예제는 아래와 같다.
하지만 필요 이상으로 자세하거나 완벽하게 명백하도록 서술하지 말라.
아래에서 "그렇지 않은 경우 false
를 반환한다"고 말하는
것은 이미 함축된 것이기에 불필요하다는 점에 주목하라.
생성자와 소멸자에 주석을 달 때, 그 코드를 읽는 사람은 생성자와 소멸자가 무엇을 위한 것인지 알고 있음을 기억하라. 그러므로 단지 "이 객체를 소멸시킨다" 따위의 것을 쓰는 것은 쓸모가 없다. 생성자가 그의 인자로 무엇을 하는지 설명하고 (예를 들면 그들이 포인터의 소유권을 가지는 경우), 소멸자가 무엇을 해제하는 지 설명하라. 설명이 사소한 경우 그냥 주석을 생략하라. 소멸자가 헤더 주석을 가지지 않는 경우는 상당히 흔하다.
함수가 작업을 수행하는 방법 상 까다로운 부분이 있는 경우, 함수 정의가 설명하는 주석을 가져야 한다. 예를 들면 그 코드에서 사용하는 코딩 비결을 설명하거나, 함수 안에서 수행하는 작업 절차에 대한 개요를 제시하거나, 왜 가능한 다른 대안을 사용하지 않고 특정한 방식으로 함수를 구현했는지 함수 정의에서 설명할 수 있다. 예를 들면 함수의 전반부에는 lock을 획득하지만 후반부에서는 lock이 필요없는 이유를 언급할 수 있다.
.h
파일이나 혹은 어딘가에 있을, 함수 선언의 주석을 단지
반복하지 않아야 하는 데 주목하라. 함수가 무엇을 하는지
요약하여 요점을 되풀이하는 것은 문제 없으나, 주석의 요점은 함수가
그것을 어떻게 수행하느냐에 대한 것이어야 한다.
(인스턴스 변수나 멤버 변수라고도 불리는) 각각의 클래스 데이터 멤버는
그것이 무엇을 위한 것인지 설명하는 주석을 가져야 한다. 만약 변수가
null
포인터나 -1
과 같은, 특별한 의미의
표식 값을 가질 수 있는 경우 이를 설명해야 한다. 예를 들면,
데이터 멤버와 마찬가지로, 모든 전역 변수는 그것이 무엇이며 어디에 사용되는지 설명하는 주석을 가져야 한다. 예를 들면,
까다롭거나 복잡한 코드 조각은 그 앞에 주석을 포함해야 한다. 예를 들면,
또한 명백하지 않은 부분도 줄 끝에 주석을 가져야 한다. 이러한 줄 끝 주석은 2개의 스페이스 문자로 코드에서 분리되어야 한다. 예를 들면,
코드가 무엇을 하고 있는지 설명하는 주석과 함수가 리턴할 때 오류가 이미 로그에 기록되었음을 언급하는 주석이 모두 있음에 주목하라.
다음 줄에 여러 주석이 있는 경우, 종종 이들을 일렬로 세우는 것이 더 읽기 쉬울 수 있다.
null 포인터나 불리언 혹은 문자로 된 숫자 값을 함수에 전달할 때, 그것들이 무엇인지에 관해 주석을 추가하거나 상수를 사용하여 코드가 스스로 설명될 수 있게 만들어야 한다. 예를 들자면 아래를 비교해 보라.
대:
그렇지 않다면, 스스로 설명할 수 있는 변수나 상수들:
절대로 코드 그 자체를 설명하지 말아야 한다는 데 주목하라. 코드를 읽는 사람이 코드의 의도를 모름에도 불구하고 코드 작성자보다 C++을 더 잘 알고 있다고 가정하라.
주석은 일반적인 문장과 마찬가지로 읽기가 쉬워야 하고, 적절한 대소문자와 구두점이 사용되어야 한다. 대부분의 경우 완전한 문장이 파편화된 문장에 비해 이해하기가 쉽다. 코드 라인의 끝에 사용되는 주석은 격식을 갖추지 못할 수도 있지만 일관성은 있어야 한다.
세미콜론을 사용해야 할 자리에 콤마를 사용했다고 코드 리뷰어에게 지적당하는 현실에 좌절할 수도 있지만, 소스코드는 높은 수준의 명확함과 가독성을 유지해야 한다. 올바른 구두점, 철자, 문법의 사용은 그 목표를 달성하는데 도움을 준다.
TODO
주석을 사용하라.
TODO
주석에서 TODO
는 모두 대문자로
해야 하며, 이름, 이메일 혹은 다른 종류의 작성자를 판별할 수 있는
방법을 제공하여 문제점을 문의할 수 있도록 해야 한다. 콜론은 선택적으로
사용할 수 있다. 일관성 있는 TODO
포맷을 사용하여 필요할 때
올바른 상황 설명을 해 줄 사람을 찾을 수 있게 하는 것이 주 목적이다.
TODO
주석은 작성자가 그 문제를 고친다는 약속은 아니다.
그러므로 TODO
주석에 언제나 이름을 포함하라.
TODO
주석의 형태가 "미래에 무엇을 한다"라면,
상세한 날짜를 남기거나 ("2005년 11월까지 수정"),
상세한 이벤트를 기록하라 (모든 클라이언트가 XML 응답을 할 수 있게 되면
삭제).
DEPRECATED
주석으로 표시해 둔다.
DEPRECATED
를 모두 대문자로 쓴 주석을 사용하여
deprecate된 인터페이스를 표시할 수 있다. 주석은 인터페이스가 선언된
윗 라인이나 같은 라인에 쓰면 된다.
DEPRECATED
뒤에 이름, 이메일 혹은 다른 식별자를 괄호 안에
쓴다.
DEPRECATED
주석은 호출측의 문제점을 수정할 수 있도록
간결하고 명확하게 작성하여야 한다. C++에서는 deprecate된 함수를
인라인으로 구현하여 새 인터페이스를 호출하게 할 수 있다.
DEPRECATED
주석을 표시한다고 해서 호출 측의 문제가 자동으로
수정되는 것은 아니다. 호출측의 코드를 직접 수정하거나 다른 사람을
시켜서 deprecate된 인터페이스를 사용하지 않도록 조치를 취해야 한다.
새로 작성하는 코드는 deprecate된 인터페이스를 사용하지 말고, 새로운 인터페이스를 사용해야 한다. deprecate된 인터페이스를 대체할 인터페이스를 모를 경우에는 deprecate 주석을 작성한 사람에게 새로운 인터페이스를 문의해서 사용해야 한다.
코딩 스타일과 포매팅은 제멋대로인 경우가 많지만, 모두가 통일된 스타일을 쓴다면 프로젝트를 파악하기가 훨씬 쉬워진다. 개개인이 모든 포매팅 규칙에 다 동의하기는 어렵고, 어떤 규칙은 익숙해지는데 시간이 걸리지만, 프로젝트의 구성원들이 규칙을 따라서 다른 사람의 코드를 쉽게 이해하도록 하는 것은 중요하다.
코드를 형식에 맞게 작성하는 것을 돕기 위해 emacs 설정파일 을 만들었다.
이 규칙에 여러 이견이 있지만, 기존의 많은 코드들이 이미 이 규칙을 따르고 있고, 우리는 일관성이 중요하다고 생각한다.
80 문자가 최대이다.
예외: 주석이 명령어 예시 또는 80 문자를 넘는 URL을 포함한다면 그 줄은 80 문자를 넘을 수 있다. 이것은 잘라내기와 붙여넣기를 쉽게 하기 때문이다.
예외: 긴 경로를 가진 #include
문은 80 문자를 넘을 수
있다. 이런 경우가 필요하지 않도록 노력하라.
예외: 헤더의 가드 는 최대 길이의 제한을 고려하지 않아도 된다.
설사 영어라 할 지라도 소스 코드에서 사용자가 보게 되는 글자들을 하드 코딩하지 않아야 한다. 그래서 ASCII가 아닌 문자를 자주 사용하지 않아야 한다. 그러나 어떤 경우 코드에 이러한 ASCII가 아닌 문자를 포함하는 것이 적절할 수 있다. 예를 들면, 외국어로 된 데이터 파일을 파싱하는 경우, 그 데이터 파일에서 구분자로 사용한 ASCII가 아닌 문자열을 하드 코딩하는 것이 적절할 수 있다. 더 일반적으로, (다국어화가 필요 없는) 유닛테스트 코드는 ASCII가 아닌 문자열을 포함할 수 있다. 그런 경우 ASCII 이상을 지원하는 대부분의 도구들이 이해하는 인코딩인 UTF-8을 사용해야 한다.
헥사 인코딩도 가독성을 좋게 하는 곳이라면 써도 좋다. 예를 들면
"\xEF\xBB\xBF"
나, 더 간단하게 u8"\uFEFF"
는
길이가 0이고 줄 바꿈이 없는 유니코드 공백 문자로 UTF-8로 소스에
포함된다면 보이지 않을 것이다.
\uXXXX
이스케이프 시퀀스를 포함한 문자열이 UTF-8로
인코딩되었음을 보증하기 위해 u8
접두어를 사용하라. UTF-8로
인코딩된 ASCII가 아닌 문자들을 포함한 문자열에는 이 접두어를 사용하지
말라. 컴파일러가 소스 파일을 UTF-8로 해석하지 않을 경우 잘못된
결과를 내놓을 수 있기 때문이다.
C++11의 char16_t
와 char32_t
를 사용하지 말라.
이것들은 UTF-8이 아닌 문자를 지원하기 위한 것들이다. 비슷한 이유로
(wchar_t
를 광범위하게 사용하고 있는 윈도우 API를 사용하는
코드를 작성하는 것이 아니라면) wchar_t
를 사용하지 말라.
들여쓰기를 위해 스페이스를 사용한다. 코드에서 탭을 사용하지 말라. 탭을 눌렀을 때 스페이스를 입력하도록 편집기를 설정하라.
함수들은 이렇게
한 줄에 넣기에 글자가 너무 많다면
혹은 첫번째 인자도 맞추기 힘들다면
몇 가지 주의할 점
만약 몇몇 인자들이 사용되지 않으면, 함수 선언에서 변수 이름을 주석처리하라.
함수 호출은 다음과 같은 형식으로 작성한다.
인자들이 모두 한 줄에 들어갈 자리가 없다면 여러 줄로 나누어 쓰되 이어지는 줄은 첫번째 인자와 같은 열에 오도록 한다. 여는 괄호 다음과 닫는 괄호 앞에는 스페이스를 추가하지 않는다.
함수의 인자가 많은 경우 가독성을 위해 인자마다 한 줄씩 쓰는 것을 고려하라
모든 인자들마다 줄바꿈하여 한 줄에 하나씩 쓰는 것도 가능하다.
특히 이 방식은 함수 시그너쳐가 길어서 최대 줄 길이에 맞출 수 없을 때 사용하는 것이 좋다.
중괄호 리스트가 어떤 이름(타입이나 변수 이름)에 이어져서 사용되는 경우
{}
가 그 이름을 가진 함수를 호출하는 괄호인 것처럼
작성한다. 만약 그러한 이름이 없으면 길이가 0인 이름이 있다고 가정한다.
else
키워드는 새 줄에서 사용한다.
기본적인 조건문에는 두 가지의 허용가능한 포맷이 있다. 첫 번째는 괄호 사이에 스페이스를 넣는 것이고 두 번째는 넣지 않는 것이다.
가장 흔한 형식은 스페이스를 쓰지 않는 것이다. 두 가지 모두 괜찮으나, 일관되어야 한다. 어떤 파일을 수정하고 있다면 그 파일에 이미 사용되고 있는 형식을 사용하라. 새로운 코드를 작성하고 있다면 해당 디렉터리나 프로젝트의 다른 파일에서 사용하고 있는 형식을 사용하라. 이미 사용되고 있는 형식을 알 수 없고 개인적인 선호도 없는 경우 스페이스를 넣지 말라.
만약 괄호 사이에 스페이스를 넣는 것을 선호한다면,
모든 경우에 if
와 여는 괄호 사이에는 스페이스 하나가
있어야 한다. 중괄호를 사용하는 조건문의 경우 닫는 괄호와 중괄호
사이에도 스페이스 하나를 사용해야 한다.
짧은 조건문은 가독성을 향상시키는 경우 한 줄에 작성될 수 있다.
조건문의 길이가 짧고 else
를 사용하지 않는 경우에만 이
방식을 사용할 수 있다.
else
가 있으면 이 방식을 사용할 수 없다.
일반적으로 한 줄짜리 구문의 경우 중괄호를 꼭 사용하지 않아도 되며,
선호하는 경우 사용하여도 된다. 복잡한 조건이나 구문들로 이루어진
조건문과 반복문의 경우 중괄호가 있을 때 가독성이 더 좋을 수 있다. 어떤
프로젝트는 if
문에 항상 중괄호를 사용할 것을 요구하기도
한다.
하지만 만약 if
-else
문 중 한 쪽이 중괄호를
사용하고 있다면 다른 쪽도 반드시 사용해야 한다.
{}
또는
continue
를 사용해야 한다.
switch
문의 case
블록은 선호에 따라 중괄호를
사용할 수도 있고 그렇지 않을 수 있다. 중괄호를 사용하는 경우엔 아래와
같은 위치에 사용해야 한다.
열거형 값에 대한 조건이 아닌 경우, switch 문은 항상
default
케이스를 포함하는 것이 좋다. (열거형의 경우
처리하지 않은 값에 대해 컴파일러가 경고할 것이다.) 만약
default
케이스가 실행되지 말아야 할 경우 간단히
assert
하라.
비어 있는 루프는 세미콜론 하나가 아닌 {}
또는
continue
를 사용하는 것이 좋다.
다음은 올바르게 작성된 포인터와 레퍼런스 표현식의 예이다.
다음에 주목하라.
*
이나 &
뒤에는
스페이스를 사용하지 않는다.
포인터 변수나 인자를 선언할 때 *을 타입에 붙여 써도 되고, 변수 이름에 붙여 써도 된다.
하나의 파일 안에서 일관되게 작성해야 한다. 그러므로 파일을 수정하는 경우 그 파일이 사용하는 스타일을 사용하라.
이 예시에서는 논리적 AND 연산자가 항상 줄의 마지막에 있다.
이 예시의 줄바꿈에서 &&
연산자가 모두 줄의
마지막에 있었음에 주목하라. 이 방식은 구글의 코드에서 일반적이나, 모든
연산자가 줄의 시작 부분에 오도록 줄바꿈하는 것도 허용된다. 가독성을
높이는 것에 도움이 될 경우 추가적인 괄호를 적절히 사용하여도 좋다.
또한 and
나 compl
과 같은 단어로 된 연산자보다
&&
와 ~
와 같은 기호 연산자를 사용하는
것이 좋다.
return
표현식을 불필요하게 괄호로 묶지 않아야 한다.
return expr;
에서의 괄호는 x = expr;
와 같은
경우에만 사용한다.
=
, ()
, {}
중에서 선택할 수 있다.
=
, ()
, {}
를 적절히 선택할 수
있다. 다음 경우가 모두 허용된다.
initializer_list
생성자가 있는 타입에서 {}
를
사용할 때는 주의해야 한다. {}
문법은 가능한 한
initializer_list
생성자가 우선시된다.
initializer_list
가 아닌 생성자를 사용하고자 하는 경우
()
를 사용하라.
또한 중괄호 형식은 정밀도를 낮추는 타입 캐스트를 막아주므로 이러한 프로그래밍 에러를 미연에 방지할 수 있다.
전처리기 지시자가 들여쓰기된 코드의 내부에 있을 때에도 줄의 처음부터 시작해야 한다.
public
, protected
, private
순서로
각각 스페이스 하나씩 들여쓰기하여 클래스 본문을 구성한다.
기본적인 포맷은 다음과 같다. (주석은 제외한다. 주석에 대해서는 클래스 주석을 참조하라.)
주의할 점:
public:
, protected:
,
private:
키워드는 스페이스 하나로 들여쓰기한다.
public
부분이 처음으로 와야 하고, 그 다음으로
protected
부분이, 마지막으로 private
부분이 와야 한다.
다음은 초기화 리스트로 허용가능한 두 가지의 포맷이다.
혹은
네임스페이스는 들여쓰기 단계를 증가시키지 않는다. 예를 들면
네임스페이스 내부에 추가적으로 들여쓰기를 하지 않는다.
중첩된 네임스페이스를 사용할 경우 각각을 한 줄에 작성한다.
줄 끝에 공백 문자를 붙이면 같은 파일을 편집하는 다른 사람이 기존의 뒤쪽 공백 문자를 삭제할 경우, 파일 병합 시에 할 일이 늘어난다. 그러므로 줄 끝에는 공백문자를 사용하지 말라. 이미 해당 줄을 수정하고 있다면 삭제하고, 그렇지 않다면 (가능하면 다른 사람이 그 파일을 사용하고 있지 않을 때) 별도의 정리 작업을 통해서 삭제하라.
이것은 규칙보단 원칙에 가깝다. 꼭 필요한 경우가 아니면 빈 줄을 사용하지 말라. 특히 함수 사이에 한 개 또는 두 개보다 많은 빈 줄을 추가하지 말라. 함수를 시작할 때 비어 있는 줄로 시작하지 않도록 노력하고, 끝낼 때에도 빈 줄을 사용하지 않아야 한다. 함수 내부에서도 빈 줄 사용을 절제하라.
기본 원칙은 더 많은 코드가 스크린에 들어올수록 프로그램의 제어 흐름을 따라가고 이해하기가 쉬워진다는 것이다. 물론 지나치게 조밀한 코드도 지나치게 펼쳐진 코드만큼 가독성을 해칠 수 있으므로 각자 판단에 따르도록 한다. 하지만 세로 공백의 사용을 전반적으로 최소화하라.
빈 줄 사용과 관련해 유용할 수 있는 몇 가지 기본 규칙:
지금까지 설명한 코딩 컨벤션은 강제적이지만 다른 모든 좋은 규칙들처럼 이것에도 예외사항이 있으며, 그 예외사항을 여기에서 다룬다.
이 가이드가 아닌 다른 규칙에 따라 작성된 코드를 수정하는 경우엔 이 규칙이 아닌 그 코드의 컨벤션에 따라 일관성있게 코드를 작성해야 한다. 어떻게 작성해야 할 지 명확하지 않은 경우 그 코드의 원작자나 현재 담당자에게 문의하라. 일관성은 파일 내의 일관성도 포함한다는 것을 기억하라.
일반적인 윈도우 스타일의 코드에 익숙하다면 잊기 쉬운 가이드 라인의 몇몇 부분을 복습하는 것이 유익하다.
iNum
로 이름 짓는 등의) 헝가리안
표기법을 사용하지 않는다. 소스 파일의 .cc
확장자를
포함하여 구글의 작명 컨벤션을 사용해야 한다.
DWORD
, HANDLE
등과 같은
그들만의 타입 동의어를 정의한다. 이것들을 윈도우 API 함수를
호출할 때 사용하는 것은 전혀 문제가 없고 사용이 권장된다.
하지만 가능한 실제 C++ 타입과 가장 가까운 것을 사용하도록 한다.
예를 들면, LPCTSTR
보다 const TCHAR *
를
사용하는 것이 좋다.
#pragma once
를 사용하지 말고, 구글의 표준 인클루드
가드를 사용하라. 인클루드 가드에 사용되는 경로는 프로젝트 트리를
기준으로 하는 상대경로를 사용하라.
#pragma
나
__declspec
같은 비표준 확장의 사용을 지양하라.
__declspec(dllimport)
와
__declspec(dllexport)
의 사용은 허용한다.
하지만 코드를 공유할 때 손쉽게 비활성화할 수 있도록
DLLIMPORT
와 DLLEXPORT
와 같은
매크로를 통해 사용해야 한다.
하지만 윈도우에서 규칙을 지키지 않아도 되는 것도 몇몇 있다.
_ATL_NO_EXCEPTIONS
를 정의하여 예외를 비활성화할 수
있다. STL에서도 예외를 비활성화할 수 있는 옵션이 있는지 알아보고
가능하면 비활성화하는 것이 좋다. 그렇지 않은 경우 컴파일러의 예외
옵션을 켜 두는 것은 허용한다. (하지만 이것은 오로지 STL이
컴파일되도록 하기 위함이고, 여전히 예외 처리 코드는 작성하지
않아야 한다.)
Stdafx.h
나 precompile.h
를
인클루드하는 것이다. 다른 프로젝트와의 코드를 쉽게 공유할 수
있도록 (precompile.cc
)를 제외하고는 이 파일을
명시적으로 인클루드하는 것을 피하고 /Fl
컴파일러
옵션을 통해 자동으로 인클루드되도록 하라.
resources.h
라는 이름이 붙는 리소스 헤더들은
매크로만을 포함하므로 이 스타일 가이드라인의 제약을 받지 않는다.
상식적이고 일관성있게 작성하라.
코드를 수정하는 경우 몇 분 정도 주변의 코드를 살펴보고 그것의 스타일을
판단하라. 그 코드가 if
문 주변에 스페이스를 사용한다면
그것에 따라야 한다. 만약 그 코드의 주석들이 작은 박스와 별표들을 사용한다면
새로 작성하는 코드도 작은 박스와 별표를 사용해야 한다.
스타일 가이드라인의 요점은 코딩에 있어서 공통적인 어휘를 가짐으로써 사람들이 서로의 말하는 방식보다 내용에 집중할 수 있게 하기 위함이고, 여기에 전체적인 스타일 규칙을 소개하는 것은 사람들이 그 어휘를 알게 하기 위함이다. 하지만 부분적인 스타일 규칙도 중요하다. 어떤 파일에 다른 부분과 심하게 달라 보이는 코드를 추가하는 경우 그 불연속성은 다른 사람들이 그 코드를 읽는 리듬을 벗어나게 할 것이다. 그렇게 하지 말자.
그러면 코드를 쓰는 방법에 대해서 충분히 쓴 것 같다. 코드 자체가 더 재미있을 것이다. 즐거운 코딩!
Revision 3.274
Benjy Weinberger