[C++] 항목 46: 타입 변환이 바람직할 경우에는 비멤버 함수를 클래스 템플릿 안에 정의해 두자
카테고리: Cpp
태그: Cpp
이 글은 아래의 책을 자세히 정리한 후, 정리한 글을 GPT에게 요약을 요청하여 작성되었습니다.
이펙티브 C++ 제3판, 스콧 마이어스 저자, 곽용재 번역
📦 7. 템플릿과 일반화 프로그래밍
👉🏻 항목 46: 타입 변환이 바람직할 경우에는 비멤버 함수를 클래스 템플릿 안에 정의해 두자
✅ 전제: 항목 24의 핵심 복습
모든 매개변수에 대해 암시적 타입 변환이 되도록 만들기 위해서는 비멤버 함수밖에 방법이 없다. (항목 24)
이번 항목의 목표:
- 기억이 나지 않는다면, 다시 한번 찾아보고 오자
 Rational클래스와operator*함수를 템플릿으로 만들 것이다
⚠️ 문제 상황: 템플릿에서 암시적 변환 실패
template<typename T>
class Rational {
public:
	// 매개변수를 참조자로 전달(항목 20)
	Rational(
		const T& numerator = 0,
		const T& denominator = 1
	);
	// 참조가 아닌 값을 반환(항목 28)
	// 함수가 const인 이유(항목 3)
	const T numerator() const;
	const T denominator() const;
	...
};
template<typename T>
const Rational<T> operator*(
	const Rational<T>& lhs,
	const Rational<T>& rhs
) { ... }
int main() {
	// oneHalf(1/2)과 int(2)를 곱하는,
	// 혼합형 수치 연산을 하려한다.
	Rational<int> oneHalf(1, 2); // 템플릿 차이를 빼면 항목 24와 동일
	Rational<int> result = oneHalf * 2; // ❌ 컴파일 에러!
}
문제:
- 분명 항목 24와 다른 점이라곤 템플릿을 사용했다는 것 밖에 없지만, 컴파일이 되지 않는다
 
📌 왜 컴파일이 안 되는가?
Rational
result = oneHalf * 2; 
컴파일러의 추론 과정:
oneHalf는Rational<int>타입 →T는int가 되며, 문제가 없다2는int타입이지만, 컴파일러는T가int라고 유추하지 못한다- 템플릿 인자 추론 과정에서는 암시적 타입 변환이 고려되지 않기 때문
 
핵심 원칙:
템플릿 인자 추론이 진행되는 동안에는 생성자 호출을 통한 암시적 타입 변환이 고려되지 않는다.
✅ 해결 방법: 프렌드 함수 사용
핵심 아이디어:
- 클래스 템플릿 안에 프렌드 함수를 넣어두면, 함수 템플릿으로서의 성격을 주지 않고, 특정한 함수 하나를 나타낼 수 있다
 Rational<T>클래스에 대해operator*를 프렌드 함수로 선언하면 된다
이유:
- 클래스 템플릿은 템플릿 인자 추론 과정에 좌우되지 않는다
 T에 대해선Rational<T>클래스가 인스턴스화될 때 바로 알 수 있다- 함수 템플릿에만 템플릿 인자 추론이 적용된다
 
template<typename T>
class Rational {
public:
	...
	friend Rational operator*(
		const Rational& lhs,
		const Rational& rhs
	);
};
template<typename T>
const Rational<T> operator*(
	const Rational<T>& lhs,
	const Rational<T>& rhs
) { ... }
int main() {
	Rational<int> oneHalf(1, 2);
	Rational<int> result = oneHalf * 2; // 컴파일은 됨!
}
동작 원리:
oneHalf객체가Rational<int>타입으로 선언되면,
Rational<int>클래스가 인스턴스로 만들어진다Rational<int>타입의 매개변수를 받는 프렌드 함수인operator*도 자동 선언된다- 이제, 함수 템플릿이 아니게 되었으므로,
컴파일러는 암시적 변환 함수를 적용할 수 있다 
🚨 새로운 문제: 링크 에러
문제:
- 그러나, 컴파일은 되지만 링크는 되지 않는다
 - 선언은 있지만, 함수의 정의가 보이지 않기 때문이다
 - 클래스 외부의 
operator*템플릿에서 함수의 정의를 제공하기를 원했지만, 되지 않는다 
링크가 되지 않는 이유:
- 클래스 안의 프렌드 선언: 비템플릿 함수 선언
    
Rational<int>가 인스턴스화되면,operator*<int>와 같은 상태가 된다
 - 클래스 밖의 정의: 함수 템플릿 정의
    
Rational<int>가 인스턴스화되면,operator*<T>와 같은 상태가 된다
 - 결과: 서로 다른 함수 시그니처이기에 링크가 연결할 수 없다
 
📌 템플릿 이름 줄임말
참고사항:
- 클래스 템플릿 내부에서는 템플릿의 이름을 그 템플릿 및 매개변수의 줄임말로 쓸 수 있다
 
friend Rational operator*(...);
friend Rational<T> operator*(...);
// 위 두 코드는 동일하다. 그러나 템플릿 함수라는 의미는 아니다.
중요:
템플릿 함수와 비템플릿 함수를 결정짓는 요인은,
함수 바로 앞의template<typename T>존재 여부이다.
✅ 최종 해결: 선언과 정의를 함께
간단히, 해결하기 위해선 operator 함수의 본문을 선언부와 붙여야 한다.
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()
		);
	}
};
결과:
- 이제, 원하던 대로 동작한다
 
주의점:
- 다만, 클래스 안에 정의된 함수는 암시적으로 인라인 선언된다
 operator*프렌드 함수도 적용
💡 프렌드 함수의 오해
프렌드 함수를 선언했지만, 클래스의 public 영역이 아닌 부분에 접근하는 것과 프렌드 권한은 아무 상관이 없다.
진실:
- 프렌드 함수는 클래스의 private 영역에 접근할 수 있도록 해주는 권한을 가질 뿐
 - 그 함수가 템플릿 내부에 위치한다고 해서, 타입 변환 규칙이나 접근 권한이 달라지는 건 아니다
 
✅ 인라인 회피: 도우미 함수 패턴
복잡한 연산의 경우 인라인을 피하기 위해 도우미 함수를 사용할 수 있다:
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<T> operator*(
		const Rational<T>& lhs,
		const Rational<T>& rhs
	) { return doMultiply(lhs, rhs); }
	...
};
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함수가 지원해주기 때문
📊 해결 방법 비교
| 방법 | 장점 | 단점 | 권장도 | 
|---|---|---|---|
| 클래스 외부 템플릿 | 명확한 구조 | 암시적 변환 불가 | ❌ | 
| 프렌드 함수 (인라인) | 간단, 타입 변환 가능 | 복잡한 로직엔 부적합 | ⭐⭐ | 
| 프렌드 + 도우미 함수 | 타입 변환 가능, 인라인 회피 | 약간 복잡한 구조 | ⭐⭐⭐ | 
🧐 정리
- 모든 매개변수에 대해 암시적 타입 변환을 지원하는 템플릿과
관계가 있는 함수를 제공하는 클래스 템플릿을 만들려고 한다면,
이런 함수는 클래스 템플릿 안에 프렌드 함수로서 정의합시다. - 템플릿 인자 추론 과정에서는 암시적 타입 변환이 고려되지 않는다.
프렌드 함수를 사용하여 이 문제를 우회할 수 있다. - 프렌드 함수는 클래스가 인스턴스화될 때 함께 선언되므로, 템플릿 인자 추론 없이 사용할 수 있다.
이를 통해 암시적 타입 변환이 가능해진다. - 복잡한 로직의 경우 프렌드 함수에서 도우미 함수를 호출하는 패턴을 사용하자.
이렇게 하면 인라인 문제를 피하면서도 타입 변환의 이점을 누릴 수 있다. 
댓글남기기