#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 버전 구성이 편리해 졌습니다.
댓글남기기