이펙티브 C++
https://www.yes24.com/Product/Goods/17525589
페이지 320p ~ 331p
이펙티브 C++(320p ~ 331p)
요약
항목 45: “호환되는 모든 타입”을 받아들이는 데는 멤버 함수 템플릿이 직방!
- 스마트 포인터는 일반적인 포인터처럼 동작하면서도 포인터가 주지 못하는 기능을 가지고 있다.
- STL 컨테이너의 반복자도 스마트 포인터로 마찬가지의 기능을 가지고 있다.
- 포인터에
++
연산자를 사용해서 연결 리스트의 한 노드에서 다른 노드로 이동하는 기능은 상상할 수도 없다.
- 포인터에도 스마트 포인터로 대신할 수 없는 특징이 있다.
- 그 중 하나가 암시적 변환이다.
- 파생 클래스 포인터는 암시적으로 기본 클래스 포인터로 변환되고, 비상수 객체에 대한 포인터는 상수 객체에 대한 포인터로의 암시적 변환이 가능하고, 등등이 있다.
class Top {};
class Middle : public Top {};
class Bottom : public Middle {};
Top* pt1 = new Middle; // Middle* -> Top* 변환
Top* pt2 = new 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<Top> pt2 = SmartPtr<Bottom>(new Bottom);
SmartPtr<const Top> pct2 = pt1;
- 현재 위의 코드는 컴파일 에러가 발생한다.
- 같은 템플릿으로부터 만들어진 다른 인스턴스들 사이에는 어떤 관계도 없기 때문에, 컴파일러의 눈에 비치는
SmartPtr<Middle>
과SmartPtr<Top>
은 완전히 별개의 클래스이다. - 따라서
SmartPtr
클래스들 사이에 어떤 변환을 하고 싶다면, 변환이 되도록 직접 프로그램을 만들어야 한다.
- 같은 템플릿으로부터 만들어진 다른 인스턴스들 사이에는 어떤 관계도 없기 때문에, 컴파일러의 눈에 비치는
- 위의 코드를 보면 모든 문장이 하나같이 new를 써서 스마트 포인터 객체를 만들고 있다.
- 그런 의미에서, 스마트 포인터의 생성자를 우리가 원하는 대로 동작하게끔 작성하는 쪽에 초점을 맞춘다.
- 생성자 함수를 직접 만드는 것으로는 우리에게 필요한 모든 생성자를 만들어내기란 불가능하다.
- 위의 클래스 계통에서는 Top - Middle - Bottom의 관계를 가지고 있지만, 나중에 확장되었을 때 다른 스마트 포인터 타입으로부터
Top
객체를 만들 방법도 마련되어야 하기 때문이다.
- 위의 클래스 계통에서는 Top - Middle - Bottom의 관계를 가지고 있지만, 나중에 확장되었을 때 다른 스마트 포인터 타입으로부터
- 원칙적으로 지금 우리가 원하는 생성자의 개수는 무제한이다.
- 템플릿을 인스턴스화하면 무제한 개수의 함수를 만들어 낼 수 있다.
- 그래서 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>
안에 들어 있기 때문이다. - 이런 꼴의 생성자 - 같은 템플릿을 써서 인스턴스화되지만 타입이 다른 타입의 객체로부터 원하는 객체를 만들어 주는 - 즉, SmartPtr로부터 SmartPtr를 만들어내는 생성자를 가리켜 일반화 복사 생성자라고 부른다.
- 위의 복사 생성자는 explicit으로 선언되지 않았다. 기본제공 포인터 타입 사이의 타입 변환이 암시적으로 이루어지며 캐스팅이 필요하지 않기 때문에 스마트 포인터도 동일한 형태로 동작하도록 하기 위하여 작성했다.
- 현재 만들어 놓은 일반화 복사 생성자는 실제로 원하는 동작보다 더 많은 것을 한다.
SmartPtr<Bottom>
으로부터SmartPtr<Top>
을 만들 수 있다.SmartPtr<Top>
으로부터SmartPtr<Bottom>
도 만들 수 있다.(원하지 않는 동작)- 이는 public 상속의 의미를 역행한다.
SmartPtr
도 get 멤버 함수를 통해 해당 스마트 포인터 객체에 자체적으로 담긴 기본제공 포인터 사본을 반환하다고 가정하면, 이를 이용해 생성자 템플릿에 우리가 원하는 타입 제약을 줄 수 있다.
template<typename T>
class SmartPtr {
public:
// 이 SmartPtr에 담긴 포인터를 다른 SmartPtr에 담긴 포인터로 초기화
template<typename U>
SmartPtr(const SmartPtr<U>& other) : heldPtr(other.get()) {};
T* get() const { reutrn heldPtr; }
private:
T* heldPtr; // SmartPtr에 담긴 기본 제공 포인터
};
- 멤버 초기화 리스트를 사용해서
SmartPtr<T>
의 데이터 멤버인 T* 타입의 포인터를SmartPtr<U>
에 들어 있는 U* 타입의 포인터로 초기화했다. - 이렇게 하면 U에서 T로 진행되는 암시적 변환이 가능할 때만 컴파일 에러가 나지 않는다.
- 이제 SmartPtr의 일반화 복사 생성자는 호환되는 타입의 매개변수를 넘겨받을 때만 컴파일된다.
- 멤버 함수 템플릿은 생성자에만 이용할 수 있는 것이 아니다. 가장 흔히 쓰이는 예는 대입 연산이다.
template<typename T>
class shared_ptr {
public:
template<class Y>
explicit shared_ptr(Y* p); // 호환되는 모든 기본제공 포인터
// shared_ptr, weak_ptr, auto_ptr 객체로부터 생성자 호출 가능
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>& r);
// 호환되는 모든 shared_ptr, auto_ptre로부터 대입 가능
template<class Y>
shared_ptr& operator=(shared_ptr<Y> const& r);
template<class Y>
shared_ptr& operator=(auto_ptr<Y>& r);
};
- 일반화 복사 생성자를 제외하고 모든 생성자가 explicit으로 선언되어 있다.
- 이는
share_ptr
로 만든 어떤 타입으로부터 또 다른 타입으로 진행되는 암시적 변환은 허용되지만, 기본제공 포인터 혹은 또 다른 타입으로부터 변환되는 것은 막겠다는 의미이다.
- 이는
- shared_ptr 생성자와 대입 연산자에 넘겨지는 auto_ptr이 const로 선언되지 않는 것은 auto_ptr은 복사 연산으로 인해 객체가 수정될 때 오직 복사된 쪽 하나만 유효하기 때문이다.
- 멤버 템플릿은 언어의 규칙을 바꾸지는 않는다. 멤버 템플릿 생성자가 있더라도 일반 생성자 정의는 필요하다. 복새 대입 연산자도 마찬가지이다.(이 함수들은 컴파일러가 없을 경우 자동으로 생성해주는 함수이다.)
항목 46: 타입 변환이 바람직할 경우에는 비멤버 함수를 클래스 템플릿 안에 정의해 두자
- 모든 매개변수에 대해 암시적 타입 변환이 되도록 만들기 위해서는 비멤버 함수밖에 방법이 없다.
template<typename T>
class Rational {
public:
Rational(const T& numerator = 0, const T& denominator);
const T numerator() const;
const T demonimator() const;
};
// 함수 템플릿 operator*
template<typename T>
const Rational<T> operator*(const Rational<T>& lhs, const Rational<T>& rhs);
- 혼합형 수치 연산이 필요하기 때문에
operator*
를 재정의하였는데, 다음과 같은 코드는 컴파일 에러가 발생한다.
Rational<int> oneHalf(1, 2);
Rational<int> result = oneHalf * 2;
- 그 이유는 혼합형 수치 연산을 위해 호출해야 하는
operator*
함수가 무엇인지 컴파일러가 알 수 없기 때문이다.- 컴파일러는
operator*
라는 이름의 템플릿으로부터 인스턴스화할 함수를 결정하기 위해 온갖 계산을 동원하기만 한다. - 이 시점에 컴파일러가 확실히 아는 것은
Rational<T>
타입의 매개변수를 두 개 받아들이는operator*
라는 이름의 함수를 인스턴스화해야 하는 것 뿐이다. - 여기에서 컴파일러는 T가 무엇인지 알 방법이 없다.
- 컴파일러는
- T의 정체를 파악하기 위해 컴파일러는 우선
operator*
호출 시에 넘겨진 인자의 모든 타입을 살핀다. 지금의 경우Rational<int>
및int
다.oneHalf
의 경우 첫 번째 매개변수가 되는데,Rational<int>
타입이기 때문에 T는 확실하게int
가 된다.2
의 경우 타입을 유추하기가 힘들다.Rational<int>
에는 explicit으로 선언되지 않은 생성자가 있기 때문에, 컴파일러가 이를 이용해서 2를Rational<int>
로 변환하고 이를 통해 T가int
라고 유추할 수 있다고 예상해볼 수 있다. 하지만 컴파일러는 이렇게 작동하지 않는다.- 그 이유는 템플릿 인자 추론 과정에서는 암시적 타입 변환이 고려되지 않기 때문이다.
- 함수 호출에 필요한 매개변수의 타입 변환은 진행 되는게 당연하지만, 어떤 함수를 호출해야 하는지 미리 알고 있어야 가능한 일이다.
- 템플릿 인자 추론을 하는 방법은 클래스 템플릿 내부에 프렌드 함수를 넣어 두는 것이다.
- 프렌드 함수를 넣는 것을 통해 함수 템플릿으로서의 성격을 주지 않고 특정한 함수 하나를 나타낼 수 있다는 사실을 이용하는 방법이다.
- 이렇게 되면
Rational<T>
클래스에 대해operator*
를 프렌드 함수로 선언하는 것이 가능하다. - 클래스 템플릿은 템플릿 인자 추론 과정에 좌우되지 않는다.(함수 템플릿에만 적용되는 과정)
- 따라서 T의 정확한 정보는
Rational<T>
클래스가 인스턴스화될 당시에 바로 알 수 있다.
template<typename T>
class Rational {
public:
// operator* 함수 선언
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>
타입의 매개변수를 받는 프렌드 함수인operator*
도 자동으로 선언된다. - 이전과 달리 지금은 함수가 선언된 것이므로(함수 템플릿이 아니라) 컴파일러는 이 함수 호출문에 대해 암시적 타입 변환을 적용할 수 있게 된다.
- 그러나 현재 코드는 컴파일은 되지만, 링크가 되지 않는다.
- 문법에 대해 먼저 살펴보자.
- 클래스 템플릿 내부에서는 템플릿의 이름(
<>
뗀 것)을 그 템플릿 및 매개변수의 줄임말로 쓸 수 있다.Rational<T>
안에서는Rational
이라고만 써도Rational<T>
가 된다.- 위의 예제에서는 타이핑을 줄이기 위해 이를 사용했다.
- 따라서 위의 코드는 다음과 정확히 똑같은 의미를 가진다.
template<typename T>
class Rational {
public:
// operator* 함수 선언
friend const Rational<T> operator*(const Rational<T>& lhs, const Rational<T>& rhs);
};
- 링크의 문제로 돌아오면, 컴파일러가 우선 어떤 함수를 호출해야 하는지를 알게 됐으니 컴파일의 문제는 해결 됐다.
- 그런데 이 함수는
Rational
안에서 선언만 되어 있지, 정의까지 되어 있는 것은 아니다. - 클래스 외부에 있는 프렌드 템플릿에서 함수 정의를 제공하고 싶은게 의도였다.
- 따라서 가장 간단한 해결 방법은 함수의 본문을 선언부와 붙이는 것이다.
template<typename T>
class Rational {
public:
// operator* 함수 선언 및 정의
friend const Rational operator*(const Rational& lhs, const Rational& rhs)
{
return Raional(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator());
}
};
- 이제 컴파일, 링크가 모두 정상적으로 되는 코드가 나왔다.
- 프렌드 함수를 선언했지만, 클래스의 public 영역이 아닌 부분에 접근하기 위함과는 아무런 관계가 없다는 것을 명심해야 한다.
- 모든 인자에 대해 타입 변환이 가능하도록 만들기 위해 비멤버 함수가 필요하고, 호출 시의 상황에 맞는 함수를 자동으로 인스턴스화하기 위해서는 그 비멤버 함수를 클래스 안에 선언해야 하는데, 클래스 안에서 비멤버 함수를 선언하는 방법이 프렌드 함수 밖에 없기 때문이다.
- 클래스 안에 정의된 함수는 암시적으로 인라인으로 선언된다.
- 따라서 클래스의 바깥에서 정의된 도우미 함수만 호출하는 식으로 구현하면 함수의 본문이 복잡한 경우에는 좋은 성능을 기대해볼 수 있다.
// Rational 템플릿 선언
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);
}
};
- 대다수의 컴파일러에서 템플릿 정의를 헤더 파일에 전부 넣을 것을 사실상 강제로 강요하다시피 하고 있으니,
doMultiply
함수도 헤더 파일 안에 정의해 넣어야 할 것이다.- 이런 템플릿은 반드시 인라인일 필요는 없다.
- 정의를 추가하면 다음과 같은 형태가 될 것이다.
// 도우미 함수 템플릿 선언 및 정의
template<typename T>
const Rational<T> doMultiply(const Rational<T>& lhs, const Rational<T>& rhs)
{
return Rational<T>(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator());
}
doMultiply
함수는 템플릿으로서 혼합형 곱셈을 지원하지 못하지만, 지원할 필요가 없다.- 이 템플릿을 사용하는 고객은
operator*
밖에 없는데,operator*
가 이미 혼합형 연산을 지원하고 있기 때문이다.operator*
함수는 자신이 받아들이는 매개변수가 제대로 곱해지도록 어떤 타입도 Rational 객체로 바꿔준다.- 이렇게 바꾼 Rational 객체 두 개는
doMultiply
템플릿 함수의 인스턴스가 받아 실제 곱셈에 사용한다.
메모
항목 45: “호환되는 모든 타입”을 받아들이는 데는 멤버 함수 템플릿이 직방!
- 호환되는 모든 타입을 받아 들이는 멤버 만들려면 멤버 함수 템플릿을 사용합시다.
- 일반화된 복사 생성 연산과 일반화된 대입 연산을 위해 템플릿을 선언한다. 하더라도, 보통의 복사 생성자와 복사 대입 연산자는 여전히 직접 선언해야 한다.
항목 46: 타입 변환이 바람직할 경우에는 비멤버 함수를 클래스 템플릿 안에 정의해 두자
- 모든 매개변수에 대해 암시적 타입 변환을 지원하는 템플릿과 관계가 있는 함수를 제공하는 클래스 템플릿을 만들려고 한다면, 이런 함수는 클래스 템플릿 안에 프렌드 함수로서 정의하자.
'책 > 이펙티브 C++' 카테고리의 다른 글
이펙티브 C++(332p ~ 340p) (1) | 2024.02.09 |
---|---|
이펙티브 C++(312p ~ 320p) (1) | 2024.02.08 |
이펙티브 C++(306p ~ 312p) (1) | 2024.02.06 |
이펙티브 C++(295p ~ 306p) (0) | 2024.02.05 |
이펙티브 C++(279p ~ 290p) (1) | 2024.02.03 |