본문 바로가기
서적 정리/Effective C++

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

by 민돌이2 2021. 12. 24.

스마트 포인터(smart pointer)는 포인터처럼 동작하면서 힙 기반 자원인 동적 할당할 때 자동으로 삭제해주는 편리함이 있다. 하지만 포인터는 스마트 포인터로 대신할 수 없는 특징이 있다. 그 중 하나가 암시적 변환(implicit conversion)이다.

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

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

int main()
{
	Top* pt1 = new Middle; //Middle* -> Top* 변환
	Top* pt2 = new Bottom; //Bottom* -> Top* 변환
	const Top* pct2 = pt1; //Top* -> const Top* 변환

	system("Pause");
	return 0;
}

 

이런 암시적 변환을 사용자 정의 스마트 포인터를 사용하면 무척 까다롭다.

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

int main()
{
	SmartPtr<Top> pt1 = SmartPtr<Middle>(new Middle); //SmartPtr<Middle> -> SmartPtr<Top> 변환. 에러
	SmartPtr<Top> pt2 = SmartPtr<Bottom>(new Bottom); //SmartPtr<Bottom> -> SmartPtr<Top> 변환. 에러
	SmartPtr<const Top> pt2 = pt1; //SmartPtr<Top> -> SmartPtr<const Top> 변환. 에러

	system("Pause");
	return 0;
}

같은 템플릿으로부터 만들어진 다른 인스턴스들 사이에는 어떤 관계도 없기 때문에, 컴파일러 입장에서는 SmartPtr<Middle>과 SmartPtr<Top>은 완전히 별개의 클래스이다.

결론적으로, SmartPtr 클래스들 사이에 어떤 변환을 하고 싶다면 변환이 되도록 직접 구현해야 한다는 것이다.

 

좋은 코드란 확장성에 대해서 열려있어야 한다. 현재는 Top, Middle, Bottom 세 가지이지만, 확장을 염려해두고 코드를 짜야한다. 위의 코드에서 SmartPtr는 new를 써서 스마트 포인터 객체를 만들고 있다. SmartPtr<Bottom> 혹은 SmartPtr<Middle>에서 SmartPtr<Top>을 생성할 수 있지만, 나중에 클래스 계통이 확장된다면 다른 스마트 포인터 타입으로부터 SmartPtr<Top> 객체를 만들 코드를 또 구현해야 한다. 답이 없어진다.

이런 상황에서 생성자를 만들어내는 템플릿을 구현하면 된다. 이 생성자 템플릿은 멤버 함수 템플릿의 한 예가 된다.

 

 

멤버 함수 템플릿(member function template)

멤버 함수템플릿이란 어떤 클래스의 멤버 함수를 찍어내는 템플릿이다.

template<typename T>
class SmartPtr
{
public:
	template<typename U> //생성자에 explicit 없음
	SmartPtr(const SmartPtr<U>& other); //일반화된 복사 생성자를 만들기 위한 멤버 템플릿
};

코드를 풀어보자면 모든 T타입 및 U타입에 대해서 SmartPtr<T> 객체가 SmartPtr<U> 객체로 부터 생성될 수 있다면 뜻이다. SmartPtr<U>의 참조자를 매개변수로 받아들이는 생성자가 SmartPtr<T> 안에 있기 때문이다.

이런 꼴의 생성자를 일반화 복사 생성자(generalized copy constructor)라고 한다.

 

 

일반화 복사 생성자(generalized copy constructor)

위의 일반화 복사 생성자는 explicit로 선언하지 않았다. 기본제공 포인터(평상시에 사용하는 포인터)는 포인터 타입 사이의 타입 변환이 암시적으로 이뤄지며 캐스팅이 필요하지 않기 때문에(파생 -> 기본), 스마트 포인터도 암시적 타입 변환이 가능하도록 하기 위해 explicit을 사용하지 않았다.

 

현재 SmartPtr에 선언된 일반화 복사 생성자는 생각보다 많은 것을 해준다.

SmartPtr<Bottom>으로부터 SmartPtr<Top>을 만들기를 원했지. SmartPtr<Top>으로부터 SmartPtr<Bottom>도 가능하다. 즉, 다운캐스팅까지 가능하다. 다운 캐스팅은 굉장히 위험하므로 대책이 필요하다. STL의 스마트 포인터를 참고해서 get 멤버 함수로 해당 객체의 포인터를 얻으면 생성자 템플릿에 원하는 타입 변환 제약을 줄 수 있을 것이다.

template<typename T>
class SmartPtr
{
public:
	template<typename U>
	SmartPtr(const SmartPtr<U>& other) //일반화된 복사 생성자를 만들기 위한 멤버 템플릿
		: heldPtr(other.get()) //이 SmartPtr에 담긴 포인터를 외부 SmartPtr에 담긴 포인터로 초기화
	{}

	T* get() const { return heldPtr; }

private:
	T* heldPtr; //SmartPtr에 담긴 기본 제공 포인터
};

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

즉, SmartPtr<T>의 일반화 복사 생성자는 호환되는 타입의 매개변수를 넘겨받을 때만 컴파일이 된다.

 

 

그 외 생성자

멤버 함수 템플릿에 일반화 복사 생성자 사용하여 다운캐스팅 에러를 예방했다. 하지만, C++의 기본 규칙까지 바꾸지는 않는다. 즉, 개발자가 구현하지 않으면 컴파일러가 자동으로 만들어내는 4가지 기본 생성자, 소멸자, 복사 생성자, 복사 대입 연산자도 개발자가 구현해야 한다.

 

 

이것만은 잊지 말자

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

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

728x90

댓글