KoreanFoodie's Study

Effective C++ | 항목 45 : "호환되는 모든 타입"을 받아들이는 데는 멤버 함수 템플릿이 직방! 본문

Tutorials/C++ : Advanced

Effective C++ | 항목 45 : "호환되는 모든 타입"을 받아들이는 데는 멤버 함수 템플릿이 직방!

GoldGiver 2022. 10. 25. 16:32

C++ 프로그래머의 필독서이자 바이블인, 스콧 마이어스의 Modern Effective C++ 를 읽고 기억할 내용을 요약하고 있습니다. 꼭 읽어보시길 추천드립니다!

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

핵심 :

1. 호환되는 모든 타입을 받아들이는 멤버 함수를 만들려면 멤버 함수 템플릿을 사용한다.
2. 일반화된 복사 생성 연산과 일반화된 대입 연산을 위해 멤버 템플릿을 선언했다 하더라도, 보통의 복사 생성자와 복사 대입 연산자는 여전히 직접 선언해야 한다.

 

포인터는 다음과 같은 암시적 변환을 지원한다.

class Base {};
class Derived: public Base {};

Base *pt1 = new Derived;

하지만 템플릿을 이용해서 만드는 사용자 정의 스마트 포인터는 위와 같은 암시적 변환을 지원하지 않는다. 

template<typename T>
class SmartPtr
{
public:
	explicit SmartPtr(T *realPtr);
}

// 컴파일러 입장에서 SmartPtr<Base> 와 SmartPtr<Derived> 는
// 완전히 별개의 클래스다! (float 과 Widget 관계 수준이다)
SmartPtr<Base> pt1 = SmartPtr<Derived>(new Derived);

 

위와 같이 우리가 원하는 복사 생성자를 만들고 싶으면, 다음과 같이 멤버 함수 템플릿(member function template, 멤버 템플릿) 을 사용해야 한다.

template<typename T>
class SmartPtr
{
public:
	template<typename U>
	SmartPtr(const SmartPtr<U>& other);
}

위와 같은 생성자를 가리켜 일반화 복사 생성자(generalized copy constructor) 라고 부른다.

하지만 위의 코드는 Derived 에서 Base 로의 업캐스팅이나, int* 에서 double* 로의 바람직하지 않은 변환까지도 가능하게 한다. 이를 막기 위해, SmartPtr 도 기존 스마트 포인터의 get 멤버 함수를 통해 객체에 담긴 기본제공 포인터의 사본을 반환한다고 가정하면, 암시적 변환이 허용하는 선에서의 캐스팅이 이루어질 수 있다.

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;
}

멤버 초기화 리스트를 사용해서 SmartPtr<T> 의 데이터 멤버인 T* 타입의 포인터를 SmartPtr<U> 에 들어 있는 U* 타입의 포인터로 초기화했다. 즉, U* 에서 T* 로 진행되는 암시적 변환이 가능할 때만 컴파일 에러가 나지 않는다.

 

멤버 함수 템플릿은 대입 연산에도 자주 쓰인다. shared_ptr 템플릿의 예시를 보자.

template<class T> class shared_ptr
{
public:
	// 호환되는 기본 제공 포인터 및 스마트 포인터로부터
	// 생성자 호출이 가능
	template<class Y>
	explicit shared_ptr(Y * p);
	template<class Y>
	explicit 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);
	// 호환되는 shared_ptr 과 auto_ptr 로부터 대입 가능
	template<class Y>
	shared_ptr& operator=(shared_ptr<Y> const& r);
	template<class Y>
	shared_ptr& operator=(auto_ptr<Y>& r);
	...
};

auto_ptr 에서 const 가 붙지 않는 것은, auto_ptr 는 복사 연산으로 객체가 수정될 때, 복사된 쪽만 유효하게 남는다는 사실을 반영한 것이다(unique_ptr 에서는 아예 복사 생성/대입 연산자가 삭제되어 있음).

다만, 일반화 복사 생성자에서 T 와 U 가 같은 타입으로 들어오면, "보통의" 복사 생성자로 인스턴스화된다. 그럼 T 와 U 가 다르면 어떻게 될까?

컴파일러는 컴파일러가 나름의 복사 생성자를 자동으로 만든다. 따라서, 우리가 원하는 동작을 하도록 하는 복사 생성자를 제대로 구현해 주어야 한다! shared_ptr 의 예시를 보자.

template<class T> class shared_ptr
{
public:
	// 복사 생성자
	shared_ptr(shared_ptr const& r);
	// 일반화 복사 생성자
	template<class Y>
	explicit shared_ptr(shared_ptr<Y> const& r);

	// 복사 대입 연산자
	shared_ptr& operator=(shared_ptr const& r);
	// 일반화 복사 대입 연산자
	template<class Y>
	shared_ptr& operator=(shared_ptr<Y> const& r);
	...
};

 

Comments