본문 바로가기
C++/Google C++ Style Guide

Google C++ Style Guide : 범위(Scoping)

by 개발자J의일상 2022. 5. 16.
반응형

Scoping


네임스페이스 (Namespaces)

 

몇 가지 예외들을 제외하고는 네임스페이스에 코드를 배치한다.

네임스페이스는 프로젝트 이름과 가능한 경로를 기준으로 고유한 이름을 가져야 한다.

using 지시문을 사용하지 마라 (예 : using namespace foo)

인라인 네임스페이스를 사용하지 마라. 명명되지 않은 네임스페이스에 대해서는 내부 연계(Internal Linkage)를 참조하라.

 

정의:

네임스페이스는 전역 범위를 고유한, 명명된 범위로 세분화하므로 전역 범위에서 이름 충돌을 방지하는데 유용하다.

 

장점:

네임스페이스는 대부분의 코드가 합리적으로 짧은 이름을 사용할 수 있도록 하면서 대규모 프로그램에서 이름 충돌을 방지하는 방법을 제공한다.

 

예를 들어 두 개의 서로 다른 프로젝트에 전역 범위에 Foo 클래스가 있는 경우 이러한 symbol들은 컴파일 시간이나 런타임에 충돌할 수 있다. 

각 프로젝트가 자신의 코드를 네임스페이스에 배치하는 경우 project1::Fooproject2::Foo는 이제 충돌하지 않는 별개의 symbol이며 각 프로젝트의 네임스페이스 내의 코드는 prefix 없이 Foo를 계속 참조할 수 있다.

 

인라인 네임스페이스는 둘러싸는 범위에 이름을 자동으로 배치한다. 

예를 들어 아래 스니펫을 보자면

namespace outer {
inline namespace inner {
  void foo();
}  // namespace inner
}  // namespace outer

outer::inner::foo()outer::foo() 표현식은 서로 바꿔 사용할 수 있다.

인라인 네임스페이스는 주로 버전 간 ABI 호환성을 위한 것이다.

 

ABI란?
응용 프로그램과 운영체제, 응용 프로그램과 라이브러리 사이에 필요한 저 수준 인터페이스를 정의한다

C++11 부터 중첩 namespace 사용 시 namespace 앞에 inline키워드를 붙일 수 있다.

 

inline namespace로 지정하면, 외부 코드에서 outer namespace로 접근하면 inline namespace가 default namespace로 설정된다. 

이게 무슨말인지는 아래 예제를 보면 이해할 수 있다.

namespace CAR
{
    namespace VER1
    {
        class ElectricCar; // VER1 Class
    }

    inline namespace VER2
    {
        class ElectricCar; // VER2 Class
    }
 
    class GasCar;
}
 
// CAR::VER2::ElectricCar, VER2가 inline namespace이기 때문에 CAR namespace로 간주
CAR::ElectricCar* car1;

CAR::VER1::ElectricCar* car2;

CAR::VER2::ElectricCar* car3;

inline namespace를 통해 라이브러리 버전 관리를 쉽게 할 수 있다!

 

새로운 버전의 class를 구현하고 최신 버전을 default로 설정하기 위해 inline namespace를 사용하면 손쉽게 버전 교체가 가능하다.

이전 버전과 최신 버전의 성능 비교를 inline을 키워드만 붙여가면서 바로 교체 비교가 가능하다.

http://egloos.zum.com/sweeper/v/3148210

 

단점:

네임스페이스는 이름이 참조하는 정의를 파악하는 메커니즘을 복잡하게 만들기 때문에 혼동될 수 있다.

 

특히 인라인 네임스페이스는 이름이 실제로 선언된 네임스페이스로 제한되지 않기 때문에 혼동될 수 있다. (outer namespace로 호출이 가능한 점) 일부 더 큰 버전 관리 정책의 일부로만 유용하다.

 

일부 컨텍스트에서는 완전히 제한된 이름으로 symbol들을 반복적으로 참조해야 한다. 깊이 중첩된 네임스페이스의 경우 많은 혼란이 초래될 수 있다.

 

결정:

네임스페이스는 다음과 같이 사용해야 한다:

  • 네임스페이스 이름에 대한 규칙을 따른다.
  • 주어진 예제와 같이 주석으로 여러 줄의 네임스페이스를 종료한다.
  • 네임스페이스는 includes, 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 파일에는 flag 또는 using 선언과 같은 추가 세부 정보가 있을 수 있다.

#include "a.h"

ABSL_FLAG(bool, someflag, false, "dummy flag");

namespace mynamespace {

using ::foo::Bar;

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

}  // namespace mynamespace
  • 생성된 프로토콜 메시지 코드를 네임스페이스에 배치하려면 .proto 파일에서 패키지 지정자를 사용한다. 자세한 내용은 Protocol Buffer Package를 참조하라.
  • 표준 라이브러리 클래스의 전방 선언을 포함하여 namespace std에 아무것도 선언하지 마라. namespace std에 엔티티를 선언하는 것은 정의되지 않은 동작이다. 즉, 이식성이 없다. 표준 라이브러리에서 엔티티를 선언하려면 적절한 헤더 파일을 포함하라.
  • using namespace std를 사용하지 말고 std에서 사용할 부분만, 예를 들어 using std::string처럼 사용하라. 
  • 당신은 namespace의 모든 이름을 사용할 수 있도록 하기 위해 using 지시문을 사용할 수 없다.
// Forbidden -- This pollutes the namespace.
using namespace foo;
  • 헤더 파일의 네임스페이스로 가져온 모든 항목이 해당 파일에서 내보낸 공용 API의 일부가 되기 때문에, 명시적으로 표시된 내부 전용 네임스페이스를 제외하고 헤더 파일의 네임스페이스 범위에서 네임스페이스 별칭을 사용하지 마라. 아래 예제들은 사용이 가능한 예
// Shorten access to some commonly used names in .cc files.
namespace baz = ::foo::bar::baz;
// Shorten access to some commonly used names (in a .h file).
namespace librarian {
namespace impl {  // Internal, not part of the API.
namespace sidetable = ::pipeline_diagnostics::sidetable;
}  // namespace impl

inline void my_inline_function() {
  // namespace alias local to a function (or method).
  namespace baz = ::foo::bar::baz;
  ...
}
}  // namespace librarian
  • inline namespace를 사용하지 마라.

내부 연결 (Internal Linkage)

 

.cc 파일의 정의를 해당 파일 외부에서 참조할 필요가 없는 경우 이름 없는 네임스페이스에 배치하거나 static으로 선언하여 내부 연결 (internal linkage)를 제공해라. ".h 파일"에서 이러한 구성 중 하나를 사용하지 마라.

 

정의:

모든 선언은 이름 없는 네임스페이스에 배치하여 내부 연결을 제공할 수 있다.

함수와 변수는 static으로 선언하여 내부 연결을 제공할 수도 있다.

즉, 선언하는 모든 항목은 다른 파일에서 액세스 할 수 없다.

다른 파일이 동일한 이름을 가진 것을 선언하면 두 엔티티는 완전히 독립적이다.

 

결정:

다른 곳에서 참조할 필요가 없는 모든 코드에 대해 .cc 파일에서 내부 연결을 사용하는 것이 좋다. 

.h 파일에서는 내부 연결을 사용하지 마라.

 

명명된 네임스페이스와 같이 명명되지 않은 네임스페이스의 형식을 지정한다.

종료 주석에서 네임스페이스 이름을 비워 둔다.

namespace {
...
}  // namespace

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

 

비 멤버 함수를 네임스페이스에 배치하는 것을 선호하라; 완전히 전역 함수를 거의 사용하지 마라. 

단순히 정적 멤버를 그룹화하기 위해 클래스를 사용하지 마라. 

클래스의 정적 메서드는 일반적으로 클래스의 인스턴스 또는 클래스의 정적 데이터와 밀접하게 관련되어야 한다.

 

장점:

비 멤버 및 정적 멤버 함수는 일부 상황에서 유용할 수 있다.

비 멤버 함수를 네임스페이스에 넣으면 전역 네임스페이스가 오염되는 것을 방지할 수 있다.

 

단점:

비 멤버 및 정적 멤버 함수는 특히 외부 리소스에 액세스 하거나 상당한 종속성이 있는 경우 새 클래스의 멤버로 더 적합할 수 있다.

 

결정:

때로는 클래스 인스턴스에 바인딩되지 않은 함수를 정의하는 것이 유용하다.

이러한 함수는 정적 멤버 또는 비멤버 함수일 수 있다.

비멤버 함수는 외부 변수에 의존해서는 안 되며 거의 항상 네임스페이스에 존재해야 한다.

정적 멤버만 그룹화하기 위한 클래스를 만들지 마라; 이것은 이름에 공통의 prefix를 부여하는 것과 다르지 않으며 이러한 그룹화는 일반적으로 어쨌든 불필요하다.

 

비멤버 함수를 정의하고 .cc 파일에서만 필요한 경우 내부 연결을 사용하여 범위를 제한하자.

 

비멤버 함수에 대해 설명을 좀 더 추가하면 effective c++에 나오는 것 중 하나를 들고 왔다.

 

웹브라우저를 나타내는 클래스 WebBrowser가 있고 이 클래스는 3가지 기능을 제공한다.

1. 캐시를 비우는 함수 : clearCashe()

2. 방문 기록을 없애는 함수 : clearHistory()

3. 쿠키를 없애는 함수 : removeCookies()

 

이 3가지 기능을 한 번에 호출하는 clearEverything() 함수도 존재한다고 하자.

이를 코드로 나타내면

class WebBrowser {
public:
    void clearCashe();
    void clearHistory();
    void removeCookies();
    
    void clearEverything(); //call clearCashe(), clearHistory(), removeCookies()
};

void clearBrowser(WebBrowser& wb) {
    wb.clearCashe();
    wb.clearHistory();
    wb.removeCookies();
}

 

이때 비멤버 함수는 clearBrowser()이다. 

그럼 clearEverything()과 clearBrowser() 둘 중 어떤 걸 사용하는 게 더 좋을까?

 

바로 비멤버 함수 clearBrowser()를 사용하는 것이다.

 

왜냐하면 비멤버 버전이 더 캡슐화가 잘 되어 있고, WebBrowser 관련 기능을 구성하는 데 있어서 패키징 유연성이 높아지고, 이로 인해 컴파일 의존도도 낮아지고, WebBowser의 확장성을 높일 수 있다.

 

자세한 건 아래 블로그를 참고 바란다.

비멤버 함수
https://fistki.tistory.com/m/33

정적 멤버 함수
https://ansohxxn.github.io/cpp/chapter8-11/

지역 변수 (Local Variables)

 

가능한 가장 좁은 범위에 함수의 변수를 배치하고 선언에서 변수를 초기화한다.

 

C++에서는 함수의 어느 곳에서나 변수를 선언할 수 있다.

가능한 한 로컬 범위로 선언하고 처음 사용하는 곳에 가깝게 선언하는 것이 좋다.

이렇게 하면 독자가 선언을 찾고 변수 유형과 초기화 대상을 쉽게 확인할 수 있다.

특히 다음과 같이 선언 및 할당 대신 초기화를 사용해야 한다.

예:

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

 

std::vector<int> v;
v.push_back(1);  // Prefer initializing using brace initialization.
v.push_back(2);
std::vector<int> v = {1, 2};  // Good -- v starts initialized.

※ 이 부분은 클린코드에서 이야기하는 선언과 초기화를 동시에 하라는 부분과 비슷한 의도로 보인다.

 

if, while 및 for 문에 필요한 변수는 일반적으로 해당 statement 내에서 선언되어야 하므로 이러한 변수는 해당 범위로 제한되어야 한다.

예: 

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);
}

정적 변수와 전역 변수 (Static and Global Variables)

 

정적 저장 기간(Static storage duration)이 있는 객체는 사소하게 파괴할 수 없는 경우가 아니면 금지된다.

 

여기서 정적 저장 기간이란?

 

식별자가 저장소 클래스 지정자 thread_local 없이 선언되고 외부 또는 내부 연결이 있거나 저장소 클래스 지정자 static이 있는 객체는 정적 저장 기간을 갖는다. 수명은 프로그램의 전체 실행이며 저장된 값은 프로그램 시작 전에 한 번만 초기화된다.

 

저장된 값은 한 번만 초기화되기 때문에 정적 저장 기간을 가진 객체는 함수 호출을 프로파일링 할 수 있다.

 

static 키워드의 다른 용도는 파일 범위이다.

#include <stdio.h>
 
void f (void)
{
    static int count = 0;   // static variable   
    int i = 0;              // automatic variable
    printf("%d %d\n", i++, count++);
}
 
int main(void)
{
    for (int ndx=0; ndx<10; ++ndx)
        f();
}

Output:

0 0
0 1
0 2
0 3
0 4
0 5
0 6
0 7
0 8
0 9

f()가 호출 시 count는 계속해서 증가하는 것을 볼 수 있다.

https://en.cppreference.com/w/c/language/static_storage_duration

 

비공식적으로 이것은 소멸자가 멤버와 기본 소멸자를 고려해서 아무것도 하지 않는다는 것을 의미한다.

보다 형식적으로는 형식에 사용자 정의 또는 가상 소멸자가 없으며 모든 기본 및 비정적 멤버가 쉽게 소멸될 수 있음을 의미한다.

정적 함수 지역 변수는 동적 초기화를 사용할 수 있다.

정적 클래스 멤버 변수 또는 네임스페이스 범위의 변수에 대한 동적 초기화 사용은 권장되지 않지만 제한된 상황에서 허용된다.

자세한 내용은 아래를 참조하라.

 

경험상: 전역 변수는 선언이 분리되어 고려될 경우, constexpr이 될 수 있는 경우 이러한 요구 사항을 충족한다.

 

정의:

모든 객체에는 그것의 수명과 관련된 저장 기간이 있다. 

정적 저장 기간이 있는 객체는 초기화 시점부터 프로그램이 끝날 때까지 유지된다.

 

이러한 객체는 네임스페이스 범위의 변수("전역 변수"), 클래스의 정적 데이터 멤버 또는 static 지정자로 선언된 함수-로컬 변수로 나타난다.

함수-로컬 정적 변수들은 컨트롤이 그것들의 선언을 처음 통과할 때 초기화된다; 정적 저장 기간이 있는 다른 모든 객체들은 프로그램 시작의 일부로 초기화된다. 정적 저장 기간이 있는 모든 객체는 프로그램 종료 시 소멸된다. (결합되지 않은 스레드(unjoined threads)가 종료되기 전에 발생)

 

초기화는 동적일 수 있으며, 이는 초기화 중에 중요한 일이 발생한다는 것을 의미한다. (예를 들어, 메모리를 할당하는 생성자 또는 현재 프로세스 ID로 초기화되는 변수를 생각해라.)

다른 종류의 초기화는 정적 초기화이다. 그러나 둘은 완전히 반대되는 것은 아니다:

정적 초기화는 항상 정적 저장 기간 (주어진 상수 또는 0으로 설정된 모든 바이트로 구성된 표현으로 객체 초기화)이 있는 객체에서 발생하는 반면, 동적 초기화는 그 이후에 요청이 오면 일어난다.

 

장점:

전역 및 정적 변수는 많은 응용 프로그램에 매우 유용하다:

명명된 상수, 일부 translation unit 내부의 보조 데이터 구조, 명령줄 플래그, 로깅, 등록 메커니즘, 배경 인프라 등 

translation unit은 C++ compilation의 기본 단위입니다. <소스 파일 하나 + 직접/간접적으로 include 된 헤더 파일의 내용물(전처리기 조건에 따라 몇몇은 무시)>로 구성되어 있습니다.
translation unit 한 개는 object file, library나 실행 가능한 프로그램으로 컴파일될 수 있습니다.

https://hashcode.co.kr/questions/1244/translation-unit%EC%97%90-%EB%8C%80%ED%95%B4%EC%84%9C

 

단점:

동적 초기화를 사용하거나 중요한 소멸자가 있는 전역 및 정적 변수는 찾기 어려운 버그로 쉽게 이어질 수 있는 복잡성을 만든다.

동적 초기화는 번역 단위(translation unit) 간에 순서가 정해져 있지 않으며, 소멸도 마찬가지이다(단, 초기화의 역순으로 소멸이 발생한다는 점 제외).

하나의 초기화가 정적 저장 기간을 가진 다른 변수를 참조하는 경우, 수명이 시작되기 전(또는 그것의 수명이 끝난 후)에 객체에 액세스 할 수 있다.

게다가, 프로그램이 종료 시 조인되지 않은 스레드를 시작할 때 해당 스레드는 소멸자가 이미 실행된 경우 수명이 끝난 후 객체에 액세스를 시도할 수 있다.

 

결정:

Decision on destruction

소멸자가 사소한 경우에는 실행에 순서가 적용되지 않는다(실제로 "실행"되지 않음);

그렇지 않으면 수명이 끝난 객체에 액세스 할 위험에 노출된다. 따라서 우리는 사소하게 파괴할 수 있는 경우에만 정적 저장 기간을 가진 객체를 허용한다. 

포인터 및 int와 같은 기본 유형(Fundamental types)은 쉽게 파괴할 수 있는 유형의 배열과 마찬가지로 쉽게 파괴할 수 있다. constexpr로 표시된 변수는 쉽게 파괴 될 수 있다.

 

constexpr란?

C++14 : 완화된 constexpr 제약 (Relaxed constexpr restrictions)

 

C++14 : 완화된 constexpr 제약 (Relaxed constexpr restrictions)

완화된 constexpr 제약 (Relaxed constexpr restrictions) 먼저 constexpr은 C++11에 추가된 기능이다. 주요 아이디어는 런타임이 아닌 컴파일 시간에 계산을 수행하여 프로그램의 성능을 향상시키는 것이다. 개

mypark.tistory.com

http://egloos.zum.com/sweeper/v/3147813
const int kNum = 10;  // Allowed

struct X { int n; };
const X kX[] = {{1}, {2}, {3}};  // Allowed

void foo() {
  static const char* const kMessages[] = {"hello", "world"};  // Allowed
}

// Allowed: constexpr guarantees trivial destructor.
constexpr std::array<int, 3> kArray = {1, 2, 3};
// bad: non-trivial destructor
const std::string kFoo = "foo";

// Bad for the same reason, even though kBar is a reference (the
// rule also applies to lifetime-extended temporary objects).
const std::string& kBar = StrCat("a", "b", "c");

void bar() {
  // Bad: non-trivial destructor.
  static std::map<int, int> kData = {{1, 0}, {2, 0}, {3, 0}};
}

참조는 객체가 아니므로 소멸성에 대한 제약 조건이 적용되지 않는다.

그러나 동적 초기화에 대한 제약 조건은 여전히 적용된다. 

특히, static T& t = *new T; 형식의 함수-로컬 정적 참조는 허용된다.

 

Decision on initialization

초기화는 더 복잡한 주제이다.

이는 클래스 생성자의 실행 여부뿐만 아니라 초기화 프로그램의 평가도 고려해야 하기 때문이다.

int n = 5;    // Fine
int m = f();  // ? (Depends on f)
Foo x;        // ? (Depends on Foo::Foo)
Bar y = g();  // ? (Depends on g and on Bar::Bar)

 

첫 번째 명령문을 제외한 모든 명령문은 불확실한 초기화 순서에 노출된다.

 

우리가 찾고 있는 개념은 C++ 표준의 형식 언어에서 상수 초기화라고 한다.

초기화 표현식이 상수 표현식임을 의미하며 객체가 생성자 호출에 의해 초기화되면 생성자도 constexpr로 지정해야 한다.

struct Foo { constexpr Foo(int) {} };

int n = 5;  // Fine, 5 is a constant expression.
Foo x(2);   // Fine, 2 is a constant expression and the chosen constructor is constexpr.
Foo a[] = { Foo(1), Foo(2), Foo(3) };  // Fine

상수 초기화는 항상 허용된다.

정적 저장 기간 변수의 상수 초기화는 constexpr 또는 가능한 경우 ABSL_CONST_INT 속성으로 표시해야 한다.

그렇게 표시되지 않은 비 로컬 정적 저장 기간 변수는 동적 초기화가 있는 것으로 가정하고 매우 신중하게 검토해야 한다.

 

대조적으로 다음 초기화들에는 문제가 있다:

// Some declarations used below.
time_t time(time_t*);      // Not constexpr!
int f();                   // Not constexpr!
struct Bar { Bar() {} };

// Problematic initializations.
time_t m = time(nullptr);  // Initializing expression not a constant expression.
Foo y(f());                // Ditto
Bar b;                     // Chosen constructor Bar::Bar() not constexpr.

비지역 변수의 동적 초기화는 권장되지 않으며 일반적으로 금지되어 있다.

그러나 프로그램의 어떤 부분도 다른 모든 초기화와 관련하여 이 초기화 순서에 의존하지 않는 경우 허용한다.

이러한 제한 사항에서, 초기화 순서는 관찰 가능한 차이를 만들지 않는다.

 

예를 들어:

int p = getpid();  // Allowed, as long as no other static variable
                   // uses p in its own initialization.

정적 지역 변수의 동적 초기화가 허용된다 (공통).

 

Common patterns

  • 전역 문자열: 명명된 전역 또는 정적 문자열 상수가 필요한 경우, string_view의 constexpr 변수, 문자 배열, 또는 문자열 리터럴을 가리키는 문자 포인터 사용을 고려하라. 문자열 리터럴에는 이미 정적 저장 기간이 있으며 일반적으로 충분하다. TotW #140을 참고하라.
  • Maps, sets 및 기타 동적 컨테이너: 검색할 세트 또는 룩업 테이블과 같은 정적 고정 컬렉션이 필요한 경우 표준 라이브러리의 동적 컨테이너는 중요한 소멸자가 있기 때문에 정적 변수로 사용할 수 없다. 대신 간단한 유형의 배열(예: int에서 int로의 map) 또는 쌍들의 배열 (예: int와 const char*의 쌍) 소규모의 컬렉션의 경우 선형 검색은 메모리 인접성으로 인해 완전히 충분하며, 표준 작업을 위해 absl/algorithm/container.h의 기능을 사용하는 것을 고려하라. 필요한 경우 정렬된 순서대로 컬렉션을 유지하고 이진 검색 알고리즘을 사용한다. 표준 라이브러리의 동적 컨테이너를 정말 선호할 경우 아래 설명된 대로 함수 로컬 정적 포인터를 사용하는 것을 고려하라.
  • 스마트 포인터 (unique_ptr, shared_ptr): 스마트 포인터는 소멸 중에 정리를 실행하므로 금지된다. 사용 사례가 이 섹션에 설명된 다른 패턴 중 하나에 맞는지 고려하라. 한 가지 간단한 솔루션은 동적으로 할당된 객체에 대한 일반 포인터를 사용하고 절대 삭제하지 않는 것이다 (마지막 항목 참조).
  • 사용자 정의 유형의 정적 변수: 직접 정의해야 하는 유형의 정적 상수 데이터가 필요한 경우 유형에 사소한 소멸자와 constexpr 생성자를 제공하라.
  • 다른 모든 방법이 실패하면 객체를 동적으로 생성하고 함수 로컬 정적 포인터 또는 참조를 사용하여 객체를 삭제할 수 없다 (예: static const auto& impl = *new T(args...);).

스레드 지역 변수 (thread_local Variables)

함수 안에 선언되지 않은 thread_local 변수는 실제 컴파일 시간 상수로 초기화되어야 하며, ABSL_CONST_INIT 속성을 사용하여 적용해야 한다.

스레드 로컬 데이터를 정의하는 다른 방법보다 thread_local을 선호한다.

 

정의:

thread_local 지정자를 사용하여 변수를 선언할 수 있다.

thread_local Foo foo = ...;

이러한 변수는 실제로 객체의 모임이므로 다른 스레드가 이것에 액세스 할 때 실제로 다른 객체에 액세스 한다.

thread_local 변수들은 여러 면에서 정적 저장 기간 변수와 매우 유사하다.

예를 들어, 네임스페이스 범위, 함수 내부 또는 정적 클래스 멤버로 선언할 수 있지만 일반 클래스 멤버로는 선언할 수 없다.

 

thread_local 변수 인스턴스는 프로그램 시작 시 한 번이 아니라 각 스레드에 대해 별도로 초기화되어야 한다는 점을 제외하고는 정적 변수와 매우 유사하게 초기화된다.

즉, 함수 내에 선언된 thread_local 변수는 안전하지만 다른 thread_local 변수는 정적 변수(와 그 외)와 동일한 초기화 순서 문제의 영향을 받는다.

 

thread_local 변수 인스턴스들은 그들의 스레드가 종료되기 전에 소멸되지 않으므로 정적 변수의 소멸 순서 문제가 없다.

 

장점:

  • 스레드 로컬 데이터는 본질적으로 경쟁으로부터 안전하다 (보통 하나의 스레드만 데이터에 액세스 할 수 있기 때문에). 따라서 스레드 로컬은 동시 프로그래밍에 유용하다.
  • thread_local은 스레드 로컬 데이터를 생성하는 유일한 표준 지원 방법이다.

단점:

  • thread_local 변수에 액세스 하면 예측할 수 없고 제어할 수 없는 양의 다른 코드가 실행될 수 있다.
  • thread_local 변수는 사실상 전역 변수이며, 스레드 안전성이 없다는 점을 제외하고 전역 변수의 모든 단점을 가지고 있다.
  • thread_local 변수가 사용하는 메모리는 실행 중인 스레드 수(최악의 경우)에 따라 확장되며, 이는 프로그램에서 상당히 클 수 있다.
  • 비정적 데이터 멤버는 thread_local일 수 없다.
  • thread_local은 특정 컴파일러 내장 함수만큼 효율적이지 않을 수 있다.

결정:

함수 내부의 thread_local 변수는 안전성 문제가 없으므로 제한 없이 사용할 수 있다.

함수 범위 thread_local을 사용하여 이를 노출하는 함수 또는 정적 메서드를 정의하여 클래스 또는 네임스페이스 범위 thread_local을 시뮬레이션할 수 있다.

Foo& MyThreadLocalFoo() {
  thread_local Foo result = ComplicatedInitialization();
  return result;
}

클래스 또는 네임스페이스 범위의 thread_local 변수는 실제 컴파일 시간 상수로 초기화되어야 한다(즉, 동적 초기화가 없어야 함).

이를 적용하려면 클래스 또는 네임스페이스 범위의 thread_local 변수에 ABSL_CONST_INIT(또는 constexpr, 하지만 드물게) 주석을 추가해야 한다. 

ABSL_CONST_INIT thread_local Foo foo = ...;

thread_local은 thread-local 데이터를 정의하기 위한 다른 메커니즘보다 우선되어야 한다.

 


https://google.github.io/styleguide/cppguide.html#Namespaces

300x250

댓글