Item 45 : Use member function templates to accept "all compatible types"
호환되는 모든 타입을 받아들이는 데는 멤버 함수 템플릿이 직방!
스마트포인터는 포인터처럼 동작하는 객체지만, 포인터가 제공하지 않는 추가적인 기능을 제공한다.
real pointer가 잘하는 것 중 하나는 implicit conversion을 지원하는 것이다.
Derived class pointer는 암시적으로 base class pointer로 변환될 수 있고, non-const 객체에 대한 포인터는 const 객체에 대한 포인터로 암시적 변환될 수 있다.
user-defined smart pointer class에 대해 고려해보자.
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>
같은 템플릿의 다른 인스턴스 사이의 inherent relationship이 없다.
컴파일러는 SmartPtr<Middle>과 SmartPtr<Top>을 완전히 다른 클래스들로 본다.
그러나, 우리가 필요한 생성자의 수가 한정적이지 않기 때문에, 명시적으로 변환되는 생성자를 만드는 것은 어렵다.
따라서 우리는 constructor template이 필요하다.
template<typename T>
class SmartPtr{
public:
template<typename U>
SmartPtr(const SmartPtr<U>& other);
...
};
모든 타입 T와 모든 타입 U에 대해 SmartPtr<T>는 SmartPtr<U>로부터 생성될 수 있다.
한 객체가 같은 템플릿의 다른 인스턴스를 가진 타입의 다른 객체로부터 생성되는 생성자를 generalized copy constructor라고 한다.
generalized copy constructor는 explicit로 선언되지 않는다.
그러나, generalized copy constructor는 우리가 원하는 것 이상을 제공한다.
smartPtr<Bottom>으로부터 smartPtr<Top>으로의 변환은 가능하지만, smartPtr<Top>으로부터 smartPtr<Bottom>은 생성되지 않도록 해야한다.
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;
};
위 코드는 포인터 U*에서 T*로의 암시적 변환이 존재하는 경우에만 타입 변환이 컴파일되도록 한다.
따라서 우리가 원하는 변환이 제한되는 constructor template을 구현할 수 있다.
멤버 함수 템플릿의 유용성은 생성자에만 한정되지 않는다.
이들의 또 다른 일반적인 역할은 Assignment 지원이다.
정리하면, 호환되는 모든 타입을 받아들이는 함수를 생성하기 위해 member function template을 사용해라.
만약, generalized copy construction또는 generalized assignment를 위한 member template를 선언했더라도, normal copy constructor와 copy assignment operator또한 선언해야 한다.
Item 46 : Define non-member functions inside templates when type conversions are desired
타입 변환이 바람직할 경우에는 비멤버 함수를 템플릿 안에 정의해 두자.
template<typename T>
class Rational{
public:
Rational(const T& numerator = 0, const T& denominator = 1);
const T numerator() const;
const T denominator() const;
...
};
template<typename T>
const Rational<T> operator*(const Rational<T>& lhs, const Rational<T>& rhs){...}
Rational<int> oneHalf(1,2);
Rational<int> result = oneHalf * 2; //error! won't compile
templatized Rational이 non-template version과 다른 무언가가 있기 때문에 컴파일이 실패했다.
위 코드의 경우, 컴파일러는 우리가 호출하기를 원하는 것을 알지 못한다.
대신에, operator*라는 템플릿에서 어떤 함수를 인스턴스화할지 알아내고자 노력한다.
컴파일러는 두개의 Rational<T> 타입의 파라미터를 가지는 operator*라는 이름의 함수를 인스턴스화 하기로 되어있다는 것을 알지만, 인스턴스화를 위해 T가 무엇인지 알아내야 한다.
oneHalf를 사용한 추론은 쉽다.
operator*의 첫번째 파라미터는 Rational<T> 타입으로 선언되며, 따라서 T는 int이다.
하지만 다른 파라미터에 대한 추론은 쉽지 않다.
operator*의 두번째 파라미터는 Rational<T> 타입이어야 하지만, operator*에 passed된 두번째 파라미터는 int이다.
이런 경우 컴파일러는 T가 무엇인지 어떻게 알 수 있을까?
template argument을 추론하는 동안 생성자 호출을 통한 암시적 형 변환은 고려되지 않는다.
따라서 T가 무엇인지 알 수 없다.
template class에서의 friend 선언이 특정한 함수를 가리킬 수 있다는 사실을 이용하여 template argument 추론 문제를 완화할 수 있다.
Rational<T>클래스는 operator*를 friend function으로 선언할 수 있다.
template argument 추론은 함수 템플릿에만 좌우되며, 클래스 템플릿에는 영향을 주지 않는다.
따라서, T의 정확한 정보는 Rational<T> 클래스가 인스턴스화 될 때 알 수 있다.
template<typename T>
class Rational{
public:
friend
const Rational operator*(const Rational& lhs, const Rational& rhs);
};
template<typename T>
const Rational<T> operator*(const Rational<T>& lhs, const Rational<T>& rhs){...}
이것은 컴파일된다.
oneHalf객체가 Rational<int>로 선언될때, 클래스 Rational<int>는 인스턴스화되고, Rational<int> 파라미터를 가지는 friend function operator*가 자동으로 선언된다.
함수가 선언됨으로써 컴파일러는 implicit conversion function을 사용할 수 있다.
그러나, 링크 단계에서 에러가 발생한다.
함수가 Rational안에서 선언만 되어있고 정의되어 있지 않다.
operator*의 선언과 함수의 body를 붙임으로써 문제를 해결할 수 있다.
template<typename T>
class Rational{
public:
friend
const Rational operator*(const Rational& lhs, const Rational& rhs)
{
return Rational(lhs.numerator()*rhs.numerator(), lhs.denominator()*rhs.denominator());
}
};
그러나, 이 방법은 또! 문제가 있다!
클래스 안에 함수가 정의되었기 때문에 암시적으로 inline으로 선언된다.
이러한 영향을 최소화하기 위해 operator*가 클래스 밖에 정의된 helper function을 호출하도록 한다.
template<typename T> class Rational;
template<typename T>
const Rational<T> doMultiply(const Rational<T>& lhs, const Rational<T>& rhs);
template<typename T>
class Rational{
public:
friend
const Rational operator*(const Rational& lhs, const Rational& rhs)
{
return doMultiply(lhs, rhs);
}
};
정리하면, 모든 매개변수에 대한 암시적 타입 변환을 지원하는 템플릿과 관계가 있는 함수를 제공하는 클래스 템플릿을 만들고자 한다면, 이 함수는 클래스 템플릿 안에 friend 함수로 정의하자.
Item 47 : Use traits classes for information about types
타입에 대한 정보가 필요하다면 특성정보 클래스를 사용하자.
'C++ > Effective C++' 카테고리의 다른 글
[Effective C++] Chapter 8: Customizing new and delete (0) | 2022.07.13 |
---|---|
[Effective C++] Chapter 7: Templates and Generic Programming(1) (0) | 2022.07.09 |
[Effective C++] Chapter 6: Inheritance and Object-Oriented Design(2) (0) | 2022.07.08 |
[Effective C++] Chapter 6: Inheritance and Object-Oriented Design(1) (0) | 2022.07.06 |
[Effective C++] Chapter 5: Implementations(2) (0) | 2022.07.06 |