Programming/C, C++

[C++] using 정의와 추가적인 기능 알아보기

변화의 물결1 2025. 4. 25. 11:08

 

 

 

안녕하세요.

 

 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 키워드이지만, 조금 더 깊게 알아보면 더 많은 기능을 내포하고 있다는 것을 확인할 수 있습니다.

 모든 기능을 다 알 수는 없지만, 이런 기능이 있다는 것을 한 번쯤 알고 있으면 좋지 않을까 합니다. 그리고 편리하다고 생각되는 코드는 실제로 적용하여 효율을 높이는 것도 재미가 있을 것입니다.

 

 

감사합니다.

 

 

반응형