45: "호환되는 모든 타입"을 받아들이는 데는 멤버 함수 템플릿이 직방!
포인터에는 스마트 포인터로 대신할 수 없는 특징이 있다. 그 중 하나가 암시적 변환을 지원한다는 점이다.
class Top {};
class Middle : public Top {};
class Bottom : public Middle {};
Top* pt1 = new Middle;
Top* pt2 = new Bottom;
const Top* pct2 = pt1;
이런 식의 타입 변환을 사용자 정의 스마트 포인터를 써서 흉내 내려면 무척 까다롭다.
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<Middle>과 SmartPtr<Top>은 완전히 별개의 클래스이다. SmartPtr 클래스들 사이에 어떤 변환을 하고 싶다면, 변환이 되도록 직접 프로그램을 만들어야 한다.
생성자 함수를 직접 만드는 것으로는 우리에게 필요한 모든 생성자를 만들어내기란 불가능하다. 클래스 계통이 더 확장되면 다른 스마트 포인터 타입으로부터 객체를 만들 방법도 마련되어야 하기 때문이다.
원칙적으로 지금 우리가 원하는 생성자의 개수는 '무제한'이다. 그러니까 SmartPtr에 생성자 함수를 둘 필요가 없다. 바로 생성자를 만들어내는 템플릿을 쓰는 것이다. 이 생성자 템플릿은 멤버 함수 템플릿(member function template)의 한 예이다. 멤버 함수 템플릿은 간단히 말해서 어떤 클래스의 멤버 함수를 찍어내는 템플릿이다.
template <typename T>
class SmartPtr {
public:
template<typename U>
SmartPtr(const SmartPtr<U>& other);
};
모든 T 타입 및 모든 U 타입에 대해서, SmartPtr<T> 객체가 SmartPtr<U>로부터 생성될 수 있다는 이야기다. 그 이유는 SmartPtr<U>의 참조자를 매개변수로 받아들이는 생성자가 SmartPtr<T> 안에 들어있기 때문이다. 이런 꼴의 생성자(같은 템플릿을 써서 인스턴스화되지만 타입이 다른 타입의 객체로부터 원하는 객체를 만들어 주는)를 가리켜 일반화 복사 생성자(generalized copy constructor)라고들 부른다.
우리는 반대로 SmartPtr<Top>으로부터 SmartPtr<Bottom>을 만들 수 있는 것까지는 바라지 않는다. shared_ptr에서 쓰는 방법을 그대로 따라서 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;
};
멤버 초기화 리스트를 사용해서 T* 타입의 포인터를 SmartPtr<U>에 들어 있는 U* 타입의 포인터로 초기화했다. 암시적 변환이 가능할 때만 컴파일 에러가 나지 않는다. 이제 호환되는 타입의 매개변수를 넘겨받을 때만 컴파일되도록 만들어졌다.
멤버 함수 템플릿은 대입 연산에서도 흔히 쓰인다. tr1::shared_ptr이 어떻게 나와 있는지 보자.
template<typename 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(auto_ptr<Y> const& r);
template<class Y>
shared_ptr& operator=(shared_ptr<Y> const& r);
template<class Y>
shared_ptr& operator=(auto_ptr<Y>& r);
};
일반화 복사 생성자를 제외하고는 모든 생성자가 explicit로 선언되어 있다. 이는 shared_ptr로 만든 어떤 타입으로부터 또 다른 shared_ptr로 만든 타입으로 진행되는 암시적 변환은 허용되지만 기본제공 포인터 혹은 다른 스마트 포인터 타입으로부터 변환되는 것은 막겠다는 뜻이다.
여기서 T 타입과 Y타입이 동일하게 들어온다면 이 일반화 복사 생성자는 분명 "보통의" 복사 생성자를 만드는 쪽으로 인스턴스화될 것이다. 그럼 컴파일러는 tr1::shared_ptr의 기본 복사 생성자를 만들까 아니면 일반화 복사 생성자 템플릿을 인스턴스화할까?
멤버 템플릿은 언어의 규칙은 바꾸지 않는다. 일반화 복사 생성자를 어떤 클래스 안에 선언하는 행위는 컴파일러 나름의 복사 생성자를 만드는 것을 막는 요소가 아니다. 따라서 어떤 클래스의 복사 생성을 전부 관리하고 싶다면 일반화 복사 생성자는 물론이고 "보통의" 복사 생성자까지 직접 선언해야 한다. 대입 연산자도 마찬가지다.
template<typename T> class shared_ptr {
public:
shared_ptr(shared_ptr const& r); // 복사 생성자
template<class Y>
shared_ptr(shared_ptr<Y> const& r); // 일반화 복사 생성자
shared_ptr& operator=(shared_ptr const& r); // 복사 대입 연산자
template<class Y>
shared_ptr& operator=(auto_ptr<Y>& r); // 일반화 복사 대입 연산자
};
이것만은 잊지 말자!
- 호환되는 모든 타입을 받아들이는 멤버 함수를 만들려면 멤버 함수 템플릿을 사용하자.
- 일반화된 복사 생성 연산과 일반화된 대입 연산을 위해 멤버 템플릿을 선언했다 하더라도, 보통의 복사 생성자와 복사 대입 연산자는 여전히 직접 선언해야 한다.
'C++ > Effective C++' 카테고리의 다른 글
[Effective C++] 47: 타입에 대한 정보가 필요하다면 특성정보 클래스를 사용하자 (0) | 2023.11.18 |
---|---|
[Effective C++] 46: 타입 변환이 바람직할 경우에는 비멤버 함수를 클래스 템플릿 안에 정의해 두자 (0) | 2023.11.16 |
[Effective C++] 44: 매개변수에 독립적인 코드는 템플릿으로부터 분리시키자 (1) | 2023.11.13 |
[Effective C++] 43: 템플릿으로 만들어진 기본 클래스 안의 이름에 접근하는 방법을 알아 두자 (0) | 2023.11.13 |
[Effective C++] 42: typename의 두 가지 의미를 제대로 파악하자 (1) | 2023.11.11 |