비한정 이름 찾기

비한정 이름 찾기 (Unqualified name lookup)

C++에서 이름을 썼을 때 그 이름이 어디서 정의된 것인지를 컴파일러가 찾는 과정을 이름 찾기(name lookup)라고 해요. 특히 스코프 해석 연산자 ::의 오른쪽에 나타나지 않는 이름, 즉 **비한정 이름(unqualified name)**은 지금 있는 스코프부터 바깥쪽으로 차례차례 뒤지면서 처음 발견된 선언에서 멈춰요. 이 페이지에서 그 탐색 순서를 문맥별로 정리해 볼게요.

비한정 이름(스코프 해석 연산자 ::의 오른쪽에 나타나지 않는 이름)에 대해 이름 찾기는 아래에서 설명하는 대로 스코프들을 조사하다가, 어떤 종류의 선언이든 하나를 찾는 순간 멈추고 더 이상 바깥 스코프를 조사하지 않아요. (참고: 일부 문맥의 조회는 특정 선언을 건너뛰어요. 예를 들어 ::의 왼쪽에 쓰인 이름의 조회는 함수·변수·열거자 선언을 무시하고, 기반 클래스 지정자로 쓰인 이름의 조회는 모든 비타입 선언을 무시해요.)

비한정 이름 찾기를 위해, using 지시어(using directive)가 지명한 네임스페이스의 모든 선언은 그 using 지시어와 지명된 네임스페이스를 (직접 또는 간접으로) 둘 다 포함하는 가장 가까운 바깥 네임스페이스에 선언된 것처럼 나타나요.

함수 호출 연산자(그리고 동등하게 표현식의 연산자)의 왼쪽에 쓰인 이름의 비한정 이름 찾기는 **인자 의존 이름 찾기(argument-dependent lookup, ADL)**에서 설명해요.

출처: cppreference

본문

파일 스코프 (File scope)

어떤 함수·클래스·사용자 선언 네임스페이스 밖의 전역(최상위 네임스페이스) 스코프에서 쓰인 이름은, 그 이름의 사용 지점 이전의 전역 스코프를 조사해요.

int n = 1;     // declaration of n
int x = n + 1; // OK: lookup finds ::n

int z = y - 1; // Error: lookup fails
int y = 2;     // declaration of y

네임스페이스 스코프 (Namespace scope)

어떤 함수나 클래스 밖의 사용자 선언 네임스페이스에서 쓰인 이름은, 그 이름의 사용 지점 이전의 이 네임스페이스를 먼저 뒤지고, 그다음 이 네임스페이스의 선언 지점 이전의 바깥 네임스페이스, 이런 식으로 전역 네임스페이스에 도달할 때까지 조사해요.

int n = 1; // declaration

namespace N
{
    int m = 2;

    namespace Y
    {
        int x = n; // OK, lookup finds ::n
        int y = m; // OK, lookup finds ::N::m
        int z = k; // Error: lookup fails
    }

    int k = 3;
}

네임스페이스 밖에서의 정의

네임스페이스 밖에서 정의된 네임스페이스 멤버 변수의 정의에 쓰인 이름은, 그 변수가 네임스페이스 안에서 쓰인 것과 같은 방식으로 조회돼요.

namespace X
{
    extern int x; // declaration, not definition
    int n = 1;    // found 1st
}

int n = 2;        // found 2nd
int X::x = n;     // finds X::n, sets X::x to 1

비멤버 함수 정의

사용자 선언 또는 전역 네임스페이스의 멤버인 함수의 정의(함수 본문 또는 기본 인자의 일부)에 쓰인 이름은, 이름이 쓰인 블록을 사용 지점 이전까지 뒤지고, 그다음 그 블록이 시작된 지점 이전까지 바깥 블록을 뒤지고, 이렇게 함수 본문인 블록에 도달할 때까지 조사해요. 그다음 함수가 선언된 네임스페이스를 그 이름을 쓰는 함수의 (선언이 아니라) 정의 지점까지, 그다음 바깥 네임스페이스들을 뒤져요.

namespace A
{
    namespace N
    {
        void f();
        int i = 3; // found 3rd (if 2nd is not present)
    }

    int i = 4;     // found 4th (if 3rd is not present)
}

int i = 5;         // found 5th (if 4th is not present)

void A::N::f()
{
    int i = 2;     // found 2nd (if 1st is not present)

    while (true)
    {
       int i = 1;  // found 1st: lookup is done
       std::cout << i;
    }
}

// int i;          // not found

namespace A
{
    namespace N
    {
        // int i;  // not found
    }
}

클래스 정의

클래스 정의(기반 클래스 지정자와 중첩 클래스 정의 포함)의 아무 곳에서나 쓰인 이름 — 단, 멤버 함수 본문, 멤버 함수의 기본 인자, 멤버 함수의 예외 명세, 기본 멤버 초기화기 안은 제외(멤버가 바깥 클래스 본문 안에 정의된 중첩 클래스에 속할 수 있음) — 에 대해 다음 스코프들이 조사돼요.

friend 선언의 경우, 이전에 선언된 엔티티를 가리키는지 결정하는 조회는 위와 같이 진행하되 가장 안쪽 바깥 네임스페이스에서 멈춰요.

namespace M
{
    // const int i = 1; // never found

    class B
    {
        // static const int i = 3;     // found 3rd (but will not pass access check)
    };
}

// const int i = 5;                    // found 5th

namespace N
{
    // const int i = 4;                // found 4th

    class Y : public M::B
    {
        // static const int i = 2;     // found 2nd

        class X
        {
            // static const int i = 1; // found 1st
            int a[i]; // use of i
            // static const int i = 1; // never found
        };

        // static const int i = 2;     // never found
    };

    // const int i = 4;                // never found
}

// const int i = 5;                    // never found

주입된 클래스 이름 (Injected class name)

해당 클래스나 템플릿의 정의 안에서, 또는 그 클래스에서 파생된 클래스 안에서 쓰인 클래스·클래스 템플릿의 이름은, 그 이름이 (public 멤버 접근으로) 멤버 선언에 의해 도입된 것처럼 비한정 이름 찾기가 지금 정의 중인 클래스를 찾아요. 자세한 내용은 주입된 클래스 이름 문서를 보세요.

멤버 함수 정의

멤버 함수 본문 안, 멤버 함수의 기본 인자, 멤버 함수의 예외 명세, 기본 멤버 초기화기 안에 쓰인 이름은, 클래스 정의와 같은 스코프들을 조사하되, 이름 사용 지점 이전 부분만이 아니라 클래스 전체 스코프가 고려된다는 점이 달라요. 중첩 클래스에 대해서는 바깥 클래스 본문 전체가 조사돼요.

class B
{
    // int i;         // found 3rd
};

namespace M
{
    // int i;         // found 5th

    namespace N
    {
        // int i;     // found 4th

        class X : public B
        {
            // int i; // found 2nd
            void f();
            // int i; // found 2nd as well
        };

        // int i;     // found 4th
    }
}

// int i;             // found 6th

void M::N::X::f()
{
    // int i;         // found 1st
    i = 16;
    // int i;         // never found
}

namespace M
{
    namespace N
    {
        // int i;     // never found
    }
}

어느 쪽이든, 클래스가 파생된 기반들을 조사할 때는 가상 상속에서 **지배(dominance)**라고도 불리는 다음 규칙을 따라요.

하위 객체 B에서 발견된 멤버 이름은, AB의 기반 클래스 하위 객체라면 어떤 하위 객체 A에서도 그 같은 멤버 이름을 숨겨요. (이것은 상속 격자에서 B의 기반이 아닌 A의 추가·비가상 사본에는 이름을 숨기지 않는다는 점에 주의하세요: 이 규칙은 가상 상속에서만 효과가 있어요.) using 선언으로 도입된 이름은 그 선언을 포함하는 클래스의 이름으로 취급돼요. 각 기반을 조사한 뒤, 결과 집합은 같은 타입의 하위 객체에서 온 static 멤버 선언이거나, 같은 하위 객체에서 온 비정적 멤버 선언을 포함해야 해요. (until C++11)
찾은 선언들과 그 선언들이 발견된 하위 객체로 구성된 **조회 집합(lookup set)**이 만들어져요. using 선언은 그것이 나타내는 멤버로, 타입 선언(주입된 클래스 이름 포함)은 그것이 나타내는 타입으로 대체돼요. C가 이름을 쓴 스코프의 클래스라면 C를 먼저 조사해요. C의 선언 목록이 비어 있으면, 각 직접 기반 Bi에 대해 조회 집합을 만든 뒤(Bi가 자신의 기반을 가지면 이 규칙을 재귀적으로 적용) 만들어진 직접 기반들의 조회 집합을 다음과 같이 C의 조회 집합에 병합해요. (since C++11)
Bi의 선언 집합이 비어 있으면 폐기,
지금까지 만든 C의 조회 집합이 비어 있으면 Bi의 조회 집합으로 대체,
Bi의 조회 집합의 모든 하위 객체가 지금까지 C의 조회 집합에 추가된 하위 객체 중 적어도 하나의 기반이면 Bi의 조회 집합 폐기,
지금까지 C의 조회 집합에 추가된 모든 하위 객체가 Bi의 조회 집합의 하위 객체 중 적어도 하나의 기반이면 C의 조회 집합을 폐기하고 Bi의 조회 집합으로 대체,
그 외에 BiC의 선언 집합이 다르면 모호한 병합이 결과: 새 C 조회 집합은 무효한 선언과 이전에 C에 병합됐던 하위 객체 및 Bi에서 도입된 하위 객체의 합집합을 가짐. 이 무효한 조회 집합은 나중에 폐기되면 오류가 아닐 수 있음.
그 외에는 새 C 조회 집합이 공유된 선언 집합과 이전에 C에 병합됐던 하위 객체 및 Bi에서 도입된 하위 객체의 합집합을 가짐. (since C++11)
struct X { void f(); };

struct B1: virtual X { void f(); };

struct B2: virtual X {};

struct D : B1, B2
{
    void foo()
    {
        X::f(); // OK, calls X::f (qualified lookup)
        f(); // OK, calls B1::f (unqualified lookup)
    }
};

// C++98 rules: B1::f hides X::f, so even though X::f can be reached from D
// through B2, it is not found by name lookup from D.

// C++11 rules: lookup set for f in D finds nothing, proceeds to bases
//  lookup set for f in B1 finds B1::f, and is completed
// merge replaces the empty set, now lookup set for f in C has B1::f in B1
//  lookup set for f in B2 finds nothing, proceeds to bases
//    lookup for f in X finds X::f
//  merge replaces the empty set, now lookup set for f in B2 has X::f in X
// merge into C finds that every subobject (X) in the lookup set in B2 is a base
// of every subobject (B1) already merged, so the B2 set is discarded
// C is left with just B1::f found in B1
// (if struct D : B2, B1 was used, then the last merge would *replace* C's 
//  so far merged X::f in X because every subobject already added to C (that is X)
//  would be a base of at least one subobject in the new set (B1), the end
//  result would be the same: lookup set in C holds just B1::f found in B1)

B의 static 멤버, B의 중첩 타입, B에 선언된 열거자를 찾는 비한정 이름 찾기는, 조사 중인 클래스의 상속 트리에 타입 B의 비가상 기반 하위 객체가 여러 개 있더라도 모호하지 않아요.

struct V { int v; };

struct B
{
    int a;
    static int s;
    enum { e };
};

struct B1 : B, virtual V {};
struct B2 : B, virtual V {};
struct D : B1, B2 {};

void f(D& pd)
{
    ++pd.v;       // OK: only one v because only one virtual base subobject
    ++pd.s;       // OK: only one static B::s, even though found in both B1 and B2
    int i = pd.e; // OK: only one enumerator B::e, even though found in both B1 and B2
    ++pd.a;       // error, ambiguous: B::a in B1 and B::a in B2 
}

friend 함수 정의

우정(friendship)을 부여하는 클래스 본문 안에서 정의된 friend 함수 정의에 쓰인 이름은, 멤버 함수와 같은 방식으로 비한정 이름 찾기가 진행돼요. 클래스 본문 밖에서 정의된 friend 함수에 쓰인 이름은, 네임스페이스의 함수와 같은 방식으로 진행돼요.

int i = 3;                     // found 3rd for f1, found 2nd for f2

struct X
{
    static const int i = 2;    // found 2nd for f1, never found for f2

    friend void f1(int x)
    {
        // int i;              // found 1st
        i = x;                 // finds and modifies X::i
    }

    friend int f2();

    // static const int i = 2; // found 2nd for f1 anywhere in class scope
};

void f2(int x)
{
    // int i;                  // found 1st
    i = x;                     // finds and modifies ::i
}

friend 함수 선언

다른 클래스의 멤버 함수에 우정을 부여하는 friend 함수 선언의 선언자에 쓰인 이름으로, 그 이름이 선언자 식별자의 템플릿 인자의 일부가 아니라면, 비한정 찾기는 먼저 멤버 함수의 클래스 전체 스코프를 조사해요. 그 스코프에서 찾지 못하면(또는 그 이름이 선언자 식별자의 템플릿 인자의 일부라면), 조회는 우정을 부여하는 클래스의 멤버 함수인 것처럼 계속돼요.

template<class T>
struct S;

// the class whose member functions are friended
struct A
{ 
    typedef int AT;

    void f1(AT);
    void f2(float);

    template<class T>
    void f3();

    void f4(S<AT>);
};

// the class that is granting friendship for f1, f2 and f3
struct B
{
    typedef char AT;
    typedef float BT;

    friend void A::f1(AT);    // lookup for AT finds A::AT (AT found in A)
    friend void A::f2(BT);    // lookup for BT finds B::BT (BT not found in A)
    friend void A::f3<AT>();  // lookup for AT finds B::AT (no lookup in A, because
                              //     AT is in the declarator identifier A::f3<AT>)
};

// the class template that is granting friendship for f4
template<class AT>
struct C
{
    friend void A::f4(S<AT>); // lookup for AT finds A::AT
                              // (AT is not in the declarator identifier A::f4)
};

기본 인자 (Default argument)

함수 선언의 기본 인자에 쓰인 이름, 또는 생성자의 멤버 초기화기의 expression 부분에 쓰인 이름은, 바깥 블록·클래스·네임스페이스 스코프를 조사하기 전에 함수 매개변수 이름을 먼저 찾아요.

class X
{
    int a, b, i, j;
public:
    const int& r;

    X(int i): r(a),      // initializes X::r to refer to X::a
              b(i),      // initializes X::b to the value of the parameter i
              i(i),      // initializes X::i to the value of the parameter i
              j(this->i) // initializes X::j to the value of X::i
    {}
};

int a;
int f(int a, int b = a); // error: lookup for a finds the parameter a, not ::a
                         // and parameters are not allowed as default arguments

정적 데이터 멤버 정의

정적 데이터 멤버의 정의에 쓰인 이름은, 멤버 함수의 정의에 쓰인 이름과 같은 방식으로 조회돼요.

struct X
{
    static int x;
    static const int n = 1; // found 1st
};

int n = 2;                  // found 2nd
int X::x = n;               // finds X::n, sets X::x to 1, not 2

열거자 선언 (Enumerator declaration)

열거자 선언의 초기화기 부분에 쓰인 이름은, 비한정 이름 찾기가 바깥 블록·클래스·네임스페이스 스코프를 조사하기 전에 같은 열거형에서 이전에 선언된 열거자들을 먼저 찾아요.

const int RED = 7;

enum class color
{
    RED,
    GREEN = RED + 2, // RED finds color::RED, not ::RED, so GREEN = 2
    BLUE = ::RED + 4 // qualified lookup finds ::RED, BLUE = 11
};

함수 try 블록의 핸들러

함수 try 블록의 핸들러에 쓰인 이름은, 함수 본문의 가장 바깥 블록의 맨 처음에 쓰인 이름처럼 조회돼요(특히 함수 매개변수는 보이지만, 그 가장 바깥 블록에 선언된 이름은 보이지 않아요).

int n = 3;          // found 3rd
int f(int n = 2)    // found 2nd

try
{
    int n = -1;     // never found
}
catch(...)
{
    // int n = 1;   // found 1st
    assert(n == 2); // lookup for n finds function parameter f
    throw;
}

오버로드된 연산자 (Overloaded operator)

표현식에 쓰인 연산자(예: a + boperator+)는, 명시적 함수 호출 표현식(예: operator+(a, b))에 쓰인 연산자와 조회 규칙이 조금 달라요. 표현식을 파싱할 때 두 번의 별도 조회가 수행돼요 — 비멤버 연산자 오버로드에 대한 조회와 멤버 연산자 오버로드에 대한 조회(두 형태가 모두 허용되는 연산자에 대해). 그런 다음 그 집합들이 오버로드 해석에서 설명한 대로 내장 연산자 오버로드와 동등하게 병합돼요. 명시적 함수 호출 문법을 쓰면 일반적인 비한정 이름 찾기가 수행돼요.

struct A {};
void operator+(A, A);  // user-defined non-member operator+

struct B
{
    void operator+(B); // user-defined member operator+
    void f();
};

A a;

void B::f() // definition of a member function of B
{
    operator+(a, a); // error: regular name lookup from a member function
                     // finds the declaration of operator+ in the scope of B
                     // and stops there, never reaching the global scope

    a + a; // OK: member lookup finds B::operator+, non-member lookup
           // finds ::operator+(A, A), overload resolution selects ::operator+(A, A)
}

템플릿 정의

템플릿 정의에 쓰인 **비의존 이름(non-dependent name)**은, 템플릿 정의를 조사할 때 비한정 이름 찾기가 일어나요. 그 시점에 이루어진 선언에 대한 바인딩은 인스턴스화 지점에서 보이는 선언의 영향을 받지 않아요. 템플릿 정의에 쓰인 **의존 이름(dependent name)**은 템플릿 인자를 알게 될 때까지 조회가 미뤄지는데, 그 시점에 ADL은 템플릿 정의 문맥과 템플릿 인스턴스화 문맥 양쪽에서 보이는 함수 선언을 조사하는 반면 with external linkage(until C++11), 비-ADL 조회는 템플릿 정의 문맥에서 보이는 함수 선언만 조사해요(다시 말해 템플릿 정의 뒤에 새 함수 선언을 추가해도 ADL을 통하지 않고서는 보이지 않아요). ADL 조회가 조사한 네임스페이스에 다른 번역 단위에 선언된, 외부 링키지를 가진 더 나은 일치가 있거나, 그 번역 단위들을 조사했다면 조회가 모호했을 경우에는 동작이 정의되지 않아요. 어떤 경우든 기반 클래스가 템플릿 매개변수에 의존하면, 그 스코프는 비한정 이름 찾기가 조사하지 않아요(정의 지점이나 인스턴스화 지점 어느 쪽에서도).

void f(char); // first declaration of f

template<class T> 
void g(T t)
{
    f(1);    // non-dependent name: lookup finds ::f(char) and binds it now
    f(T(1)); // dependent name: lookup postponed
    f(t);    // dependent name: lookup postponed
//  dd++;    // non-dependent name: lookup finds no declaration
}

enum E { e };
void f(E);   // second declaration of f
void f(int); // third declaration of f
double dd;

void h()
{
    g(e);  // instantiates g<E>, at which point
           // the second and the third uses of the name 'f'
           // are looked up and find ::f(char) (by lookup) and ::f(E) (by ADL)
           // then overload resolution chooses ::f(E).
           // This calls f(char), then f(E) twice

    g(32); // instantiates g<int>, at which point
           // the second and the third uses of the name 'f'
           // are looked up and find ::f(char) only
           // then overload resolution chooses ::f(char)
           // This calls f(char) three times
}

typedef double A;

template<class T>
class B
{
    typedef int A;
};

template<class T>
struct X : B<T>
{
    A a; // lookup for A finds ::A (double), not B<T>::A
};

참고: 의존 이름 조회 규칙 문서에서 이 규칙의 근거와 함의를 볼 수 있어요.

템플릿 이름

이 절은 불완전해요
이유: .-> 뒤 템플릿 이름의 이중 스코프 조회

템플릿 밖의 클래스 템플릿 멤버

이 절은 불완전해요

더 알아보기

  • 한정 이름 찾기(qualified name lookup)와 인자 의존 이름 찾기(ADL) 문서에서 다른 조회 방식과 비교해 볼 수 있어요.
  • 스코프와 템플릿 인자 추론, 오버로드 해석 문서에서 이름 찾기와 이어지는 규칙을 확인할 수 있어요.
  • cppreference의 비한정 이름 찾기 원문에서 결함 보고(defect report) 기록을 더 볼 수 있어요.