#5. [레거시 C++ 가이드] 네임스페이스(namespace)
모던 C++
- (C++11~) 인라인 네임스페이스가 추가되어 API 버전 구성이 편리해 졌습니다.
- (C++17~) 단순한 중첩 네임스페이스가 추가되어
::
로 표현할 수 있습니다.
개요
프로젝트 규모가 커지면 직관적이고 단순한 이름들은 중복되기 쉽습니다. 예를 들면 GetObject()
같은 것들이요.
이런 경우 접두어를 사용하여 이름을 구분지을 수도 있지만, C++에서는 네임스페이스를 이용하여 이름 충돌을 피하고, 코드를 논리적으로 응집합니다.
-
이름 충돌 회피
1 2 3 4 5 6 7 8 9
namespace A { int f() {return 10;} } namespace B { int f() {return 20;} } EXPECT_TRUE(A::f() == 10); // 네임스페이스 A의 f() 호출 EXPECT_TRUE(B::f() == 20); // 네임스페이스 B의 f() 호출
-
구성 요소의 논리적 응집
1 2 3 4 5
// Parser 에 필요한 항목들을 논리적으로 묶음 namespace Parser { void Tokenizer() {} void Load() {} }
사용법은 다음과 같습니다.
항목 | 내용 |
---|---|
정의 | namespace Test { void f() {} } |
사용 | Test::f(); |
using 선언 | 네임스페이스의 특정 항목 사용.using Test::f; |
using 지시문 | 네임스페이스의 전체 항목 사용.using namespace Test; |
네임스페이스 항목의 선언과 정의 분리
네임스페이스에서 선언한 함수를 정의하려면,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
namespace C {
int f() {return 30;} // 정의
int g(); // 선언
int h(); // 선언
}
namespace C {
int g() { // 네임스페이스에서 정의
return f(); // 같은 네임스페이스이면 C::f() 와 같이 명시하지 않아도 됨
}
}
int C::h() { // C::로 명시해서 정의할 수 있음
return f();
}
EXPECT_TRUE(C::g() == 30);
EXPECT_TRUE(C::h() == 30);
using 선언
네임스페이스의 특정 항목을 현재의 이름 공간으로 가져옵니다.
다음은 Test
네임스페이스에서 f
라는 이름을 가져옵니다.
1
using Test::f;
using 지시문
네임스페이스의 전체 항목을 현재의 이름 공간으로 가져옵니다.
다음은 Test
네임스페이스의 모든 이름을 가져옵니다.
1
using namespace Test;
서로 다른 네임스페이스 항목 사용
같은 네임스페이스내에 있는 항목들 끼리는 네임스페이스 명을 생략해도 됩니다.
다른 네임스페이스의 항목은
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
namespace D {
void d1() {}
void d2() {
d1(); // 같은 네임스페이스 내의 항목은 네임스페이스 명 생략
}
}
namespace E {
void e() {
D::d1(); // 다른 네임스페이스의 것은 명시적으로 네임스페이스 명 지정
}
void f() {
using D::d1; // using 선언으로 d를 가져옴
d1(); // d1는 using 선언으로 가져왔으니 네임스페이스 명 없이 사용
D::d2(); // d2는 가져오지 않았으니 네임스페이스 명을 지정하여 사용
}
}
namespace F {
using namespace D; // using 지시문으로 D의 것은 다 가져옴
void f() {
d1(); // D의 것은 그냥 사용
d2(); // D의 것은 그냥 사용
}
}
전역 공간 using 지시문 금지
using 지시문을 전역 공간에 사용하면, 이름 충돌의 가능성이 높아 집니다. 전역 공간에서 사용하지 마세요.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
namespace MyModule1 {
void Test() {}
}
namespace MyModule2 {
void Test() {}
}
using namespace MyModule1; // 전역 공간에 MyModule1의 항목들을 모두 가져옵니다.
using namespace MyModule2; // 전역 공간에 MyModule2의 항목들을 모두 가져옵니다.
namespace MyModule3 {
void f() {
Test(); // (X) 컴파일 오류. MyModule1::Test() 인지, MyModule2::Test() 인지 모릅니다.
}
}
무기명 네임스페이스
이름 없이 네임스페이스를 정의하면, 해당 파일에서만 사용할 수 있습니다.
1
2
3
namespace {
void f(); // 정의된 파일에서만 사용가능 함
}
중첩 네임스페이스
네임스페이스를 구조적으로 그룹핑 하기 위해 중첩할 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
namespace MyLib {
namespace Parser {
void Tokenizer() {}
}
namespace File {
void Load() {};
void Save() {};
}
}
MyLib::Parser::Tokenizer();
MyLib::File::Load();
(C++17~) 단순한 중첩 네임스페이스가 추가되어
::
로 표현할 수 있습니다.
별칭과 합성
기존 네임스페이스의 별칭을 정의할 수 있습니다. 이때, 별칭인 네임스페이스에서는 새로운 정의나 선언을 추가할 수 없습니다.
1
2
3
4
5
6
7
8
9
10
namespace MyTestLibrary {
int f() {return 40;}
}
namespace MTL = MyTestLibrary; // 별칭 정의
namespace MTL {
void g() {} // (X) 컴파일 오류. 별칭으로 정의한 namespace에 새로운 정의는 추가할 수 없다.
}
EXPECT_TRUE(MTL::f() == 40);
여러개의 네임스페이스를 합성할 수도 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
namespace G {
int f() {return 50;}
}
namespace H {
int g() {return 60;}
}
namespace MyModule { // 여러개의 네임스페이스를 합성할 수 있음
using namespace G;
using namespace H;
}
EXPECT_TRUE(MyModule::f() == 50);
EXPECT_TRUE(MyModule::g() == 60);
네임스페이스 명시시 유지보수성
네임스페이스를 어떻게 사용하느냐에 따라 유지보수성에 차이가 있습니다.
먼저 swap()
을 명시적으로 사용했다가 성능 개선을 위해 리팩토링 하는 경우를 살펴봅시다.
1
2
3
4
5
6
7
namespace My {
class T {};
void f() {
T a, b;
std::swap(a, b); // std를 명시적으로 작성했습니다.
}
}
이제 My
네임스페이스에 std::swap()
보다 성능이 좋은 swap()
함수를 만들어 보고 성능 확인을 해본다고 가정합시다.
새로운 swap()
함수를 사용하도록 리팩토링 해야 하는데요, 같은 네임스페이스에 있는 함수라 다음처럼 그냥 std::
만 삭제하면 됩니다. 만약 1,000군데 사용했다면, 찾기/바꾸기로 하면 되고요. 그런데, 바꿔놓고 보니 기존의 std::swap()
버전이 성능이 더 좋아 되돌린다면, 또다시 찾기/바꾸기를 해서 바꾸면 됩니다. 되기는 합니다만, 어딘지 좀 미련한 느낌입니다.
1
2
3
4
5
6
7
8
namespace My {
class T {};
void swap(T& left, T& right); // 새로운 swap 함수입니다.
void f() {
T a, b;
swap(a, b); // 같은 네임스페이스의 함수라 명시하지 않았습니다.
}
}
using 선언을 이용하면 좀더 그럴싸 하게 작업할 수 있습니다.
다음처럼 애초에 using 선언만 하여 암시적으로 사용한다면, 코드 본문은 수정할 필요없이 using 선언만 수정하면 되니까요.
1
2
3
4
5
6
7
8
namespace My {
using std::swap;
class T {};
void f() {
T a, b;
swap(a, b); // using 선언에 따라 std::swap을 사용합니다.
}
}
이제 리팩토링하면, using std::swap;
만 삭제하면 됩니다.
1
2
3
4
5
6
7
8
9
namespace My {
// using std::swap; // 요것만 삭제하면 됩니다.
class T {};
void swap(T& left, T& right);
void f() {
T a, b;
swap(a, b);
}
}
네임스페이스를 이용한 코드 관리
using 선언 이나 using 지시문을 활용하면, 코드의 버전 관리나 OS별 관리가 좀더 편리해집니다.
다음 코드는 MyLib
을 OS별로 구성하고, YourLib
에서 OS를 선택해서 사용하는 예입니다.
using namespace MyLib::Windows;
와 같이 using 지시문을 사용하여 YourLib
에서 Windows
용 f()
를 호출하도록 했는데요, 이를 using namespace MyLib::Linux;
로 변경하면 YourLib
에서 사용하는 모든 f()
함수를 Linux
용 f()
함수로 한방에 변경할 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
namespace MyLib {
namespace Windows {
int f() {return 1;} // #1
}
namespace Linux {
int f() {return 2;} // #2
}
};
namespace YourLib {
using namespace MyLib::Windows; // Windows 버전을 사용합니다.
int g() {
return f(); // MyLib::Windows::f()가 호출됩니다.
}
};
EXPECT_TRUE(YourLib::g() == 1);
(C++11~) 인라인 네임스페이스가 추가되어 API 버전 구성이 편리해 졌습니다.
댓글남기기