- 호환되는 모든 타입을 받아들이는 멤버 함수를 만들려면 멤버 함수 템플릿을 사용해라.
- 일반화된 복사 생성 연산과 일반화된 대입 연산을 위해 멤버 템플릿을 선언했다 하더라도, 보통의 복사 생성자와 복사 대입 연산자는 직접 선언해야 한다.
호환되는 모든 타입을 받아들이는 데는 멤버 함수를 사용
스마트 포인터는 그냥 포인터처럼 동작하면서도 포인터가 주지 못하는 기능을 갖고 있다.
예를 들면 힙 기반 자원의 삭제를 지원하는 auto_ptr이나 tr1::shared_ptr 객체가 있다.
STL 컨테이너의 반복자도 스마트 포인터와 같다.
포인터에도 스마트 포인터로 대신할 수 없는 특징이 있다.
그중 하나가 암시적 변환을 지원하는 점이다.
파생 클래스 포인터는 암시적으로 기본 클래스 포인터로 변환되고, 비상수 객체에 대한 포인터는 상수 객체에 대한 포인터로의 암시적 변환이 가능하다.
class Top { ... };
class Middle: public Top { ... };
class Bottom: public Middle { ... };
Top* pt1 = new Middle; // Middle* -> Top*
Top* pt2 = nnew 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<Middle> -> SmartPtr<Top>
SmartPtr<Top> pt2 = SmartPtr<Bottom>(new Bottom); // SmartPtr<Bottom> -> SmartPtr<Top>
SmartPtr<const Top> pct2 = pt1; // SmartPtr<Top> -> SmartPtr<const Top>
위와 같은 코드를 통과시키고 싶다.
같은 템플릿으로부터 만들어진 다른 인스턴스들 사이에는 어떤 관계도 없기 때문에, 컴파일러는 별개의 클래스로 인식한다.
SmartPtr클래스들 사이에 어떤 변환을 하고 싶다면 직접 만들어야 한다.
하지만 필요한 모든 생성자 함수를 직접 만드는 것은 불가능하다.
위의 클래스 계통에서는 SmartPtr<Middle>혹은 SmartPtr<Bottom>으로부터 SmartPtr<Top>을 생성할 수 있지만, 나중에 클래스 계통이 더 확장되거나 다른 스마트 포인터 타입으로부터 SmartPtr<Top>객체를 만들 방법도 마련해야 되기 때문이다.
class BelowBottom: public Bottom { ... };
위와 같은 클래스가 추가된다면 SmartPtr<BelowBottom>으로부터 SmartPtr<Top>객체를 생성하는 부분도 지원해야 한다는 얘기이다.
템플릿을 인스턴스화하면 이 문제를 해결할 수 있다.
즉, SmartPtr에 생성자 함수를 둘 필요가 없이 생성자를 만들어내는 템플릿을 쓰는 것이다.
이 생성자 템플릿은 멤버 함수 템플릿의 한 예이다.
멤버 함수 템플릿은 간단히 말해서 어떤 클래스의 멤버 함수를 찍어내는 템플릿을 말한다.
template<typename T>
class SmartPtr {
public:
template<typename U>
SmartPtr(const SmartPtr<U>& other); // 일반화된 복사 생성자를 만드는 멤버 템플릿
...
};
위의 코드를 풀어보면 다음과 같다.
모든 T타입 및 모든 U타입에 대해서, SmartPtr<T> 객체가 SmartPtr<U>로부터 생성될 수 있다.
SmartPtr<U>의 참조자를 매개변수로 받아들이는 생성자가 SmartPtr<T>안에 들어 있기 때문이다.
이런 꼴의 생성자를 가리켜 일반화 복사 생성자라고 부른다.
위의 예제에 나온 일반화 복사 생성자는 explicit로 선언되지 않는다.
기본제공 포인터는 포인터 타입 사이의 타입 변환이 암시적으로 이루어지며 캐스팅이 필요하지 않기 때문에, 스마트 포인터도 이렇게 되는 게 맞다고 생각한다.
그래서 템플릿으로 만든 생성자 앞에 explicit 키워드를 빼야 한다.
SmartPtr에 선언된 일반화 복사 생성자는 의도하지 않은 더 많은 것을 한다.
SmartPtr<Bottom>으로부터 SmartPtr<Top>을 만들 수 있을 뿐 아니라, 그 반대도 가능하다.
public 상속의 의미의 역행이다.
게다가 지금의 생성자로는 SmartPtr<double>로부터 SmartPtr<int>를 만드는 것도 가능해서 문제가 있다.
기본제공 포인터 타입으로 바꿔 봤을 때 int*에서 double*로 진행되는 암시적 변환이 가능하지 않기 때문이다.
auto_ptr 및 tr1::shared_ptr에서 쓰는 방법을 그대로 따라서 SmartPtr도 get 멤버 함수를 통해 해당 스마트 포인터 객체의 기본제공 포인터의 사본을 반환한다고 가정하면, 이것을 이용해서 생성자 템플릿에 원하는 타입 변환 제약을 줄 수 있다.
template<typename T>
class SmartPtr {
public:
template<typename U>
SmartPtr(const SmartPtr<U>& other): heldPtr(other.get()) // 다른 SmartPtr의 포인터로 초기화
{ ... }
T* get() const { return heldPtr; }
...
private:
T* heldPtr; // 기본 제공 포인터
};
멤버 초기화 리스트를 사용해서, SmartPtr<T>의 데이터 멤버인 T*타입의 포인터를 SmartPtr<U>에 들어 있는 U*타입의 포인터로 초기화했다.
이렇게 하면 U*에서 T*로 진행되는 암시적 변환이 가능할 때만 컴파일 에러가 나지 않는다.
SmartPtr<T>의 일반화 복사 생성자는 호환되는 타입의 매개변수를 넘겨받을 때만 컴파일이 된다는 뜻이다.
멤버 함수 템플릿은 다른 부분에서도 활용할 수 있다.
가장 흔히 쓰이는 예는 대입 연산이다.
예를 들면, TR1의 shared_ptr 클래스 템플릿은 호환되는 모든 기본제공 포인터, tr1::shared_ptr, auto_ptr, tr1::weak_ptr객체들로부터 생성자 호출이 가능한 데다, 이들 중 tr1::weak_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); // weak_ptr 생성자
template<class Y>
explicit shared_ptr(auto_ptr<Y>& r); // auto_ptr 생성자
template<class Y>
shared_ptr& operator=(shared_ptr<Y> const& r); // shared_ptr 대입 연산
template<class Y>
shared_ptr& operator=(auto_ptr<Y>& r); // auto_ptr 대입 연산
...
};
일반화 복사 생성자를 제외하고 모든 생성자가 explicit로 선언되어 있다.
shared_ptr로 만든 어떤 타입으로부터 또 다른 타입으로 진행되는 암시적 변환은 허용되지만 기본제공 포인터 혹은 다른 스마트 포인터 타입으로부터 변환되는 것은 막겠다는 뜻이다.
tr1::shared_ptr 생성자와 대입 연산자에 넘겨지는 auto_ptr이 const로 선언되지 않았는데 tr1::shared_ptr 및 tr1::weak_ptr은 const로 넘겨진다.
auto_ptr은 복사 연산으로 인해 객체가 수정될 때 오직 복사된 쪽에만 유효하게 적용되기 때문이다.
멤버 함수 템플릿은 코드 재사용만큼 훌륭한 기능이지만, C++ 언어의 기본 규칙을 바꿀 수는 없다.
컴파일러는 기본 생성자, 소멸자, 복사 생성자, 복사 대입 연산자가 선언되지 않았다면 자동으로 만들어 낸다.
tr1::shared_ptr에는 분명히 일반화 복사 생성자가 선언되어 있는데, 동일한 T타입과 Y타입이 들어온다면 복사 생성자를 생성할지 일반화 복사 생성자 템플릿을 인스턴스화할지 헷갈릴 수 있다.
정답은 일반적인 복사 생성자를 만들어 내는 것이다.
일반화 복사 생성자를 어떤 클래스 안에 선언하는 것은 컴파일러가 복사 생성자를 만드는 것을 막는 요소가 아니다.
즉, 일반화 복사 생성자와 복사 생성자는 다르다는 것이다.
복사 대입 연산자도 마찬가지다.
만약, 어떤 클래스의 복사 생성을 전부 제어하고 싶다면 복사 생성자도 직접 선언해야 한다.
tr1::shared_ptr은 다음과 같이 처리하였다.
template<class 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=(shared_ptr<Y> const& r); // 일반화 복사 대입 연산자
...
};