안녕하세요.
using 키워드를 쉽게 namespace 활성화로만 사용하고 있다는 생각에, 추가적인 기능에 대해 알아보았습니다.
using은 코드의 가독성을 높이고, 특히 네임스페이스 관리와 클래스 상속 관계에서 발생하는 미묘한 문제들을 해결하는 데 중요한 역할을 합니다.
1. using 기본 기능 - 네임스페이스 활성화
C++에서는 이름 충돌을 방지하고 코드를 모듈화 하기 위해 네임스페이스(namespace)를 사용합니다.
예를 들어, C++ 표준 라이브러리의 대부분 기능은 std 네임스페이스 안에 정의되어 있습니다.
#include <iostream>
#include <vector>
int main() {
std::cout << "Hello, World" << std::endl; // std:: 접두사 필요
std::vector<int> numbers;
return 0;
}
매번 std::를 붙이는 것은 번거로울 수 있습니다. 이때 using 키워드를 사용하여 특정 네임스페이스의 이름을 현재 스코프(scope)로 가져올 수 있습니다.
1) 방법 1: using 지시어 (using directive)
#include <iostream>
#include <vector>
// std 네임스페이스의 모든 이름을 현재 스코프로 가져옴
using namespace std;
int main() {
cout << "Hello, World" << endl; // std:: 접두사 불필요
vector<int> numbers;
return 0;
}
using namespace std;는 편리하지만, 해당 네임스페이스의 모든 이름을 가져오기 때문에 의도치 않은 이름 충돌이 발생할 수 있습니다.
특히 헤더 파일에서는 사용하지 않는 것이 좋습니다.
2) 방법 2: using 선언 (using declaration)
더 안전한 방법은 필요한 이름만 명시적으로 가져오는 것입니다.
#include <iostream>
#include <vector>
// 필요한 이름만 현재 스코프로 가져옴
using std::cout;
using std::endl;
using std::vector;
int main() {
cout << "Hello, World" << endl; // std:: 접두사 불필요
vector<int> numbers;
// string sName; // 오류! std::string은 가져오지 않았음
return 0;
}
2. 상속과 using 관계에서 함수 이름 숨김(Name Hiding) 문제 해결
using의 또 다른 중요한 용도는 클래스 상속 관계에서 발생할 수 있는 함수 이름 숨김(Name Hiding) 현상을 해결하는 것입니다.
1) 문제 상황
부모 클래스(BASE_SPACE)에 여러 개의 오버로드된 함수(print)가 있고, 자식 클래스(DERIVED_SPACE)에서 이 함수들 중 일부와 같은 이름의 함수를 하나라도 재정의(override)하거나 새로 정의하면, 자식 클래스에서는 기본적으로 부모 클래스의 해당 이름을 가진 모든 함수가 가려집니다(hidden).
아래 코드를 봅시다. (using BASE_SPACE::print; 줄이 주석 처리된 경우)
#include <iostream>
namespace space {
class BASE_SPACE {
public:
void print() {
std::cout << "BASE print() " << std::endl;
}
void print(int _data) {
std::cout << "BASE print(int) " << _data << std::endl;
}
void print(const char* _str) {
std::cout << "BASE print(char*) "<< _str << std::endl;
}
};
class DERIVED_SPACE : public BASE_SPACE {
public :
// using BASE_SPACE::print; // 이 줄이 없다면?
void print() { // BASE_SPACE::print() 오버라이드
std::cout << "DERIVED print() " << std::endl;
}
void print(const char* _data) { // BASE_SPACE::print(const char*) 오버라이드
std::cout << "DERIVED print(char*) " << _data << std::endl;
}
// DERIVED_SPACE에는 print(int) 함수가 없음
};
}
using namespace space;
int main()
{
DERIVED_SPACE dSpace;
dSpace.print(); // DERIVED_SPACE::print() 호출
dSpace.print("Hello"); // DERIVED_SPACE::print(const char*) 호출
// dSpace.print(100); // 컴파일 오류!
}
DERIVED_SPACE에서 print()와 print(const char*)를 정의했기 때문에, 컴파일러는 DERIVED_SPACE의 스코프에서 print라는 이름을 찾습니다.
함수를 찾으면, 부모 클래스(BASE_SPACE)의 스코프는 더 이상 검색하지 않습니다(print 함수 가려짐). 따라서 DERIVED_SPACE에 없는 print(int) 함수는 찾을 수 없게 되어 컴파일 오류가 발생합니다.
2) 해결책: using 선언 사용
이 문제를 해결하기 위해 자식 클래스 내에서 using 선언을 사용합니다.
#include <iostream>
namespace space {
class BASE_SPACE {
public:
void print() { std::cout << "BASE print() " << std::endl; }
void print(int _data) { std::cout << "BASE print(int) " << _data << std::endl; }
void print(const char* _str) { std::cout << "BASE print(char*) "<< _str << std::endl; }
};
class DERIVED_SPACE : public BASE_SPACE {
public :
// BASE_SPACE에 정의된 모든 'print' 함수 이름을 현재 스코프(DERIVED_SPACE)로 가져옵니다.
using BASE_SPACE::print;
// 이제 BASE_SPACE::print들을 기반으로 오버라이딩/추가 정의
void print() { // BASE_SPACE::print() override
std::cout << "DERIVED print() " << std::endl;
}
// print(int)는 BASE_SPACE 버전을 그대로 사용하게 됨
void print(const char* _data) { // BASE_SPACE::print(const char*) override
std::cout << "DERIVED print(char*) " << _data << std::endl;
}
};
}
using namespace space;
int main()
{
DERIVED_SPACE dSpace;
dSpace.print(); // DERIVED_SPACE에 오버라이드된 버전 호출
dSpace.print(100); // DERIVED에 해당 시그니처가 없으므로 Base 함수 호출.
dSpace.print("abc"); // DERIVED_SPACE에 오버라이드된 함수 호출
}
using BASE_SPACE::print; 선언은 "BASE_SPACE에 있는 'print'라는 이름의 모든 함수를 DERIVED_SPACE의 스코프에서도 사용할 수 있게 해 주세요"라는 의미입니다. 이렇게 하면, 자식 클래스에서 print 함수를 호출할 때 컴파일러는 다음과 같이 동작합니다.
3. 클래스 구조 다이어그램
위 예제 코드의 클래스 상속 구조와 주요 함수들을 시각적으로 표현하면 다음과 같습니다.
DERIVED_SPACE가 BASE_SPACE를 상속받고, using 선언을 통해 BASE_SPACE의 print 함수들을 가져온 후, 일부를 오버라이드하는 관계를 명확하게 보여줍니다.
4. using을 이용한 타입 별칭 (Type Alias) 방법
C++11부터 using은 typedef를 대체하여 타입 별칭(Type Alias)을 만드는 데에도 사용됩니다. 가독성이 더 좋고 템플릿과 함께 사용하기 편리합니다.
#include <vector>
#include <string>
#include <map>
// typedef 방식
typedef std::vector<int> IntVector_t;
typedef std::map<std::string, int> StringIntMap_t;
// using 방식 (C++11 이상)
using IntVector = std::vector<int>;
using StringIntMap = std::map<std::string, int>;
// 템플릿과 함께 사용할 때 using의 장점
template<typename T>
using PtrMap = std::map<T*, std::string>; // using 방식이 더 직관적
// typedef를 사용한 템플릿 별칭은 조금 더 복잡함
template<typename T>
struct PtrMap_t {
typedef std::map<T*, std::string> type;
};
int main() {
IntVector vec1;
IntVector_t vec2;
StringIntMap map1;
StringIntMap_t map2;
PtrMap<int> ptrMap1; // using 사용
PtrMap_t<int>::type ptrMap2; // typedef 사용
}
단순한 using 키워드이지만, 조금 더 깊게 알아보면 더 많은 기능을 내포하고 있다는 것을 확인할 수 있습니다.
모든 기능을 다 알 수는 없지만, 이런 기능이 있다는 것을 한 번쯤 알고 있으면 좋지 않을까 합니다. 그리고 편리하다고 생각되는 코드는 실제로 적용하여 효율을 높이는 것도 재미가 있을 것입니다.
감사합니다.
'Programming > C, C++' 카테고리의 다른 글
[C++] 함수 템플릿 기초와 Name Mangling (Name Decoration) 대해 알아보기 (0) | 2025.05.03 |
---|---|
[C++] 연산자 오버로딩(Operator Overloading)에 대해 좀 더 알아보기 (0) | 2025.04.29 |
[C++] namespace 사용법 읽을거리 (0) | 2025.04.23 |
[C++] 동적 바인딩과 가상 함수에 대한 짧게 읽을거리 (1) | 2025.04.17 |
[C++] explicit에 대해 알아두면 좋은 읽을거리 (0) | 2025.04.11 |