네임스페이스
네임스페이스 (Namespaces)
프로젝트가 커지면 이름 충돌(name conflict)이 자주 생겨요. 예를 들어 내가 만든 vector 클래스와 표준 라이브러리의 std::vector가 같은 이름을 쓰는 식이죠. 이 문제를 해결해 주는 게 네임스페이스(namespace)예요.
출처: cppreference
본문
네임스페이스는 큰 프로젝트에서 이름 충돌을 막는 방법을 제공해요.
네임스페이스 블록 안에 선언된 엔티티(entities)는 네임스페이스 스코프(namespace scope)에 놓여요. 그래서 다른 스코프에 있는 같은 이름의 엔티티로 오인되지 않도록 해주죠.
모든 네임스페이스 블록 바깥에 선언된 엔티티는 전역 네임스페이스(global namespace)에 속해요. 전역 네임스페이스는 전역 스코프(global scope)에 속하며, 앞에 ::를 붙여 명시적으로 가리킬 수 있어요. 전역 네임스페이스는 선언이 없지만, unnamed(무명) 네임스페이스는 아니에요.
같은 이름을 가진 여러 네임스페이스 블록은 허용돼요. 이 블록 안의 모든 선언은 같은 네임스페이스 스코프에 선언돼요.
문법
namespace ns-name { declarations } (1)
inline namespace ns-name { declarations } (2) (C++11부터)
namespace { declarations } (3)
ns-name :: member-name (4)
using namespace ns-name ; (5)
using ns-name :: member-name ; (6)
namespace name = qualified-namespace ; (7)
namespace ns-name :: member-name { declarations } (8) (C++17부터)
namespace ns-name :: inline member-name { declarations } (9) (C++20부터)
설명
네임스페이스
inline(optional) namespace attr(optional) identifier { namespace-body }
inline— (C++11부터) 있으면 이 네임스페이스를 인라인 네임스페이스(inline namespace)로 만듦(아래 참고). 원래 네임스페이스 정의(original-namespace-definition)가inline을 쓰지 않았다면 확장 네임스페이스 정의(extension-namespace-definition)에는 나타날 수 없음.attr— (C++17부터) 임의 개수의 속성의 선택적 시퀀스.identifier— 이전에 사용되지 않은 식별자이면 이는 원래 네임스페이스 정의임. 네임스페이스의 이름이면 확장 네임스페이스 정의임.::로 구분된 바깥 네임스페이스 지정자들의 시퀀스가identifier로 끝나면 이는 중첩 네임스페이스 정의(nested-namespace-definition, C++17부터)임.namespace-body— 어떤 종류의 선언이든(클래스·함수 정의와 중첩 네임스페이스 포함) 비어 있을 수도 있는 시퀀스.
네임스페이스 정의는 네임스페이스 스코프에서만 허용돼요(전역 스코프 포함).
기존 네임스페이스를 다시 여는(재개하는) 것, 공식적으로는 확장 네임스페이스 정의가 되려면, 네임스페이스 정의에 사용된 식별자에 대한 탐색이 바깥 네임스페이스 또는 바깥 네임스페이스 안의 인라인 네임스페이스의 멤버로 선언된 네임스페이스 이름(네임스페이스 별칭이 아닌)으로 해석되어야 해요.
namespace-body는 이름 탐색에 영향을 주는 네임스페이스 스코프를 정의해요.
namespace-body 안에 나타난 선언(중첩 네임스페이스 정의 포함)이 도입하는 모든 이름은 이 네임스페이스 정의가 identifier를 도입한 원래 네임스페이스 정의든, 이미 정의된 네임스페이스를 "다시 연" 확장 네임스페이스 정의든, identifier의 멤버가 돼요.
네임스페이스 본문 안에 선언된 네임스페이스 멤버는 명시적 지정(qualification)을 사용해 그 밖에서 정의하거나 다시 선언할 수 있어요.
namespace Q
{
namespace V // V는 Q의 멤버이고 Q 안에서 완전히 정의됨
{ // namespace Q::V { // 위 줄들의 C++17 대안
class C { void m(); }; // C는 V의 멤버이고 V 안에서 완전히 정의됨
// C::m은 선언만 됨
void f(); // f는 V의 멤버이지만 여기서는 선언만 됨
}
void V::f() // V의 멤버 f를 V 밖에서 정의
// f의 바깥 네임스페이스는 여전히 전역, Q, Q::V
{
extern void h(); // 이건 ::Q::V::h를 선언함
}
void V::C::m() // V::C::m을 네임스페이스(와 클래스 본문) 밖에서 정의
// 바깥 네임스페이스는 전역, Q, Q::V
{}
}
네임스페이스 밖에서의 정의와 재선언은 다음 경우에만 허용돼요:
- 선언 지점(point of declaration) 이후에,
- 네임스페이스 스코프에서, 그리고
- 원래 네임스페이스를 감싸는(전역 네임스페이스 포함) 네임스페이스들에서.
또한 반드시 qualified-id 문법을 사용해야 해요.
namespace Q
{
namespace V // V에 대한 원래 네임스페이스 정의
{
void f(); // Q::V::f의 선언
}
void V::f() {} // OK
void V::g() {} // 오류: g()는 아직 V의 멤버가 아님
namespace V // V에 대한 확장 네임스페이스 정의
{
void g(); // Q::V::g의 선언
}
}
namespace R // Q에 대한 바깥 네임스페이스가 아님
{
void Q::V::g() {} // 오류: R 안에서 Q::V::g를 정의할 수 없음
}
void Q::V::g() {} // OK: 전역 네임스페이스는 Q를 감쌈
비지역 클래스 X 안의 friend 선언이 도입하는 이름은 X의 가장 안쪽 바깥 네임스페이스의 멤버가 되지만, 클래스 정의 이전이나 이후에 네임스페이스 스코프에 일치하는 선언이 제공되지 않으면 보통 이름 탐색(비정규화든 정규화든)에는 보이지 않아요. 그런 이름은 네임스페이스와 클래스를 모두 고려하는 ADL을 통해 찾을 수 있어요.
이런 friend 선언은 그 이름이 이전에 선언된 이름과 충돌할지를 판단할 때 가장 안쪽 바깥 네임스페이스만 고려해요.
void h(int);
namespace A
{
class X
{
friend void f(X); // A::f는 friend
// A::f, A::g, A::h는 네임스페이스 스코프에서 보이지 않음
// 네임스페이스 A의 멤버이긴 하지만
class Y
{
friend void g(); // A::g는 friend
friend void h(int); // A::h는 friend, ::h와 충돌하지 않음
};
};
X x;
void g() // A::g의 정의
{
f(x); // A::X::f는 ADL 통해 찾음
}
void f(X) {} // A::f의 정의
void h(int) {} // A::h의 정의
// A::f, A::g, A::h는 이제 네임스페이스 스코프에서 보이며
// A::X와 A::X::Y의 friend이기도 함
}
인라인 네임스페이스 (Inline namespaces)
인라인 네임스페이스는 원래 네임스페이스 정의에서 선택적 키워드 inline을 사용하는 네임스페이스예요. 인라인 네임스페이스의 멤버는 여러 상황(아래 나열)에서 마치 바깥 네임스페이스의 멤버인 것처럼 취급돼요.
이 성질은 전이적(transitive)이에요. 네임스페이스 N이 인라인 네임스페이스 M을 포함하고, M이 다시 인라인 네임스페이스 O를 포함하면, O의 멤버는 마치 M이나 N의 멤버처럼 사용할 수 있어요.
인라인 네임스페이스를 지명하는 using 지시자(using-directive)가 바깥 네임스페이스에 암시적으로 삽입돼요(무명 네임스페이스의 암시적 using 지시자와 비슷). 인자 종속 탐색(argument-dependent lookup, ADL)에서 어떤 네임스페이스가 연관 네임스페이스 집합에 추가되면, 그 인라인 네임스페이스들도 추가돼요. 그리고 인라인 네임스페이스가 연관 네임스페이스 목록에 추가되면, 그 바깥 네임스페이스도 추가돼요.
인라인 네임스페이스의 각 멤버는 바깥 네임스페이스의 멤버인 것처럼 부분 특수화, 명시적 인스턴스화, 명시적 특수화될 수 있어요.
바깥 네임스페이스를 조사하는 정규화된 이름 탐색은, 바깥 네임스페이스에 같은 이름이 존재하더라도 인라인 네임스페이스의 이름들을 포함해요.
// C++14에서 std::literals와 그 멤버 네임스페이스들은 인라인이다
{
using namespace std::string_literals; // makes visible operator""s
// from std::literals::string_literals
auto str = "abc"s;
}
{
using namespace std::literals; // makes visible both
// std::literals::string_literals::operator""s
// and std::literals::chrono_literals::operator""s
auto str = "abc"s;
auto min = 60s;
}
{
using std::operator""s; // makes both std::literals::string_literals::operator""s
// and std::literals::chrono_literals::operator""s visible
auto str = "abc"s;
auto min = 60s;
}
특수화에 관한 규칙은 라이브러리 버저닝(versioning)을 허용해요. 라이브러리 템플릿의 서로 다른 구현을 서로 다른 인라인 네임스페이스에 정의할 수 있고, 사용자가 여전히 바깥 네임스페이스에서 템플릿을 확장할 수 있게 해주거든요.
무명 네임스페이스 (Unnamed namespaces)
무명 네임스페이스 정의(unnamed-namespace-definition)는 다음과 같은 형태의 네임스페이스 정의예요.
inline(optional) namespace attr(optional) { namespace-body }
inline— (C++11부터) 있으면 이 네임스페이스를 인라인 네임스페이스로 만듦.attr— (C++17부터) 임의 개수의 속성의 선택적 시퀀스.
이 정의는 고유한 이름을 가진 네임스페이스의 정의이자, 현재 스코프에서 이 무명 네임스페이스를 지명하는 using 지시자의 정의로 취급돼요(참고: 암시적으로 추가된 using 지시자 덕분에 네임스페이스가 정규화된 이름 탐색과 비정규화된 이름 탐색에 사용 가능하지만, 인자 종속 탐색에는 사용할 수 없어요). 고유한 이름은 프로그램 전체에서 고유하지만, 번역 유닛 안에서는 각 무명 네임스페이스 정의가 같은 고유 이름에 매핑돼요. 같은 스코프의 여러 무명 네임스페이스 정의는 같은 무명 네임스페이스를 나타내요.
namespace
{
int i; // defines ::(unique)::i
}
void f()
{
i++; // increments ::(unique)::i
}
namespace A
{
namespace
{
int i; // A::(unique)::i
int j; // A::(unique)::j
}
void g() { i++; } // A::(unique)::i++
}
using namespace A; // A의 모든 이름을 전역 네임스페이스로 도입
void h()
{
i++; // error: ::(unique)::i and ::A::(unique)::i are both in scope
A::i++; // ok, increments ::A::(unique)::i
j++; // ok, increments ::A::(unique)::j
}
무명 네임스페이스의 이름이 외부 연결(external linkage)으로 선언될 수 있더라도, 그 네임스페이스 이름이 고유하기 때문에 다른 번역 유닛에서 접근할 수 없어요. (C++11 이전까지) 무명 네임스페이스와 그 안에 직접 또는 간접적으로 선언된 모든 네임스페이스는 내부 연결(internal linkage)을 가져요. 즉 무명 네임스페이스 안에 선언된 어떤 이름도 내부 연결을 가져요. (C++11부터)
Using 선언 (Using-declarations)
다른 곳에서 정의된 이름을 이 using 선언이 나타나는 선언 영역(declarative region)에 도입해요.
using typename(optional) nested-name-specifier unqualified-id ; (C++17 이전까지)
using declarator-list ; (C++17부터)
typename— using 선언이 기본 클래스에서 멤버 타입을 클래스 템플릿에 도입할 때, 종속 이름을 해석하기 위해 필요하다면 사용될 수 있는 키워드.nested-name-specifier— 이름과 스코프 해석 연산자::의 시퀀스로, 스코프 해석 연산자로 끝남. 단일::는 전역 네임스페이스를 가리킴.unqualified-id— id 표현식.declarator-list—typename(optional) nested-name-specifier unqualified-id형태의 선언자들을 쉼표로 구분한 목록. 선언자 뒤에 팩 확장(pack expansion)을 나타내는 타원(...)이 올 수 있지만, 이 형태는 파생 클래스 정의에서만 의미가 있음.
using 선언은 네임스페이스 멤버를 다른 네임스페이스와 블록 스코프에 도입하거나, 기본 클래스 멤버를 파생 클래스 정의에 도입하거나, 열거자(enumerator)를 네임스페이스·블록·클래스 스코프에 도입하는 데(C++20부터) 사용될 수 있어요.
둘 이상의 using 선언자를 가진 using 선언은 하나의 using 선언자를 가진 using 선언들의 대응 시퀀스와 동등해요. (C++17부터)
파생 클래스 정의에서의 사용에 대해서는 using 선언 문서를 참고하세요.
using 선언으로 네임스페이스 스코프에 도입된 이름은, 다른 스코프에서의 정규화된 탐색을 포함해 다른 이름들처럼 사용될 수 있어요:
void f();
namespace A
{
void g();
}
namespace X
{
using ::f; // 전역 f가 이제 ::X::f로 보임
using A::g; // A::g가 이제 ::X::g로 보임
using A::g, A::g; // (C++17) OK: 네임스페이스 스코프에서 이중 선언 허용
}
void h()
{
X::f(); // ::f 호출
X::g(); // A::g 호출
}
using 선언으로 네임스페이스에서 멤버를 가져온 후 그 네임스페이스가 확장되어 같은 이름에 대한 추가 선언이 도입되면, 그 추가 선언들은 using 선언을 통해 보이지 않아요(using 지시자와 대조적). 예외가 하나 있어요. using 선언이 클래스 템플릿을 지명하면, 나중에 도입되는 부분 특수화는 기본 템플릿을 통해 탐색이 진행되므로 사실상 보이게 돼요.
namespace A
{
void f(int);
}
using A::f; // ::f는 이제 A::f(int)의 동의어
namespace A // 네임스페이스 확장
{
void f(char); // ::f가 의미하는 바는 바뀌지 않음
}
void foo()
{
f('a'); // f(char)가 존재함에도 f(int)를 호출
}
void bar()
{
using A::f; // 이 f는 A::f(int)와 A::f(char) 둘 다의 동의어
f('a'); // f(char)를 호출
}
using 선언은 template-id, 네임스페이스, 또는 스코프 열거자(scoped enumerator)를 지명할 수 없어요. (C++20 이전까지) using 선언의 각 선언자는 정확히 하나의 이름만 도입해요. 예를 들어 열거형에 대한 using 선언은 그 열거자 중 어떤 것도 도입하지 않아요.
같은 이름의 일반 선언, 숨김(hiding), 오버로딩 규칙에 대한 모든 제약이 using 선언에도 적용돼요:
namespace A
{
int x;
}
namespace B
{
int i;
struct g {};
struct x {};
void f(int);
void f(double);
void g(char); // OK: 함수 이름 g가 struct g를 숨김
}
void func()
{
int i;
using B::i; // error: i가 두 번 선언됨
void f(char);
using B::f; // OK: f(char), f(int), f(double)은 오버로드
f(3.5); // B::f(double) 호출
using B::g;
g('a'); // B::g(char) 호출
struct g g1; // g1이 struct B::g 타입이라고 선언
using B::x;
using A::x; // OK: struct B::x를 숨김
x = 99; // A::x에 대입
struct x x1; // x1이 struct B::x 타입이라고 선언
}
using 선언으로 함수가 도입됐는데, 같은 이름과 매개변수 목록을 가진 함수를 선언하는 것은 ill-formed예요(같은 함수에 대한 선언이 아닌 한). using 선언으로 함수 템플릿이 도입됐는데, 같은 이름·매개변수 타입 목록·반환 타입·템플릿 매개변수 목록을 가진 함수 템플릿을 선언하는 것은 ill-formed예요. 두 using 선언이 같은 이름과 매개변수 목록을 가진 함수를 도입할 수 있지만, 그 함수를 호출하려고 하면 프로그램은 ill-formed예요.
namespace B
{
void f(int);
void f(double);
}
namespace C
{
void f(int);
void f(double);
void f(char);
}
void h()
{
using B::f; // B::f(int), B::f(double)을 도입
using C::f; // C::f(int), C::f(double), C::f(char)를 도입
f('h'); // C::f(char) 호출
f(1); // error: B::f(int) 아니면 C::f(int)?
void f(int); // error: f(int)가 C::f(int) 및 B::f(int)와 충돌
}
어떤 엔티티가 안쪽 네임스페이스에 선언되었지만 정의되지는 않았고, 그다음 바깥 네임스페이스에서 using 선언으로 선언되고, 그다음 같은 비정규화된 이름으로 바깥 네임스페이스에 정의가 나타나면, 그 정의는 바깥 네임스페이스의 멤버이고 using 선언과 충돌해요:
namespace X
{
namespace M
{
void g(); // X::M::g()를 선언하지만 정의하지 않음
}
using M::g;
void g(); // Error: X::M::g()와 충돌하는 X::g를 선언하려 함
}
더 일반적으로, 어떤 네임스페이스 스코프에 나타나고 비정규화된 식별자로 이름을 도입하는 선언은, 다른 네임스페이스가 아니라 항상 자신이 있는 네임스페이스에 멤버를 도입해요. 예외는 인라인 네임스페이스에 정의된 기본 템플릿의 명시적 인스턴스화와 명시적 특수화인데, 이것들은 새 이름을 도입하지 않기 때문에 바깥 네임스페이스에서 비정규화된 식별자를 사용할 수 있어요.
Using 지시자 (Using-directives)
using 지시자는 다음 문법을 가진 블록 선언(block-declaration)이에요.
attr(optional) using namespace namespace-name ; (1)
attr(optional) using namespace splice-specifier ; (2) (C++26부터)
attr— (C++11부터) 이 using 지시자에 적용되는 임의 개수의 속성.namespace-name— (정규화될 수도 있는) 네임스페이스의 이름. 이 이름을 탐색할 때, 탐색은 네임스페이스 선언만 고려해요. 종속 splice 지정자를 포함해서는 안 됨(C++26부터).splice-specifier— 종속이면 안 되는 splice 지정자.
using 지시자는 네임스페이스 스코프와 블록 스코프에서만 허용돼요. using 지시자 이후 어떤 이름의 비정규화된 이름 탐색 관점에서, 그리고 그것이 나타나는 스코프가 끝날 때까지, namespace-name 또는 splice-specifier(C++26부터)의 모든 이름은, exactly that using 지시자와 namespace-name/splice-specifier(C++26부터)를 모두 포함하는 가장 가까운 바깥 네임스페이스에 선언된 것처럼 보여요.
using 지시자는 자신이 나타나는 선언 영역에 어떤 이름도 추가하지 않아요(using 선언과 달리). 따라서 똑같은 이름의 선언을 막지 않아요.
using 지시자는 비정규화된 탐색 목적에서 전이적(transitive)이에요. 어떤 스코프가 어떤 namespace-name을 지명하는 using 지시자를 포함하고, 그 네임스페이스 자신이 어떤 namespace-name-2에 대한 using 지시자를 포함한다면, 그 효과는 두 번째 네임스페이스의 using 지시자들이 첫 번째 안에 나타난 것과 같아요. 이 전이적 네임스페이스들이 나타나는 순서는 이름 탐색에 영향을 주지 않아요.
namespace A
{
int i;
}
namespace B
{
int i;
int j;
namespace C
{
namespace D
{
using namespace A;
// A의 이름들이 D로 "주입(injected)"된다.
// D 안의 비정규화된 탐색은 이 이름들이 전역 스코프와
// 같은 스코프를 가진다고 간주한다(이름 숨김 목적 등).
// D를 가리키는 정규화된 탐색(D::name)은
// D 안의 비정규화된 탐색과 같은 이름을 찾을 것이다.
int j;
int k;
int a = i; // i는 B::i, A::i는 B::i에 숨겨졌기 때문
int b = ::i; // error: 전역 네임스페이스에는 아직 i가 없음
}
using namespace D; // D와 A의 이름들이 C로 주입됨
int k = 89; // OK: using으로 도입된 이름과 동일한 이름을 선언하는 것은 OK
int l = k; // ambiguous: C::k or D::k
int m = i; // ok: B::i가 A::i를 숨김
int n = j; // ok: D::j가 B::j를 숨김
}
}
// 이들은 모두 동등한 정의들이다:
int t0 = B::i;
int t1 = B::C::a;
int t2 = B::C::D::a;
using 지시자로 어떤 네임스페이스를 지명한 후 그 네임스페이스가 확장되고 추가 멤버와/또는 using 지시자가 추가되면, 그 추가 멤버와 추가 네임스페이스들은 using 지시자를 통해 보여요(using 선언과 대조적):
namespace D
{
int d1;
void f(char);
}
using namespace D; // introduces D::d1, D::f, D::d2, D::f,
// E::e, and E::f into global namespace!
int d1; // OK: 선언할 때 D::d1과 충돌하지 않음
namespace E
{
int e;
void f(int);
}
namespace D // 네임스페이스 확장
{
int d2;
using namespace E; // 전이적 using 지시자
void f(int);
}
void f()
{
d1++; // error: ambiguous ::d1 or D::d1?
::d1++; // OK
D::d1++; // OK
d2++; // OK, d2는 D::d2
e++; // OK: 전이적 using 덕분에 e는 E::e
f(1); // error: ambiguous: D::f(int) or E::f(int)?
f('a'); // OK: 유일한 f(char)는 D::f(char)
}
참고 사항 (Notes)
어떤 네임스페이스 스코프에서의 using namespace std; using 지시자는 std 네임스페이스의 모든 이름을 전역 네임스페이스로 도입해요(전역 네임스페이스가 std와 사용자 선언 네임스페이스 둘 다를 포함하는 가장 가까운 네임스페이스이기 때문에). 이는 바람직하지 않은 이름 충돌을 일으킬 수 있어요. 이런 using 지시자 및 기타 using 지시자는 헤더 파일의 파일 스코프에서 쓰면 일반적으로 나쁜 관행으로 여겨져요.
기능 테스트 매크로:
| 기능 테스트 매크로 | 값 | 표준 | 기능 |
|---|---|---|---|
__cpp_namespace_attributes |
201411L | C++17 | 네임스페이스용 속성 |
키워드
namespace, using, inline
예제
네임스페이스를 사용해 std 네임스페이스에 이미 이름이 있는 클래스를 어떻게 만드는지 보여주는 예시예요.
#include <vector>
namespace vec
{
template<typename T>
class vector
{
// ...
};
} // of vec
int main()
{
std::vector<int> v1; // 표준 vector.
vec::vector<int> v2; // 사용자 정의 vector.
// v1 = v2; // Error: v1 and v2 are different object's type.
{
using namespace std;
vector<int> v3; // std::vector와 같음
v1 = v3; // OK
}
{
using vec::vector;
vector<int> v4; // vec::vector와 같음
v2 = v4; // OK
}
}
vec 네임스페이스 안에 std와 같은 이름의 vector 클래스를 만들고, using namespace std; 또는 using vec::vector;로 어떤 vector를 쓸지 선택하는 걸 볼 수 있어요.
결함 보고 (Defect reports)
| DR | 적용 대상 | 발표 당시 동작 | 올바른 동작 |
|---|---|---|---|
| CWG 101 | C++98 | 네임스페이스 스코프나 블록 스코프의 함수 선언과 using 선언으로 도입된 함수가 같은 함수를 선언하면 프로그램이 ill-formed였음 (모호성 없음) | 허용됨 |
| CWG 373 | C++98 | 탐색이 using 지시자 피연산자의 마지막 이름에 대해서만 네임스페이스 선언을 고려했음 (클래스는 네임스페이스를 포함할 수 없어서 최적이 아님) | 탐색 제약이 using 지시자 피연산자의 모든 이름에 적용됨 |
| CWG 460 | C++98 | using 선언이 네임스페이스를 지명할 수 있었음 | 금지됨 |
| CWG 565 | C++98 | using 선언이 같은 스코프의 다른 함수와 동일한 함수를 도입할 수 없었지만, 함수 템플릿에는 제약이 적용되지 않았음 | 함수 템플릿에도 같은 제약 적용 |
| CWG 986 | C++98 | using 지시자가 정규화된 탐색에만 전이적이었음 | 비정규화된 탐색에만 전이적 |
| CWG 987 | C++98 | 중첩 네임스페이스에 선언된 엔티티가 바깥 네임스페이스의 멤버이기도 했음 | 중첩 스코프 제외됨 |
| CWG 1021 | C++98 | using 선언으로 네임스페이스에 정의가 도입된 엔티티가 그 네임스페이스에서 정의된 것으로 간주되는지 불명확했음 | 그 네임스페이스에서 정의된 것으로 간주되지 않음 |
| CWG 1838 | C++98 | 바깥 네임스페이스의 비정규화된 정의가 다른 네임스페이스에 선언만 되고 using으로 끌어들인 엔티티를 정의할 수 있었음 | 비정규화된 정의는 항상 자신의 네임스페이스를 가리킴 |
| CWG 2155 | C++98 | CWG 이슈 1838의 해결이 클래스와 열거형 선언에 적용되지 않았음 | 적용됨 |
더 알아보기
- 네임스페이스 별칭 — 기존 네임스페이스의 별칭(alias)을 만듦