본문 바로가기
이론/[책]Effective C++

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

by 사과잼빵 2017. 12. 28.

스마트 포인터에서 포인터와 같은 암시적 형 변환을 지원하게 해주기 위해 코드를 작성해보면 그 작업이 엄청 까다롭다는 것을 깨달을 수 있다.

template<typename T>

class SmartPtr

{

public:

  explicit SmartPtr(T* realPtr);

};


class Top

{};


class Middle : public Top 

{};


class Bottom : public Middle

{};


SmartPtr<Top> pt1 = SmartPtr<Middle>(new Middle);

SmartPtr<Top> pt1 = SmartPtr<Bottom>(new Bottom);

포인터의 경우 암시적 변환이 가능하지만 SmartPtr<Top>과 SmartPtr<Middle>은 컴파일러가 봤을 때 완전 별개의 클래스입니다. 따라서 직접 코드를 작성해줘야 합니다.


template<typename T>

class SmartPtr

{

public:

  explicit SmartPtr(T* realPtr);

  SmartPtr(SmartPtr<Middle>& other);

  SmartPtr(SmartPtr<Bottom>& other);

  ...

}

위와 같이 모든 생성자를 직접 만들어 줘야하는데 원칙적으로는 우리가 원하는 생성자의 개수는 '무제한'입니다. 무제한 적인 코드를 직접 모두 작성해주기란 불가능합니다. 따라서 생성자를 만들어내는 템플릿을 사용해야합니다. 아래와 같이 멤버 함수 템플릿을 작성하면 됩니다.

template<typename T>

class SmartPtr

{

public:

  template<typename U>

  SmartPtr(SmartPtr<U>& other);  //일반화된 복사생성자를 만들기 위한 멤버 템플릿

  ...

}

위와 같이 다른 템플릿을 써서 인스턴스화되지만 타입이 다른 객체로부터 원하는 객체를 만들어 주는 생성자를 가리켜 일반화 복사 생성자(generalized copy constructor)라고 합니다.


위의 코드에는 문제가 있는데 SmartPtr<Top>으로 부터 SmartPtr<Bottom>을 만든다거나 SmartPtr<int>로 부터 SmartPtr<double>를 만들어 버릴 수도 있습니다.

auto_ptr 및 tr1::shared_ptr에서 쓰는 방법을 그대로 따라서 위와 같은 문제를 막기 위한 제약을 걸 수 있습니다.


template<typename t>

class SmartPtr

{

public:

  template<typename U>

  SmartPtr(const SmartPtr<U>& other)

    : heldPtr(other.get()){}

  T* get() const { return heldPtr; }


private:

  T* heldPtr;

}


heldPtr(other.get()) 이 부분 때문에 U타입 포인터에서  T타입 포인터로 암시적 변환이 불가능하다면 컴파일 에러가 발생하게 됩니다.   


멤버 함수 템플릿은 생성자 뿐 아니라 대입 연산 등에도 쓰일 수 있다.

template <typename T>

class SmartPtr

{

public:

  template<typename U>

  SmartPtr& operator=(SmartPtr<U>& other);

}


멤버 함수 템플릿 사용에 있어서 사용자들이 착각 할 수도 있는 부분이 있는데 이 멤버 템플릿은 언어의 규칙을 바꾸지는 않습니다.

예를 들면 클래스에 생성자나 복사 생성자가 선언되어 있지 않으면 컴파일러가 자동으로 만들어 주는데 일반화 복사 생성자가 있다고 해서 기본 복사 생성자가 안만들어지는 것은 아니라는 말입니다.