정규화된 이름 탐색
정규화된 이름 탐색 (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):
::오른쪽이 왼쪽과 같은 클래스를 가리키면 항상 그 클래스의 생성자로 간주했음 → 허용되는 경우(예: 정교화된 타입 지정자에서는 제외)에만 생성자로 이름을 붙임.