본문 바로가기

C/C++

[Google C++ Style Guide] 2. Scoping

Google C++ Style Guide

 

 

2. Scoping

A.     Namespaces (네임 스페이스)

.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 { kUnusedkEOFkError };       // 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(someflagfalse"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.
using namespace foo;

 

- .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들은 별칭을 가능한 작게 유지한다.

 

 

B.      Nested Classes (중첩 클래스)

인터페이스의 일부분으로서 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() {…})를 사용한다.

 

 

D.     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) 을 정확하게 구현한다. (i의 범위는 for문 내부이다.)따라서 같은 범위 내의 다른 for문에서 i를 재사용 할 수 있다. if문과 while문도 마찬가지이다.

 

while (const charp = strchr(str'/')) str = p + 1;

 

한가지 위험 부담이 있다변수가 객체라면생성자는 범위 내로 들어갈 때마다 생성자를 호출하고 범위를 벗어날 때마다 소멸자를 호출한다.

아래와 같이 구현하지 않는다.

 

// Inefficient implementation:

for (int i = 0i < 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 = 0i < 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 포인터이어야 한다스마트 포인터의 소멸자는 소멸자 호출 순서 문제가 발생할 지도 모른다.