스코프(scope)
스코프(scope)
C++ 프로그램에 나타나는 선언은 각각 어떤(어쩌면 서로 떨어져 있는) 스코프 안에서만 보여요. 같은 이름이 다른 스코프에서 서로 다른 대상을 가리킬 수 있는 이유가 바로 이 스코프 규칙 덕분이에요. 스코프 안에서는 비한정 이름 탐색(unqualified name lookup)을 통해 이름을 그 선언과 연결할 수 있어요.
출처: cppreference
본문
전반(General)
모든 프로그램에는 프로그램 전체를 담는 **전역 스코프(global scope)**가 있어요.
그 외 모든 스코프 S는 다음 중 하나에 의해 도입돼요.
- 선언
- 매개변수 목록의 매개변수
- 문(statement)
- 처리기(handler)
- 계약 단언(contract assertion) (C++26부터)
S는 항상 다른 스코프 안에 나타나며, 따라서 그 스코프가 S를 포함해요.
프로그램 지점에서의 둘러싸는 스코프(enclosing scope)는 그 지점을 포함하는 임의의 스코프이고, 그중 가장 작은 스코프를 그 지점에서의 **직접 스코프(immediate scope)**라고 불러요.
스코프가 프로그램 지점 P와 그 지점을 포함하지 않는 스코프 S 사이를 가로막는다(intervene)는 것은, 그 스코프가 S 자신이거나 S를 포함하면서 P를 포함하지 않는다는 뜻이에요.
템플릿 매개변수 스코프가 아닌 임의의 스코프 S의 부모 스코프(parent scope)는, S를 포함하면서 템플릿 매개변수 스코프가 아닌 가장 작은 스코프예요.
달리 지정되지 않는 한:
- 선언은 그 위치(locus)에서 직접 스코프에 거주(inhabit)해요.
- 선언의 대상 스코프(target scope)는 그 선언이 거주하는 스코프예요.
- 선언이 (다시)도입하는 모든 이름은 그 대상 스코프에서 그 선언에 바인딩돼요.
엔티티가 스코프 S에 속한다는 것은, S가 그 엔티티의 어떤 선언의 대상 스코프라는 뜻이에요.
// 전역 스코프 스코프
// 스코프 S T
int x; // ─┐ // 프로그램 지점 X
// │
{ // │ ─┐
{ // │ │ ─┐
int y; // │ │ │ // 프로그램 지점 Y
} // │ │ ─┘
} // ─┘ ─┘
위 프로그램에서:
- 전역 스코프, 스코프
S, 스코프T는 프로그램 지점Y를 포함해요. 달리 말하면 이 세 스코프 모두 프로그램 지점Y에서의 둘러싸는 스코프예요. - 전역 스코프는 스코프
S와T를 포함하고, 스코프S는 스코프T를 포함해요. - 따라서 스코프
T는 세 스코프 중 가장 작은 스코프예요. 그 뜻은:- 스코프
T는 프로그램 지점Y에서의 직접 스코프다. - 변수
y의 선언은 그 위치에서 스코프T에 거주한다. - 스코프
T는y의 선언의 대상 스코프다. - 변수
y는 스코프T에 속한다.
- 스코프
- 스코프
S는 스코프T의 부모 스코프이고, 전역 스코프는 스코프S의 부모 스코프예요. - 스코프
S는 프로그램 지점X와 스코프T사이를 가로막아요.
블록 스코프(Block scope)
각각의
- 선택 문(selection statement)(
if,switch), - 반복 문(iteration statement)(
for, 범위 기반for(C++11부터),while,do-while), - 확장 문(expansion statement)(
template for)(C++26부터), - 처리기(handler), 또는
- 처리기의 복합 문이 아닌 복합 문(compound statement)
은, 그 문 또는 처리기를 포함하는 **블록 스코프(block scope)**를 도입해요.
블록 스코프에 속하는 변수를 **블록 변수(block variable)**라고 불러요.
int i = 42;
int a[10];
for (int i = 0; i < 10; i++) // 안쪽 "i"는 for 문이 도입한
a[i] = i; // 블록 스코프에 거주한다
int j = i; // j = 42
만약 선언이 블록 스코프 S에 거주하면서 함수를 선언하거나 extern 지정자를 사용한다면, 그 선언은 이름 붙은 모듈에 붙어 있어서는 안 되고(C++20부터), 그 대상 스코프는 더 큰 둘러싸는 스코프(가장 안쪽 둘러싸는 네임스페이스 스코프)이지만, 이름은 그 직접 스코프 S에 바인딩돼요.
만약 이름 독립적이지 않은 선언이고(C++26부터) 다음 중 하나의 블록 스코프 S에서 이름을 바인딩하는 선언이
- 함수 본문 또는 함수 try 블록의 복합 문,
- 람다 표현식의 복합 문
{ body }(C++11부터), - 선택 또는 반복 문의, 그 자체가 선택/반복 문이 아닌 하위 문(substatement), 또는
- 함수 try 블록의 처리기
S의 부모 스코프를 대상 스코프로 하는 선언과 잠재적으로 충돌한다면, 프로그램은 ill-formed예요.
if (int x = f()) // "x"를 선언
{ // if 블록은 if 문의 하위 문이다
int x; // 오류: "x"의 재선언
}
else
{ // else 블록 역시 if 문의 하위 문이다
int x; // 오류: "x"의 재선언
}
void g(int i)
{
extern int i; // 오류: "i"의 재선언
}
함수 매개변수 스코프(Function parameter scope)
각 매개변수 선언 P는 P를 포함하는 **함수 매개변수 스코프(function parameter scope)**를 도입해요.
선언된 매개변수가 함수 선언의 매개변수 목록에 속한 경우:
- 함수 선언이 함수 정의라면, 도입된 스코프는 함수 정의의 끝까지 확장돼요.
- 그 외 경우(함수 선언이 함수 프로토타입이라면), 도입된 스코프는 함수 선언자의 끝까지 확장돼요.
- 두 경우 모두에서 스코프는 함수 선언의 위치를 포함하지 않아요.
선언된 매개변수가 람다 표현식의 매개변수 목록에 속한 경우, 도입된 스코프는 { body }의 끝까지 확장돼요. (C++11부터)
선언된 매개변수가 추론 가이드(deduction guide)의 매개변수 목록에 속한 경우, 도입된 스코프는 그 추론 가이드의 끝까지 확장돼요. (C++17부터)
선언된 매개변수가 requires 표현식의 매개변수 목록에 속한 경우, 도입된 스코프는 { requirement-seq }의 끝까지 확장돼요. (C++20부터)
int f(int n) // 매개변수 "n"의 선언이
{ // 함수 매개변수 스코프를 도입한다
/* ... */
} // 함수 매개변수 스코프는 여기서 끝난다
람다 스코프(Lambda scope)
각 람다 표현식은 [ captures ] 바로 뒤에서 시작해서 { body }의 끝까지 확장되는 **람다 스코프(lambda scope)**를 도입해요.
초기화자를 가진 람다 표현식 E의 캡처는 E가 도입한 람다 스코프에 거주해요.
auto lambda = [x = 1, y]() // 이 람다 표현식이 람다 스코프를 도입하고,
{ // 캡처 "x"의 대상 스코프가 된다
/* ... */
}; // 람다 스코프는 세미콜론 앞에서 끝난다
(C++14부터)
네임스페이스 스코프(Namespace scope)
네임스페이스 N에 대한 각 네임스페이스 정의는, N에 대한 모든 네임스페이스 정의의 선언들을 포함하는 네임스페이스 스코프(namespace scope) S를 도입해요.
대상 스코프가 S이거나 S에 포함되는 각 비-friend 재선언 또는 특수화에 대해, 다음 부분도 스코프 S에 포함돼요:
- 클래스(템플릿) 재선언 또는 클래스 템플릿 특수화의 경우, class-head-name 뒤의 부분.
- 열거형 재선언의 경우, enum-head-name 뒤의 부분.
- 그 외의 재선언 또는 특수화의 경우, 선언자의 unqualified-id 또는 qualified-id 뒤의 부분.
전역 스코프는 전역 네임스페이스의 네임스페이스 스코프예요.
namespace V // "V"의 네임스페이스 정의가
{ // 네임스페이스 스코프 "S"를 도입한다
// 스코프 "S"의 첫 부분은 여기서 시작한다
void f();
// 스코프 "S"의 첫 부분은 여기서 끝난다
}
void V::f() // "f" 뒤의 부분도 스코프 "S"의 일부다
{
void h(); // V::h를 선언한다
} // 스코프 "S"의 두 번째 부분은 여기서 끝난다
클래스 스코프(Class scope)
클래스 또는 클래스 템플릿 C의 각 선언은, C의 클래스 정의의 member-specification을 포함하는 클래스 스코프(class scope) S를 도입해요.
대상 스코프가 S이거나 S에 포함되는 각 비-friend 재선언 또는 특수화에 대해, 다음 부분도 스코프 S에 포함돼요:
- 클래스(템플릿) 재선언 또는 클래스 템플릿 특수화의 경우, class-head-name 뒤의 부분.
- 열거형 재선언의 경우, enum-head-name 뒤의 부분.
- 그 외의 재선언 또는 특수화의 경우, 선언자의 unqualified-id 또는 qualified-id 뒤의 부분.
class C // "C"의 클래스 정의가
{ // 클래스 스코프 "S"를 도입한다
// 스코프 "S"의 첫 부분은 여기서 시작한다
void f();
// 스코프 "S"의 첫 부분은 여기서 끝난다
}
void C::f() // "f" 뒤의 부분도 스코프 "S"의 일부다
{
/* ... */
} // 스코프 "S"의 두 번째 부분은 여기서 끝난다
열거형 스코프(Enumeration scope)
열거형 E의 각 선언은, E의 (불투명이 아닌(C++11부터)) 열거형 선언의 enumerator-list(있을 경우)를 포함하는 **열거형 스코프(enumeration scope)**를 도입해요.
enum class E // "E"의 열거형 선언이
{ // 열거형 스코프 "S"를 도입한다
// 스코프 "S"는 여기서 시작한다
e1, e2, e3
// 스코프 "S"는 여기서 끝난다
}
템플릿 매개변수 스코프(Template parameter scope)
각 템플릿 템플릿 매개변수(template template parameter)는, 그 템플릿 템플릿 매개변수의 전체 템플릿 매개변수 목록과 requires 절(C++20부터)을 포함하는 **템플릿 매개변수 스코프(template parameter scope)**를 도입해요.
각 템플릿 선언 D는 D의 템플릿 매개변수 목록의 시작에서 D의 끝까지 확장되는 템플릿 매개변수 스코프 S를 도입해요. 템플릿 매개변수 목록 밖에 있는 선언으로서 S에 거주하게 될 선언은, 대신 D와 같은 스코프에 거주해요.
템플릿 매개변수 스코프에는 템플릿 매개변수만 속하고, 부모 스코프가 템플릿 매개변수 스코프인 것은 템플릿 매개변수 스코프뿐이에요.
// "X"의 클래스 템플릿 선언이
// 템플릿 매개변수 스코프 "S1"을 도입한다
template
<
// 스코프 "S1"은 여기서 시작한다
template // 템플릿 템플릿 매개변수 "T"가
// 또 다른 템플릿 매개변수 스코프 "S2"를 도입한다
<
typename T1
typename T2
> requires std::convertible_from<T1, T2> // 스코프 "S2"는 여기서 끝난다
class T,
typename U
>
class X; // 스코프 "S1"은 세미콜론 앞에서 끝난다
namespace N
{
template <typename T>
using A = struct X; // "X"는 템플릿 선언과 같은 스코프,
// 즉 "N"의 스코프에 거주한다
}
계약 단언 스코프(Contract-assertion scope)
각 계약 단언(contract assertion) C는 C를 포함하는 **계약 단언 스코프(contract-assertion scope)**를 도입해요.
식별자를 가진 사후조건 단언(postcondition assertion)이 이름 독립적이지 않고, 그 사후조건 단언이 함수 func와 연관되어 있으며, 대상 스코프가 다음 중 하나인 선언 D와 잠재적으로 충돌하면 프로그램은 ill-formed예요:
func의 함수 매개변수 스코프.D가 람다 표현식과 연관되어 있다면, 사전조건 단언의 가장 가까운 둘러싸는 람다 스코프. (C++26부터)
선언 지점(Point of declaration)
일반적으로 이름은 그 첫 선언의 위치(locus) 이후부터 보여요. 위치는 다음과 같이 정해져요.
단순 선언에 선언된 이름의 위치는 그 이름의 선언자 바로 뒤, 그리고 초기화자(있을 경우) 바로 앞이에요.
int x = 32; // 바깥 x가 이 스코프에 있다
{
int x = x; // 안쪽 x는 초기화자(= x) 앞에서 스코프에 들어온다
// 이는 안쪽 x를 바깥 x의 값(32)으로 초기화하지 않고,
// 안쪽 x 자신의 (불확정) 값으로 초기화한다
}
std::function<int(int)> f = [&](int n){ return n > 1 ? n * f(n - 1) : n; };
// 함수 f의 이름은 람다 안에서 스코프에 있으므로
// 참조로 올바르게 캡처되어 재귀 함수를 만들 수 있다
const int x = 2; // 바깥 x가 스코프에 있다
{
int x[x] = {}; // 안쪽 x는 초기화자(= {}) 앞에서, 그러나
// 선언자(x[x]) 뒤에서 스코프에 들어온다
// 선언자 안에서는 아직 바깥 x가 스코프에 있다
// 그래서 int 2개짜리 배열이 선언된다
}
클래스 또는 클래스 템플릿 선언의 위치는 class-head에서 그 클래스 이름을 가리키는 식별자(또는 템플릿 특수화를 가리키는 template-id) 바로 뒤예요. 클래스 또는 클래스 템플릿 이름은 기반 클래스 목록에서 이미 스코프에 있어요.
struct S: std::enable_shared_from_this<S> {}; // S는 콜론 지점에서 스코프에 있다
enum 지정자 또는 불투명 열거형 선언(C++11부터)의 위치는 그 열거형을 가리키는 식별자 바로 뒤예요.
enum E : int // E는 콜론 지점에서 스코프에 있다
{
A = sizeof(E)
};
타입 별칭 또는 별칭 템플릿 선언의 위치는 그 별칭이 가리키는 type-id 바로 뒤예요.
using T = int; // 바깥 T가 세미콜론 지점에서 스코프에 있다
{
using T = T*; // 안쪽 T는 세미콜론 지점에서 스코프에 들어오고,
// 세미콜론 앞에서는 아직 바깥 T가 스코프에 있다
// 즉 T = int*와 같다
}
생성자를 이름 짓지 않는 using 선언의 선언자 위치는 그 선언자 바로 뒤예요.
template<int N>
class Base
{
protected:
static const int next = N + 1;
static const int value = N;
};
struct Derived: Base<0>, Base<1>, Base<2>
{
using Base<0>::next, // next는 콤마 지점에서 스코프에 있다
Base<next>::value; // Derived::value는 1이다
};
열거자의 위치는 그 정의 바로 뒤예요(변수처럼 초기화자 앞이 아니에요).
const int x = 12;
{
enum
{
x = x + 1, // 열거자 x는 콤마 지점에서 스코프에 있고,
// 콤마 앞에서는 바깥 x가 스코프에 있다,
// 열거자 x는 13으로 초기화된다
y = x + 1 // y는 14로 초기화된다
};
}
주입 클래스 이름(injected-class-name)의 위치는 그 클래스(또는 클래스 템플릿) 정의의 여는 중괄호 바로 뒤예요.
template<typename T>
struct Array
// : std::enable_shared_from_this<Array> // 오류: 주입 클래스 이름이 아직 스코프에 없다
: std::enable_shared_from_this<Array<T>> // OK: 템플릿 이름 Array가 스코프에 있다
{ // 주입 클래스 이름 Array는 이제 public 멤버 이름처럼 스코프에 있다
Array* p; // Array<T>에 대한 포인터
};
함수 지역 미리 정의된 변수 __func__의 암시적 선언 위치는 함수 정의의 본문 바로 앞이에요. (C++11부터)
구조적 바인딩(structured binding) 선언의 위치는 identifier-list 바로 뒤지만, 구조적 바인딩 초기화자는 선언되는 이름들 중 어느 것도 참조할 수 없어요. (C++17부터)
범위 기반 for 루프의 item-declaration에 선언된 변수 또는 구조적 바인딩(C++17부터)의 위치는 range-initializer 바로 뒤예요.
템플릿 for 확장의 item-declaration에 선언된 변수 또는 구조적 바인딩의 위치는 expansion-initializer 바로 뒤예요. (C++26부터)
std::vector<int> x;
for (auto x : x) // vector x는 닫는 괄호 앞에서 스코프에 있고,
// auto x는 닫는 괄호 지점에서 스코프에 들어온다
{
// auto x가 스코프에 있다
}
(C++11부터)
템플릿 매개변수의 위치는 그 완전한 템플릿 매개변수(선택적 기본 인자 포함) 바로 뒤예요.
typedef unsigned char T;
template<
class T = T, // 템플릿 매개변수 T는 콤마 지점에서 스코프에 있고,
// unsigned char의 typedef 이름은 콤마 앞에서 스코프에 있다
T // 템플릿 매개변수 T가 스코프에 있다
N = 0
>
struct A
{
};
식별자를 가진 사후조건 단언의 위치는 그 : 바로 뒤예요. (C++26부터)
개념(concept) 정의의 위치는 개념 이름 바로 뒤지만, 개념 정의는 선언 중인 개념 이름을 참조할 수 없어요. (C++20부터)
이름 붙은 네임스페이스 정의의 위치는 네임스페이스 이름 바로 뒤예요.
더 알아보기
- 이름 탐색(name lookup): 한정/비한정 이름이 선언과 연결되는 규칙.
- 템플릿: 템플릿 매개변수 스코프와
requires절의 상호작용. - 람다 표현식: 람다 스코프와 캡처.