Google C++ Style Guide
2. Scoping
.cc(.cpp) 파일에 이름없는 namespace를 사용한다. 프로젝트나 경로를 기반으로 namespace의 이름을 정한다. using 명령어를 사용하지 않는다.
정의 :
이름 공간(Namespace)은 개체를 구분할 수 있는 범위를 나타내는 말로 일반적으로 namespace는 전역공간에서 이름 충돌을 하지 못하도록 하는데 유용하다.
장점 :
namespace는 (계층적) 네임 축(axis of naming) 을 제공하며 클래스에 의해 제공되는 네임 축도 포함된다. 예를 들어 전역 공간에 두 개의 다른 프로젝트에 클래스 Foo가 존재한다면, 런타임 또는 컴파일 타임 시 충돌이 발생한다. 이 충돌을 방지하기 위해서 각 프로젝트에 project1::Foo 나 project2::Foo namespace를 사용하면 된다.
단점 :
namespace는 추가적으로 (계층적) 네임 축(axis of naming) 을 제공하고 클래스에 의해 제공되는 네임 축도 포함되기 때문에, 혼동될 수 있다.
그리고 헤더파일에 이름없는 스페이스(unnamed spaces) 사용은 C++ One Definition Rule (ODR)을 위반할 여지가 있다.
결론 :
namespace의 사용은 아래의 정책을 따른다.
Unnamed Namespaces
- 이름없는 namespace는 런타임 시 이름 충돌을 피하기위해 .cc(.cpp)파일에 사용을 권장한다.
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 |
그러나, 특별한 클래스와 연관된 파일범위(file-scope) 선언은 이름없는 namespace의 멤버로서라기 보다 정적 데이터 멤버, 또는 정적 멤버 함수로서 클래스 안에 선언될 수 있다. 이름없는 namespace 끝에 // namespace 주석을 쓴다.
- .h 헤더파일에서는 이름없는 namespace를 사용하지 않는다.
Named Namespaces
이름있는 namespace는 아래처럼 사용한다.
- namespace는 헤더파일 ,gflags 정의/선언, 클래스 전방선언 이후에 전체 소스파일을 감싼다.
.h file |
namespace mynamespace { // All declarations are within the namespace scope. // Notice the lack of indentation. class MyClass { public: ... void Foo(); }; } // namespace mynamespace |
.cc(.cpp) file |
namespace mynamespace { // Definition of functions is within scope of the namespace. void MyClass::Foo() { ... } } // namespace mynamespace |
일반적으로 .cc(.cpp) 파일은 다른 namespace 안에 참조클래스가 포함되면 더 복잡하다.
.cc(.cpp) file |
#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 |
- namespace std 내에 어떤 것도 선언하지 않는다. 표준 라이브러리 클래스의 전방선언도 하지 않는다. std안에 개체를 선언하는 것은 이식성이 없다는 등의 미정의 행동을 보일 것이다.
표준라이브러리부터 개체를 선언하기 위해서 적절한 헤더파일을 포함한다.
- 이용 가능한 namespace로부터 모든 네임을 만들기 위해 using 연산자를 사용하지 말것
// Forbidden -- This pollutes the namespace. |
- .cc(.cpp) 파일과 헤더 파일의 함수, 메소드, 클래스 내부에서는 using연산자를 사용해도 된다.
// OK in .cc files. // Must be in a function, method or class in .h files. using ::foo::bar; |
- namespace alias(별칭)은 .cc(.cpp) 파일 내부 어디에서도 가능하며, .h 파일 내의 named namespace 내부의 어디에서도 가능하다. 또한 함수와 메소드 내에서도 가능하다.
// 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 파일내의 alias(별칭)은 이 파일을 #include하는 모든 파일에 보인다. 따라서 프로젝트 외부의 public header와 그것을 #include하는 header들은 별칭 정의를 피해야 한다. Public API들은 별칭을 가능한 작게 유지한다.
인터페이스의 일부분으로서 public nested class를 사용해도 좋지만, namespace가 전역 범위 외부에서 선언되어야 한다.
정의 :
클래스는 다른 클래스 안에서 정의될수 있다. 그 클래스를 멤버 클래스라고 부른다.
class Foo { private: // Bar is a member class, nested within Foo. class Bar { ... }; }; |
장점 :
중첩된(혹은 멤버) 클래스는 그 클래스를 감싸고 있는 클래스에 의해 내부에서만 사용될 때 유용하다. 중첩 클래스는 그 클래스를 감싸고 있는 클래스 내부에서 전방 선언될 수 있으며, 중첩 클래스 정의를 포함시키지 않기 위해 .cc(.cpp) 파일에서 정의한다.
단점 :
중첩된 클래스는 그 클래스를 감싼 클래스 내부에서만 전방선언이 가능하다. 그래서 Foo::Bar*를 조작하는 모든 헤더 파일에서 Foo를 위한 전체 선언을 include 해야 한다.
결론 :
실제 인터페이스의 부분으로서 사용되지 않는다면 중첩 클래스(nested class)를 만들지 않는다.
(e.g, 어떤 메소드의 옵션들을 갖고 있는 클래스)
C. Nonmember, Static Member, and Global Functions (비멤버/정적멤버/전역 함수)
전역 함수보다 namespace 내부 또는 정적(static) 멤버 함수를 선호한다. 전역 함수는 거의 사용하지 않는다.
장점 :
비멤버 함수와 정적 함수는 어떤 상황에서 유용하다. namespace 내부에서 비멤버 함수를 사용하면 전역 namespace의 오염을 피할 수 있다.
단점 :
비멤버 함수와 정적 멤버 함수는 새로운 클래스의 멤버 함수로서 오해할 여지가 있다. 특히 외부 리소스를 접근하거나 의미있는(significant) 종속성을 갖는 경우이다.
결론 :
함수 정의 시 클래스 인스턴스화 할 필요가 없는 경우가 있다. 그러한 함수를 비멤버 함수나 정적 멤버 함수로 만든다. 비멤버 함수는 외부 변수에 의존성이 없어야 하며, 거의 항상 namespace 내부에 존재해야 한다. 정적 데이터를 공유하지 않는 정적 멤버 함수를 그룹화하기 위해 클래스를 만드는 것보다, 그대신 namespace를 사용한다.
생산(production) 클래스로서 같은 컴파일 단위에서 정의된 함수들은 다른 컴파일 유닛으로부터 직접 호출됐을 때 불필요한 coupling 과 link-time 의존성 문제가 발생할 지도 모른다. 정적 멤버 함수가 특히 그렇다. 가능하면 분리된 라이브러리 내에서는 namespace 내부에서 새로운 클래스를 만들거나 함수를 사용하는 것을 고려한다.
반드시 비멤버 함수를 정의 해야 하고, 그것이 .cc(.cpp) 파일에서만 필요하다면, 그 범위를 제한하기 위해 이름없는(unnamed) namespace 또는 정적 결합(static linkage) (eg, static int Foo() {…})를 사용한다.
함수 내에서 변수는 가능한 가장 작은 범위에 놓으며, 선언과 동시에 초기화 한다.
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) 을 정확하게 구현한다. (i의 범위는 for문 내부이다.)따라서 같은 범위 내의 다른 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); } |
위와 같은 경우에는 변수를 루프 밖에 선언한다.
Foo f; // My ctor and dtor get called once each. for (int i = 0; i < 1000000; ++i) { f.DoSomething(i); } |
E. Static and Global Variables (정적/전역 변수)
클래스 타입에 대해 정적/전역 변수는 사용하지 않는다. 생성자와 소멸자의 순서가 명확하지 않기 때문에 버그를 발견하기 어렵다.
전역 변수를 포함하여 정적 저장 기간(static storage duration)을 갖는 객체, 정적 변수, 정적 클래스 멤버 변수, 함수 정적 변수(function static variables)는 *Plain Old Data(POD)가 되어야 한다: int, char, float, pointer, arrays/struct of POD
*POD(Plain Old Data) : C++에서는 메모리 구조 측면에서 두 종류의 object를 갖는데, 하나는 C의 structure나 build-in type과 동일한 메모리 구조를 갖는 object이고 다른 하나는 다형성이나 생성자, 소멸자 등의 특성을 사용할 수 있는 object이다. 전자를 POD라 하고, 후자를 non-POD라 한다. POD object는 C의 구조체처럼 똑같이 사용할 수 있다. 즉, memcpy나 memset등을 사용할 수 있다. 그러나 non-POD 즉, 클래스를 초기화 하거나 복사하기 위해 memcpy/memset를 사용하면 명백한 표준 위반이다.
C++에서 클래스 생성자와 정적 변수 초기화 순서는 부분적으로만 명시되며 빌드할 때마다 변하기도 하기 때문에 버그 발견이 어렵다. 그래서 정적/전역 변수를 사용해선 안되며, 정적 POD 변수는 getenv()나 getpid()와 같은 함수처럼 다른 전역 변수에 의존하지 않는다면 함수의 리턴 값으로 초기화해서도 안된다.
마찬가지로, 소멸자 호출 순서는 생성자 호출 순서의 역순으로 정의되는데, 생성자 순서가 불명확하기 때문에 소멸자 호출 순서 역시 불명확하다. 예를 들어, 프로그램 종료 시점에 정적 변수는 파괴되겠지만, 다른 스레드에 의해 실행되는 코드가 그 변수에 접근하려고 시도하다 실패한다. 혹은 정적 string 변수 소멸자는 string에 대한 참조를 포함하고 있는 다른 변수의 소멸자보다 먼저 호출된다.
결론은 POD 데이터를 갖는 정적 변수는 사용가능 하지만, STL의 vector이나 string은 정적 변수로 선언하지 않는다. 대신 C array나 const char []를 사용한다.
만약 클래스 타입에 정적/전역 변수가 필요하다면, main()이나 pthread_once() 에서 절대 메모리 해제되지 않는 포인터를 초기화해서 사용한다. 이 포인터는 스마트 포인터가 아닌 raw 포인터이어야 한다. 스마트 포인터의 소멸자는 소멸자 호출 순서 문제가 발생할 지도 모른다.