정규화된 이름 탐색

정규화된 이름 탐색 (Qualified name lookup)

이름이 스코프 해석 연산자 ::의 오른쪽에 오면, 그 이름이 어느 스코프에서 찾아질까요? 이 페이지에서는 정규화된 이름(qualified name)이 어떻게 탐색되는지, 그리고 탐색 대상이 클래스 멤버·네임스페이스 멤버·열거자 중 무엇인지에 따라 달라지는 규칙을 설명할게요.

출처: cppreference

본문

정규화된 이름(qualified name)은 스코프 해석 연산자 ::의 오른쪽에 나타나는 이름이에요(정규화된 식별자 참고). 정규화된 이름은 다음을 가리킬 수 있어요.

  • 클래스 멤버(정적·비정적 함수, 타입, 템플릿 등),
  • 네임스페이스 멤버(다른 네임스페이스 포함),
  • 열거자(enumerator).

::의 왼쪽에 아무것도 없으면, 탐색은 전역 네임스페이스 스코프의 선언만 고려해요. 그래서 지역 선언에 가려졌더라도 그런 이름을 참조할 수 있어요.

#include <iostream>

namespace M {
    const char* fail = "fail\n";
}

using M::fail;

namespace N {
    const char* ok = "ok\n";
}

using namespace N;

int main()
{
    struct std {};

    std::cout << ::fail; // Error: unqualified lookup for 'std' finds the struct
    ::std::cout << ::ok; // OK: ::std finds the namespace std
}

:: 오른쪽 이름에 대한 이름 탐색을 하기 전에, 왼쪽 이름의 탐색이 먼저 완료되어야 해요(decltype 표현식이 쓰이거나 왼쪽에 아무것도 없는 경우는 제외). 이 탐색(그 이름의 왼쪽에 또 다른 ::가 있는지에 따라 정규화될 수도, 비정규화될 수도 있어요)은 오직 네임스페이스, 클래스 타입, 열거형, 그리고 특수화가 타입인 템플릿만 고려해요. 왼쪽에서 찾은 이름이 네임스페이스나 클래스·열거형·의존 타입을 가리키지 않으면 프로그램은 ill-formed예요.

struct A
{
    static int n;
};

int main()
{
    int A;
    A::n = 42; // OK: unqualified lookup of A to the left of :: ignores the variable
    A b;       // Error: unqualified lookup of A finds the variable A
}

template<int>
struct B : A {};

namespace N
{
    template<int>
    void B();

    int f()
    {
        return B<0>::n; // Error: N::B<0> is not a type
    }
}

정규화된 이름이 선언자(declarator)로 쓰일 때, 같은 선언자 안에서 그 정규화된 이름 뒤에 오는 이름들(앞에 오는 이름은 제외)의 비정규화된 탐색은 그 멤버의 클래스나 네임스페이스 스코프에서 수행돼요.

class X {};

constexpr int number = 100;

struct C
{
    class X {};
    static const int number = 50;
    static X arr[number];
};

X C::arr[number], brr[number];    // Error: look up for X finds ::X, not C::X
C::X C::arr[number], brr[number]; // OK: size of arr is 50, size of brr is 100

:: 뒤에 ~가 오고 그다음 식별자가 오면(즉 소멸자나 유사 소멸자(pseudo-destructor)를 지정하면), 그 식별자는 :: 왼쪽 이름과 같은 스코프에서 탐색돼요.

struct C { typedef int I; };

typedef int I1, I2;

extern int *p, *q;

struct A { ~A(); };

typedef A AB;

int main()
{
    p->C::I::~I(); // The name I after ~ is looked up in the same scope as I before ::
                   // (that is, within the scope of C, so it finds C::I)

    q->I1::~I2();  // The name I2 is looked up in the same scope as I1
                   // (that is, from the current scope, so it finds ::I2)

    AB x;
    x.AB::~AB();   // The name AB after ~ is looked up in the same scope as AB before ::
                   // (that is, from the current scope, so it finds ::AB)
}

열거자 (Enumerators)

왼쪽 이름의 탐색 결과가 열거형(스코프 있는/없는 모두)이면, 오른쪽 이름의 탐색 결과는 그 열거형에 속한 열거자여야 해요. 그렇지 않으면 프로그램은 ill-formed예요. (since C++11)

클래스 멤버 (Class members)

왼쪽 이름의 탐색 결과가 클래스/구조체 또는 공용체 이름이면, :: 오른쪽 이름은 그 클래스의 스코프에서 탐색돼요(그 클래스나 베이스의 멤버 선언을 찾을 수 있어요). 단 다음 예외가 있어요.

  • 소멸자는 위에서 설명한 대로(:: 왼쪽 이름의 스코프에서) 탐색돼요.
  • 사용자 정의 변환 함수 이름의 변환 타입 id(conversion-type-id)는 먼저 클래스 스코프에서 탐색돼요. 없으면 현재 스코프에서 탐색해요.
  • 템플릿 인자에서 쓰인 이름은 (템플릿 이름의 스코프가 아니라) 현재 스코프에서 탐색돼요.
  • using-선언의 이름은 같은 스코프에서 선언된 변수·데이터 멤버·함수·열거자 이름에 가려진 클래스/열거형 이름도 고려해요.

:: 오른쪽이 왼쪽과 같은 클래스를 가리키면, 그 이름은 그 클래스의 생성자를 가리켜요. 이런 정규화된 이름은 생성자 선언과 상속 생성자(inheriting constructor)를 위한 using-선언에서만 쓸 수 있어요. 함수 이름이 무시되는 탐색(즉 :: 왼쪽 이름 탐색, 정교화된 타입 지정자(elaborated type specifier)나 베이스 지정자에서 이름 탐색)에서는 같은 구문이 주입된 클래스 이름(injected-class-name)으로 해석돼요.

struct A { A(); };

struct B : A { B(); };

A::A() {} // A::A names a constructor, used in a declaration
B::B() {} // B::B names a constructor, used in a declaration

B::A ba;  // B::A names the type A (looked up in the scope of B)
A::A a;   // Error: A::A does not name a type

struct A::A a2; // OK: lookup in elaborated type specifier ignores functions
                // so A::A simply names the class A as seen from within the scope of A
                // (that is, the injected-class-name)

정규화된 이름 탐색을 쓰면 중첩 선언이나 파생 클래스에 가려진 클래스 멤버에 접근할 수 있어요. 정규화된 멤버 함수 호출은 절대 가상이 아니에요.

struct B { virtual void foo(); };

struct D : B { void foo() override; };

int main()
{
    D x;
    B& b = x;

    b.foo();    // Calls D::foo (virtual dispatch)
    b.B::foo(); // Calls B::foo (static dispatch)
}

네임스페이스 멤버 (Namespace members)

:: 왼쪽 이름이 네임스페이스를 가리키거나 왼쪽에 아무것도 없으면(전역 네임스페이스를 가리킴), :: 오른쪽 이름은 그 네임스페이스의 스코프에서 탐색돼요. 단 템플릿 인자에서 쓰인 이름은 현재 스코프에서 탐색돼요.

namespace N
{
    template<typename T>
    struct foo {};

    struct X {};
}

N::foo<X> x; // Error: X is looked up as ::X, not as N::X

네임스페이스 N의 스코프 안에서의 정규화된 탐색은 먼저 N 안의 모든 선언과 N의 인라인 네임스페이스 멤버(그리고 전이적으로 그들의 인라인 네임스페이스 멤버) 안의 모든 선언을 고려해요. 그 집합에 선언이 없으면, N과 N의 모든 전이적 인라인 네임스페이스 멤버에서 발견된 using-지시문이 이름을 붙인 모든 네임스페이스의 선언을 고려해요. 이 규칙은 재귀적으로 적용돼요.

int x;

namespace Y
{
    void f(float);
    void h(int);
}

namespace Z
{
    void h(double);
}

namespace A
{
    using namespace Y;
    void f(int);
    void g(int);
    int i;
}

namespace B
{
    using namespace Z;
    void f(char);
    int i;
}

namespace AB
{
    using namespace A;
    using namespace B;
    void g();
}

void h()
{
    AB::g();  // AB is searched, AB::g found by lookup and is chosen AB::g(void)
              // (A and B are not searched)

    AB::f(1); // First, AB is searched. There is no f
              // Then, A, B are searched
              // A::f, B::f found by lookup
              // (but Y is not searched so Y::f is not considered)
              // Overload resolution picks A::f(int)

    AB::x++;  // First, AB is searched. There is no x
              // Then A, B are searched. There is no x
              // Then Y and Z are searched. There is still no x: this is an error

    AB::i++;  // AB is searched. There is no i
              // Then A, B are searched. A::i and B::i found by lookup: this is an error

    AB::h(16.8); // First, AB is searched. There is no h
                 // Then A, B are searched. There is no h
                 // Then Y and Z are searched
                 // Lookup finds Y::h and Z::h. Overload resolution picks Z::h(double)
}

같은 선언이 두 번 이상 발견되어도 괜찮아요.

namespace A { int a; }

namespace B { using namespace A; }

namespace D { using A::a; }

namespace BD
{
    using namespace B;
    using namespace D;
}

void g()
{
    BD::a++; // OK: finds the same A::a through B and through D
}

결함 보고서 (Defect reports)

이전에 발표된 C++ 표준에 소급 적용된, 동작을 바꾸는 결함 보고서는 다음과 같아요.

  • CWG 215 (C++98): :: 앞의 이름은 클래스 이름이나 네임스페이스 이름이어야 해서 템플릿 매개변수를 둘 수 없었음 → 이름이 클래스·네임스페이스·의존 타입을 가리켜야 함.
  • CWG 318 (C++98): :: 오른쪽이 왼쪽과 같은 클래스를 가리키면 항상 그 클래스의 생성자로 간주했음 → 허용되는 경우(예: 정교화된 타입 지정자에서는 제외)에만 생성자로 이름을 붙임.

더 알아보기