스토리텔링 개발자

[Effective C++] 45. 멤버 함수 템플릿으로 암시적 변환 지원 본문

개발/Effective C++

[Effective C++] 45. 멤버 함수 템플릿으로 암시적 변환 지원

김디트 2024. 7. 17. 11:08
728x90

항목 45. “호환되는 모든 타입”을 받아들이는데는 멤버 함수 템플릿이 직방!

 

 

 

포인터의 암시적 변환(implicit conversion)
  • 스마트 포인터로는 대신할 수 없는 포인터의 특징이다.
// 포인터의 경우
class Top { ... };
class Middle : public Top { ... };
class Bottom : public Middle { ... };
Top* pt1 = new Middle; // Middle* -> Top* 암시적 변환
Top* pt2 = new Bottom; // Bottom* -> Top* 암시적 변환
const Top* pct2 = pt1; // Top* -> const Top* 암시적 변환

// 사용자 정의 스마트 포인터의 경우
template<typename T>
class SmartPtr
{
public:
    explicit SmartPtr(T* realPtr);
    ...
};

SmartPtr<Top> pt1 = SmartPtr<Middle>(new Middle); // 실패!
SmartPtr<Top> pt2 = SmartPtr<Bottom>(new Bottom); // 실패!
SmartPtr<const Top> pct2 = pt1; // 실패!
  • 같은 템플릿으로 만들어진 다른 인스턴스들 사이에는 어떤 관계도 없으므로 컴파일러 입장에서는 SmartPtr<Top>과 SmartPtr<Middle>은 완전히 별개의 클래스이다.
  • 결국, 이 변환을 가능하게 하려면 변환 방법을 직접 마련해야 한다.

 

 

 

스마트 포인터 간의 변환을 지원하는 방법
  • 생성자 함수를 직접 만든다. 
    • SmartPtr<Middle> 혹은 SmartPtr<Bottom>으로부터 SmartPtr<Top>을 생성할 수는 있겠지만..
    • 클래스 계통이 확장될때마다 유지보수를 해주어야 하고, 그때마다 SmartPtr을 수정하고 싶지는 않다.
  • 생성자를 만들어내는 템플릿(template)을 사용한다.
    • 멤버 함수 템플릿(member function template, 멤버 템플릿)의 한 예시라고 할 수 있다.

 

 

 

멤버 함수 템플릿(member function template)
  • 어떤 클래스의 멤버 함수를 찍어내는 템플릿
template<typename T>
class SmartPtr
{
public:
    template<typename U>
    SmartPtr(const SmartPtr<U>& other); // 일반화된 복사 생성자를 위한 멤버 템플릿
    ...
}
  • 위 예시는 일반화 복사 생성자(generalized copy constructor)라고 할 수 있다.
    • 같은 템플릿을 써서 인스턴스화 되지만 타입이 다른 타입의 객체로부터 원하는 객체를 만들어주는 생성자

 

 

 

일반화 복사 생성자 살펴보기
  • explicit으로 선언되지 않았다.
    • 기본제공 포인터의 동작과 같이 타입 변환이 암시적으로 이루어지며, 캐스팅이 필요하지 않게 하기 위해서 제거하였다.
  • 기본제공 포인터보다 더 많은 일을 해준다.
    • SmartPtr<Bottom>으로부터 SmartPtr<Top>을 만드는 것만을 원했으나..
    • SmartPtr<Top>으로부터 SmartPtr<Bottom>을 만드는 것까지 지원하는 형태이다.
    • 심지어 SmartPtr<double>으로부터 SmartPtr<int>를 만든다거나 하는 것까지 가능하다.
      • 기본제공 포인터에서는 int*에서 double* 암시적 변환은 불가능한데...

 

 

 

기본제공 포인터만큼만 일하도록 수정해보자
  • 기본 제공 포인터의 getter를 사용하여 암시적 변환이 가능할때만 컴파일 되도록 한다.
template<typename T>
class SmartPtr
{
public:
    template<typename U>
    SmartPtr(const SmartPtr<U>& other) : heldPtr(other.get()) // U*로 T*를 초기화
    { ... }
    
    T* get() const { return heldPtr; } // getter 함수 제공
    ...
private
    T* heldPtr; // 기본 제공 포인터
}
  • 이렇게 하면, U*에서 T*로 진행되는 암시적 변환이 가능할 때만 컴파일이 된다!

 

 

 

멤버 함수 템플릿의 추가적인 활용법
  • 대입 연산에 사용될 수 있다.
  • 아래 예시에서 생성자 템플릿과 대입 연산자 템플릿을 함께 쓴 것을 확인할 수 있다.

 

 

 

std::shared_ptr 클래스 템플릿
  • template<class T> shared_ptr
    {
    public:
        // 생성자
        template<class Y>
        explicit shared_ptr(Y* p);
        template<class Y>
        shared_ptr(shared_ptr<Y> const& r);
        template<class Y>
        explicit shared_ptr(weak_ptr<Y> const& r);
        template<class Y>
        explicit shared_ptr(auto_ptr<Y>& r);
        
        // 대입 연산자
        template<class Y>
        shared_ptr& operator=(shared_ptr<Y> const& r);
        template<class Y>
        shared_ptr& operator=(auto_ptr<Y>& r);
        ...
  • 호환되는 모든 기본제공 포인터, std::shared_ptr, auto_ptr, weak_ptr 객체들로부터 생성자 호출이 가능하다.
  • weak_ptr을 제외한 나머지를 모두 대입 연산에 사용할 수 있다.
  • 생성자 중 일반화 복사 생성자(shared_ptr<Y> const&에 대한 복사 생성자)만 explicit이 빠져 있는 이유.
    • shared_ptr로 만든 어떤 타입에서 또 다른 shared_ptr 타입으로의 암시적 변환은 허용한다.
    • 하지만 기본제공 포인터나 다른 스마트 포인터 타입으로부터의 암시적 변환은 막겠다.
  • 매개변수로 auto_ptr만 const로 선언되지 않은 이유.
    • auto_ptr은 복사 연산으로 인해 객체가 수정될 때 복사된 쪽 하나만 유효하게 남는 특성이 있다.
    • 그러므로 매개변수로 넘겨진 기존 auto_ptr은 유효하지 않게 수정되어야 하므로 const로 선언하면 안 된다.

 

 

 

유의할 점
  • 일반화 복사 생성자가 선언되어 있더라도, 복사 생성자와 복사 대입 생성자를 컴파일러가 알아서 만드는 경우가 있다.
    • 멤버 함수 템플릿이 C++ 언어의 기본 규칙까지 바꾸진 않기 때문이다.
  • .C++ 언어의 기본 규칙
    • 복사 생성자가 필요한데 프로그래머가 직접 선언하지 않으면 컴파일러가 자동으로 하나 만든다.
  • 하지만 일반화 복사 생성자는 컴파일러가 복사 생성자를 만드는 것을 가로막는 요소가 아니다!
  • 따라서 복사 생성자가 자동 생성되는 걸 막으려면 보통 복사 생성자도 직접 선언해 줘야 한다.
template<class T> class shared_ptr
{
public:
    shared_ptr(shared_ptr const& r); // 복사 생성자
    
    template<class Y>
    shared_ptr(shared_ptr<Y> const& r); // 일반화 복사 생성자
    
    sahred_ptr& operator=(shared_ptr const& r); // 복사 대입 연산자
    
    template<class Y>
    shared_ptr& operator=(shared_ptr<Y> const& r); // 일반화 복사 대입 연산자
    ...
}

 

728x90
Comments