스코프(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에서의 둘러싸는 스코프예요.
  • 전역 스코프는 스코프 ST를 포함하고, 스코프 S는 스코프 T를 포함해요.
  • 따라서 스코프 T는 세 스코프 중 가장 작은 스코프예요. 그 뜻은:
    • 스코프 T는 프로그램 지점 Y에서의 직접 스코프다.
    • 변수 y의 선언은 그 위치에서 스코프 T에 거주한다.
    • 스코프 Ty의 선언의 대상 스코프다.
    • 변수 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)

각 매개변수 선언 PP를 포함하는 **함수 매개변수 스코프(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)**를 도입해요.

각 템플릿 선언 DD의 템플릿 매개변수 목록의 시작에서 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) CC를 포함하는 **계약 단언 스코프(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 절의 상호작용.
  • 람다 표현식: 람다 스코프와 캡처.