저장 클래스 지정자

저장 클래스 지정자 (Storage class specifiers)

변수의 저장 기간과 링크를 제어하고 싶을 때 쓰는 키워드가 저장 클래스 지정자(저장 스펙시파이어)예요. 이 페이지에서는 저장 기간의 종류와 각 지정자가 어디에 붙을 수 있는지를 정리하고, 링크의 개념까지 함께 다룰게요.

출처: cppreference

본문

저장 클래스 지정자는 이름의 선언 문법에서 decl-specifier-seq의 일부예요. 이름의 범위와 함께, 이름의 두 독립적인 속성인 *저장 기간(storage duration)*과 *링크(linkage)*를 제어해요.

저장 기간 (Storage duration)

저장 기간은 객체를 담는 저장소의 최소 잠재 수명을 정의하는 객체의 속성이에요. 저장 기간은 객체를 만드는 데 쓰인 구문에 의해 결정되며 다음 중 하나예요:

  • static 저장 기간
  • thread 저장 기간 (C++11부터)
  • automatic 저장 기간
  • dynamic 저장 기간

static, thread(C++11부터), automatic 저장 기간은 선언이 도입한 객체들과 임시 객체에 연결돼요. dynamic 저장 기간은 new 표현식이 만든 객체나 암묵적으로 생성된 객체에 연결돼요.

저장 기간 범주는 참조에도 적용돼요.

하위 객체와 참조 멤버의 저장 기간은 그 완전 객체의 저장 기간과 같아요.

지정자 (Specifiers)

다음 키워드들이 저장 클래스 지정자예요:

- auto (C++11 이전)
- register (C++17 이전)
  • static
  • thread_local (C++11부터)
  • extern
  • mutable

decl-specifier-seq에는 저장 클래스 지정자가 최대 하나만 올 수 있어요. 다만 thread_localstatic이나 extern과 함께 나타날 수 있어요(C++11부터).

mutable은 저장 기간에 영향을 주지 않아요. 사용법은 const/volatile 문서를 봐요.

다른 저장 클래스 지정자들은 다음 선언들의 decl-specifier-seq에 나타날 수 있어요:

지정자 변수 선언 함수 선언 구조적 바인딩 선언 (C++17부터)
비-멤버 멤버 비-멤버 멤버
비-매개변수 함수 매개변수 비-static static 비-static static
--- --- --- --- --- --- ---
auto 블록 범위만 아니요 아니요 아니요 아니요
register 블록 범위만 아니요 아니요 아니요 아니요
static 아니요 static 선언 네임스페이스 범위만 static 선언
thread_local 아니요 아니요 아니요 아니요
extern 아니요 아니요 아니요 아니요

무명 유니온static으로 선언할 수 있어요.

register는 그렇게 선언된 변수가 많이 사용될 것이므로 그 값을 CPU 레지스터에 저장할 수 있다는 힌트예요. 이 힌트는 무시될 수 있고, 대부분의 구현에서 변수의 주소를 취하면 무시돼요. 이 사용은 폐기(deprecated)됐어요. (C++17 이전)

static 저장 기간

다음 조건을 모두 만족하는 변수는 static 저장 기간을 가져요:

  • 네임스페이스 범위에 속하거나 static이나 extern으로 처음 선언된다.
  • thread 저장 기간을 갖지 않는다(C++11부터).

이 엔티티들의 저장소는 프로그램이 지속되는 동안 유지돼요.

#### thread 저장 기간 thread_local로 선언된 모든 변수는 thread 저장 기간을 가져요. 이 엔티티들의 저장소는 생성된 스레드가 지속되는 동안 유지돼요. 스레드마다 별개의 객체나 참조가 있고, 선언된 이름을 사용하면 현재 스레드와 연결된 엔티티를 가리켜요. (C++11부터)

automatic 저장 기간

다음 변수들이 automatic 저장 기간을 가져요:

  • 블록 범위에 속하고 static, thread_local(C++11부터), extern으로 명시적으로 선언되지 않은 변수. 이런 변수의 저장소는 그 변수가 만들어진 블록이 종료될 때까지 유지돼요.
  • 매개변수 범위(즉 함수 매개변수)에 속하는 변수. 함수 매개변수의 저장소는 파괴 직후까지 유지돼요.

또한 임시 객체도 automatic 저장 기간을 가져요.

dynamic 저장 기간

프로그램 실행 중 다음 방법으로 만들어진 객체가 dynamic 저장 기간을 가져요:

링크 (Linkage)

이름은 외부 링크(external linkage), 모듈 링크(module linkage, C++20부터), 내부 링크(internal linkage), 또는 *링크 없음(no linkage)*을 가질 수 있어요:

  • 외부 링크를 가진 이름의 엔티티는 다른 번역 단위에서 재선언될 수 있고, 그 재선언은 다른 모듈에 연결될 수 있어요(C++20부터).
  • 모듈 링크를 가진 이름의 엔티티는, 재선언이 같은 모듈에 연결되는 한 다른 번역 단위에서 재선언될 수 있어요(C++20부터).
  • 내부 링크를 가진 이름의 엔티티는 같은 번역 단위의 다른 범위에서 재선언될 수 있어요.
  • 링크가 없는 이름의 엔티티는 같은 범위에서만 재선언될 수 있어요.

다음 링크들이 인식돼요:

링크 없음 (No linkage)

블록 범위에서 선언된 다음 이름들은 링크가 없어요:

  • extern으로 명시적으로 선언되지 않은 변수(static 수정자와 무관);
  • 지역 클래스와 그 멤버 함수;
  • typedef, 열거, 열거자처럼 블록 범위에서 선언된 다른 이름들.

외부, 모듈(C++20부터), 내부 링크로 지정되지 않은 이름들도, 어느 범위에서 선언됐든 링크가 없어요.

내부 링크 (Internal linkage)

네임스페이스 범위에서 선언된 다음 이름들은 내부 링크를 가져요:

  • static으로 선언된 변수, 변수 템플릿(C++14부터), 함수, 함수 템플릿;
  • 비-템플릿(C++14부터) 비-volatile const-한정 타입 변수. 단, 다음은 제외해요:
    • 명시적으로 extern으로 선언된 경우, 또는
    • 이전에 선언됐고 그 이전 선언이 내부 링크가 아니었던 경우;
  • 무명 유니온의 데이터 멤버.
또한 무명 네임스페이스나 무명 네임스페이스 안의 네임스페이스에 선언된 모든 이름은, 심지어 명시적으로 extern으로 선언된 것도 내부 링크를 가져요. (C++11부터)

외부 링크 (External linkage)

외부 링크를 가진 변수와 함수는 언어 링크도 가져서, 서로 다른 프로그래밍 언어로 작성된 번역 단위를 연결할 수 있게 해줘요.

네임스페이스 범위에서 선언된 다음 이름들은, 무명 네임스페이스에 선언됐거나 선언이 명명된 모듈에 연결되고 export되지 않지 않으면(C++20부터) 외부 링크를 가져요:

  • 위에 나열되지 않은 변수와 함수(즉 static으로 선언되지 않은 함수, static으로 선언되지 않은 비-const 변수, extern으로 선언된 모든 변수);
  • 열거;
  • 클래스 이름, 그 멤버 함수, static 데이터 멤버(const였든 아니든), 중첩 클래스와 열거, 그리고 클래스 본문 안의 friend 선언으로 처음 도입된 함수;
  • 위에 나열되지 않은 모든 템플릿 이름(즉 static으로 선언된 함수 템플릿이 아닌 것).

블록 범위에서 처음 선언된 다음 이름들은 외부 링크를 가져요:

  • extern으로 선언된 변수 이름;
  • 함수 이름.
#### 모듈 링크 네임스페이스 범위에서 선언된 이름은, 그 선언이 명명된 모듈에 연결되고 export되지 않으며 내부 링크를 갖지 않으면 모듈 링크를 가져요. (C++20부터)

static 블록 변수 (Static block variables)

static 또는 thread(C++11부터) 저장 기간을 가진 블록 변수는, 제어가 그 선언을 처음 통과할 때 초기화돼요(영-초기화상수 초기화는 블록에 처음 들어가기 전에 수행될 수 있어서, 그 경우는 예외). 이후 호출에서는 그 선언이 건너뛰어져요.

  • 초기화가 예외를 던지면 그 변수는 초기화된 것으로 간주되지 않고, 다음에 제어가 그 선언을 통과할 때 초기화가 다시 시도돼요.
  • 초기화가 그 변수가 초기화되고 있는 블록으로 재귀적으로 들어가면 동작은 정의되지 않아요(UB).
  • 여러 스레드가 같은 static 지역 변수를 동시에 초기화하려 하면 초기화는 정확히 한 번만 일어나요(std::call_once로 임의의 함수에 비슷한 동작을 얻을 수 있어요). 대부분의 구현은 이중 확인 잠금(double-checked locking) 패턴의 변형을 사용해서, 이미 초기화된 지역 static의 런타임 오버헤드를 단일 비-원자 boolean 비교로 줄여요(C++11부터).

static 저장 기간을 가진 블록 변수의 소멸자는 프로그램 종료 시 호출돼요. 단 초기화가 성공적으로 일어난 경우에만 그래요.

같은 inline 함수(암묵적으로 inline일 수 있음)의 모든 정의에 있는 static 저장 기간 변수들은, 그 함수가 외부 링크를 갖는 한, 하나의 번역 단위에 정의된 같은 객체를 가리켜요.

번역-단위-지역 엔티티 (Translation-unit-local entities)

번역-단위-지역 엔티티의 개념은 C++20에서 표준화됐어요. 자세한 내용은 이 페이지를 봐요.

엔티티는 다음 중 하나라도 해당하면 *번역-단위-지역(translation-unit-local, 줄여서 TU-local)*이에요:

  • 내부 링크를 가진 이름을 갖거나,
  • 링크가 있는 이름이 없는데 TU-local 엔티티 정의 안에서 도입됐거나,
  • 템플릿 인자나 템플릿 선언이 TU-local 엔티티를 사용하는 템플릿 또는 템플릿 특수화이다.

TU-local이 아닌 엔티티의 타입이 TU-local 엔티티에 의존하거나, TU-local이 아닌 엔티티의 선언이나 추론 가이드(C++17부터)가 그 범위 밖에서 TU-local 엔티티를 이름으로 가리키면 문제(보통 ODR 위반)가 일어날 수 있어요. 그런 사용은 다음에서 금지돼요:

  • 비-inline 함수나 함수 템플릿의 함수 본문
  • 변수나 변수 템플릿의 이니셜라이저
  • 클래스 정의 안의 friend 선언
  • 변수의 값 사용(그 변수가 상수 표현식에서 사용 가능한 경우)
이런 사용은 모듈 인터페이스 유닛(private-module-fragment 밖)이나 모듈 파티션에서 금지되고, 그 외 맥락에서는 폐기(deprecated)됐어요. 한 번역 단위에 나타난 선언은, 다른 번역 단위에 선언된 TU-local 엔티티(헤더 유닛이 아닌)를 이름으로 가리킬 수 없어요. 템플릿을 위해 인스턴스화된 선언은 특수화의 인스턴스화 지점에 나타나요. (C++20부터)

주의할 점 (Notes)

최상위 네임스페이스 범위(원래 C의 파일 범위)에서 const이고 extern이 아닌 이름은 C에서는 외부 링크지만 C++에서는 내부 링크예요.

C++11부터 auto는 더 이상 저장 클래스 지정자가 아니고, 타입 추론을 나타내는 데 쓰여요.

C에서 register 변수의 주소는 취할 수 없지만, C++에서 register로 선언된 변수는 저장 클래스 지정자 없이 선언된 변수와 의미적으로 구분되지 않아요. (C++17 이전)
C++에서는 C와 달리 변수를 register로 선언할 수 없어요. (C++17부터)

내부 또는 외부 링크를 가진 thread_local 변수의 이름은, 코드가 같은 스레드에서 실행되는지 다른 스레드에서 실행되는지에 따라 같은 인스턴스나 다른 인스턴스를 가리킬 수 있어요.

extern 키워드는 언어 링크명시적 템플릿 인스턴스화 선언을 지정하는 데도 쓰일 수 있지만, 그 경우엔 저장 클래스 지정자가 아니에요(선언이 언어 링크 지정에 직접 포함되어 extern 지정자를 포함한 것처럼 취급되는 경우는 예외).

저장 클래스 지정자는 thread_local을 제외하고 명시적 특수화명시적 인스턴스화에 허용되지 않아요:

template<class T>
struct S
{
    thread_local static int tlm;
};

template<>
thread_local int S<float>::tlm = 0;  // 여기에는 "static"이 나타나지 않음
const(constexpr이 암시할 수 있음) 변수 템플릿은 원래 기본적으로 내부 링크를 가졌는데, 이는 다른 템플릿 엔티티와 일관성이 없었어요. 결함 보고 CWG2387이 이를 수정했어요. (C++14부터)
inline은 기본적으로 외부 링크를 줘서 CWG2387의 우회책으로 작동해요. 이것이 많은 변수 템플릿에 inline추가됐다가 CWG2387이 받아들여진 뒤 제거된 이유예요. 표준 라이브러리 구현도, 지원하는 컴파일러가 CWG2387을 구현하지 않는 한 inline을 사용해야 해요. GCC Bugzilla #109126MSVC STL PR #4546 참고. (C++17부터)

예제

여기서는 thread_local 변수의 동작을 확인해 볼게요. 각 스레드가 자기만의 카운터를 가지는지 보는 예제예요.

#include <iostream>
#include <mutex>
#include <string>
#include <thread>

thread_local unsigned int rage = 1;
std::mutex cout_mutex;

void increase_rage(const std::string& thread_name)
{
    ++rage; // 락 밖에서 수정해도 OK: 스레드-지역 변수이기 때문
    std::lock_guard<std::mutex> lock(cout_mutex);
    std::cout << "Rage counter for " << thread_name << ": " << rage << '\n';
}

int main()
{
    std::thread a(increase_rage, "a"), b(increase_rage, "b");

    {
        std::lock_guard<std::mutex> lock(cout_mutex);
        std::cout << "Rage counter for main: " << rage << '\n';
    }

    a.join();
    b.join();
}

가능한 출력:

Rage counter for a: 2
Rage counter for main: 1
Rage counter for b: 2

ragethread_local이라 main 스레드의 값(1)과 각 작업 스레드의 값(2)이 서로 독립적으로 존재해요. 각 스레드가 ++rage를 한 번씩 실행해 자신의 카운터만 올리는 걸 볼 수 있어요.

결함 보고 (Defect reports)

다음 동작 변경 결함 보고는 과거에 발표된 C++ 표준에 소급 적용됐어요.

DR 적용 표준 발표 당시 동작 수정된 동작
CWG 216 C++98 클래스 범위의 무명 클래스·열거가 네임스페이스 범위와 다른 링크를 가졌음 이 범위들에서 모두 외부 링크를 가짐
CWG 389 C++98 링크 없는 이름이 링크 있는 엔티티를 선언하는 데 쓰이면 안 됨 링크 없는 타입은 (C 언어 링크를 가진 경우를 제외) 링크 있는 변수·함수의 타입으로 쓰면 안 됨
CWG 426 C++98 같은 번역 단위에서 내부·외부 링크를 동시에 가진 엔티티가 선언될 수 있었음 이 경우 ill-formed
CWG 527 C++98 CWG 389의 타입 제한이 자체 번역 단위 밖에서 이름 지을 수 없는 변수·함수에도 적용됨 이 변수·함수(링크 없음·내부 링크, 또는 무명 네임스페이스 안)에는 제한 완화
CWG 809 C++98 register가 하는 일이 거의 없었음 폐기(deprecated)
CWG 1648 C++11 thread_localextern과 결합돼도 static이 암시됐음 다른 저장 클래스 지정자가 없을 때만 암시
CWG 1686 C++98 C++11 네임스페이스 범위의 비-static 변수는 명시적으로 const(C++98)/constexpr(C++11)로 선언된 경우에만 내부 링크를 가졌음 타입이 const-한정이면 충분
CWG 2019 C++98 참조 멤버의 저장 기간이 미지정이었음 완전 객체와 같음
CWG 2387 C++14 const-한정 변수 템플릿이 기본적으로 내부 링크를 가지는지 불명확했음 const 한정은 변수 템플릿·그 인스턴스의 링크에 영향 없음
CWG 2533 C++98 암묵 생성 객체의 저장 기간이 불명확했음 명확해짐
CWG 2850 C++98 함수 매개변수 저장소가 언제 해제되는지 불명확했음 명확해짐
CWG 2872 C++98 "가리킬 수 있다"의 의미가 불명확했음 표현 개선
P2788R0 C++20 네임스페이스에서 const-한정 변수를 선언하면 모듈 유닛에서도 내부 링크를 줌 내부 링크를 주지 않음

더 알아보기

  • static 멤버(클래스 안에서의 static): static members
  • C++11 이후 auto는 타입 추론을 나타내는 auto 지정자로 쓰여요.