성공할 게임개발자

[Effective c++] [7. 템플릿과 제네릭 프로그래밍] 45. "호환되는 모든 타입"을 받아들이는 데는 멤버 함수 템플릿이 직방! 본문

C++

[Effective c++] [7. 템플릿과 제네릭 프로그래밍] 45. "호환되는 모든 타입"을 받아들이는 데는 멤버 함수 템플릿이 직방!

fn000 2025. 6. 26. 14:27

스마트 포인터로 대신할 수 없는 포인터의 특징

 

암시적 변환(implicit conversion)지원

파생클래스 포인터는 암시적으로 기본 클래스 포인터로 변환되고.. 비상수 객체에 대한 포인터는 상수 객체에 대한 포인터로 암시적 변환이 가능하고...

 

 

예시

calss Top { ... };
class Middle : public Top { ... };
class Bottom : public Middle { ... };

Top* pt1 = new Middle;			// Middle* >> Top*의 암시적 변환
Top* pt2 = ndw 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>는 Smart<Middle>과 완전히 별개의 클래스이다. 만약 SmartPtr 클래스들 사이에 어떤 변환을 하고싶다면 변환이 되도록 직접 프로그램을 만들어야 한다.

 

위의 코드의 경우 모든 문장을 new를 써서 스마트 포인터 객체를 만들고 있다. 하지만 생성자 함수를 직접만드는 것으로는 우리에게 필요한 모든 생성자를 만들어내기란 불가능하다. Bottom을 상속받는 BelowBottom의 경우, SmartPtr<BelowBottom> 으로부터 SmartPtr<Top>객체를 생성하는 부분도 우리가 지원해야 한다.

 

원칙적으로 이러한 변환에서 우리가 원하는 생성자는 무한대이고 템플릿을 인스턴스화하면 이론적으론 무한대의 함수를 만들어낼 수 있다.

해결책으로 SmartPtr에 생성자 함수를 둘 필요없이 생성자를 만들어내는 템플릿을 쓸 수 있다. 이 생성자 템플릿은 이번 항목에서 이야기할 멤버 함수 템플릿(member function template)의 한 예 이다. 즉, 어떤 클래스의 멤버 함수를 찍어내는 템플릿이다.

 

template<typename T>
class SmartPtr
{
public:
    // 일반화된 복사 생성자를 만들기 위해 마련한 멤버 템플릿
    template<typename U>
    SmartPtr(const SmartPtr<U>& otehr);
    ...
};

" 모든 T타입 및 모든 U타입에 대해서, SmartPtr<T> 객체가 SmartPtr<U>로부터 생성될 수 있다 "

 

그 이유는 SmartPtr<U>의 참조자를 매개변수로 받아들이는 생성자가 SmartPtr<T> 안에 들어있기 때문

 

SmartPtr<U>로 부터 SmartPtr<T>를 만들어내는 생성자 ( 같은 템플릿을 써서 인스턴스화되지만 타입이 다른 타입의 객체로 부터 원하는 객체를 만들어 주는)를 일반화 복사 생성자(generalized copy constructor)라고 부른다.

 

일반화된 복사 생성자가 explicit으로 선언되지 않은 이유는 기본제공 포인터는 타입변환이 암시적으로 이루어지고 캐스팅이 필요하지 않기 때문에(파생 클래스포인터 -> 기본 클래스 포인터) 스마트 포인터도 이렇게 동작하도록 구현. 여기서는 템플릿으로 만든 생성자 앞에 expliciit 키워드를 빼면 딱 그렇게 된다.

 

 

리스크

SmartPtr<Bottom>에서 SmartPtr<Top>으로의 변환만 원했지만 그 반대도 가능하고 심지어 double에서 int로 변환도 가능하기 때문에 제약을 걸어주어야한다.

 

 

 

해결방법

std::unique_ptr , std::shared_ptr에서 쓰는 방법을 그대로 따라서 SmartPtr도 get멤버 함수를 통해 해당 스마트 포인터객체에 자체적으로 담긴 기본제공 포인터의 사본을 반환한다.(항목 15) 이것을 이용해서 타입변환 제약을 준다.

template<typename T>
class SmartPtr
{
public:
    // T타입의 포인터를 U타입의 포인터로 초기화
    template<typename U>
    SmartPtr(const SmartPtr<U>& other) : heldPtr(other.get()) { ... }
    
    T* get() const { return heldPtr; }
    ...
private:
    T* heldPtr;
};

멤버 초기화 리스트를 사용해서 T* 타입의 포인터를 U* 타입의 포인터로 초기화했다. 이렇게 하면 U* 에서 T*로 진행되는 암시적 변환이 가능할 때만 컴파일 에러가 나지 않는다. 따라서 SmartPtr<T>의 일반화 복사 생성자는 호환되는 타입의 매개변수를 넘겨받을 때만 컴파일되도록 만들어졌다.

 

결국 커스텀 스마트 포인터도 내부에서 기본제공 포인터의 성질에 의존하여 작동하는 구조이다

 

 

 

 

멤버 함수 템플릿의 활용은 비단 생성자에만 그치지 않고 대입연산에도 쓰인다.

 

std::shared_ptr 클래스 템플릿은 호환되는 기본제공 포인터 std::shared_ptr, std::unique_ptr, std::weak_ptr 객체들로 부터 생성자 호출이 가능하고 std::weak_ptr을 제외하곤 대입연산에 쓸 수 있도록 만들어졌다. 

 

std::shared_ptr 템플릿

template<class T> class 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(unique_ptr<Y>& r);
    template<class Y>
       	shared_ptr& operator=(shared_ptr<Y> const& r);
    template<class Y>
       	shared_ptr& operator=(unique_ptr<Y>& r);        
    ...
};

일반 복사생성자를 제외하고 모든 생성자가 explicit으로 선언되었다.

 

다른 shared_prt<Y>로 부터 shared_ptr<T>변환은 허용(암시적 변환가능) 하지만, T*, weak_ptr, unique_ptr => shared_ptr변환은 명시적이여야한다는 이야기이다.

 

또한, std::shared_ptr 생성자와 대입연산자에 넘겨지는 unique_ptr이 const로 선언되지 않았다. 그 이유는 unique_ptr은 복사 연산으로 인해 객체가 수정될 때 복사된 쪽 하나만 유효하게 남는다는 사실을 반영한 것.

 

 

 

멤버 함수 템플릿은 훌륭하지만 C++의 기본 규칙까지는 바꾸지 않는다.

개발자가 내버려두면, 컴파일러가 만드는 것 중, 복사 생성자에 관한 이야기이다. std::shared_ptr에는 분명히 일반화 복사생성자가 선언되어 있는데, T타입과 Y타입이 동일하게 들어온다면일반화 복사생성자는 분명 "보통의" 복사생성자를 만드는 쪽으로 인스턴스화 될 것이다. C++는 템플릿보다 일반 함수가 우선이라는 규칙을 따른다.

 

따라서 템플릿 안에 "보통의" 복사생성자를 구현한다.

template<class T> 
class shared_ptr
{
public:
    // "보통의" 복사생성자 선언
    shared_ptr(shared_ptr const& r);
    
    // 일반화 복사생성자
    template<calss Y>
    shared_ptr(shared_ptr<Y> const& r);
        
    // "보통의" 복사 대입 연산자
    shared_ptr& operator=(shared_ptr const& r);
      
    // 일반화 복사 대입 연산자
    template<calss Y>
    shared_ptr& operator=(shared_ptr<Y> const& r);
    ...
};

 

 

 

중요포인트! 

 

호환되는 모든 타입을 받아들이는 멤버 함수를 만들려면 멤버 함수 템플릿을 사용한다.

 

일반화된 복사생성연산과 일반화된 대입연산을 위해 멤버 템플릿을 선언했다 하더라도, 보통의 복사생성자와 복사 대입연산자는 여전히 직접 선언해야한다.