[C++] 항목 46: 타입 변환이 바람직할 경우에는 비멤버 함수를 클래스 템플릿 안에 정의해 두자

업데이트:     Updated:

카테고리:

태그:

이 글은 아래의 책을 자세히 정리한 후, 정리한 글을 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;

컴파일러의 추론 과정:

  1. oneHalfRational<int> 타입Tint가 되며, 문제가 없다
  2. 2int 타입이지만, 컴파일러는 Tint라고 유추하지 못한다
    • 템플릿 인자 추론 과정에서는 암시적 타입 변환이 고려되지 않기 때문

핵심 원칙:

템플릿 인자 추론이 진행되는 동안에는 생성자 호출을 통한 암시적 타입 변환이 고려되지 않는다.


✅ 해결 방법: 프렌드 함수 사용

핵심 아이디어:

  • 클래스 템플릿 안에 프렌드 함수를 넣어두면, 함수 템플릿으로서의 성격을 주지 않고, 특정한 함수 하나를 나타낼 수 있다
  • 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; // 컴파일은 됨!
}

동작 원리:

  1. oneHalf 객체가 Rational<int> 타입으로 선언되면,
    Rational<int> 클래스가 인스턴스로 만들어진다
  2. Rational<int> 타입의 매개변수를 받는 프렌드 함수인 operator*도 자동 선언된다
  3. 이제, 함수 템플릿이 아니게 되었으므로,
    컴파일러는 암시적 변환 함수를 적용할 수 있다

🚨 새로운 문제: 링크 에러

문제:

  • 그러나, 컴파일은 되지만 링크는 되지 않는다
  • 선언은 있지만, 함수의 정의가 보이지 않기 때문이다
  • 클래스 외부의 operator* 템플릿에서 함수의 정의를 제공하기를 원했지만, 되지 않는다

링크가 되지 않는 이유:

  1. 클래스 안의 프렌드 선언: 비템플릿 함수 선언
    • Rational<int>가 인스턴스화되면, operator*<int>와 같은 상태가 된다
  2. 클래스 밖의 정의: 함수 템플릿 정의
    • Rational<int>가 인스턴스화되면, operator*<T>와 같은 상태가 된다
  3. 결과: 서로 다른 함수 시그니처이기에 링크가 연결할 수 없다

📌 템플릿 이름 줄임말

참고사항:

  • 클래스 템플릿 내부에서는 템플릿의 이름을 그 템플릿 및 매개변수의 줄임말로 쓸 수 있다
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 함수가 지원해주기 때문

📊 해결 방법 비교

방법 장점 단점 권장도
클래스 외부 템플릿 명확한 구조 암시적 변환 불가
프렌드 함수 (인라인) 간단, 타입 변환 가능 복잡한 로직엔 부적합 ⭐⭐
프렌드 + 도우미 함수 타입 변환 가능, 인라인 회피 약간 복잡한 구조 ⭐⭐⭐

🧐 정리

  1. 모든 매개변수에 대해 암시적 타입 변환을 지원하는 템플릿과
    관계가 있는 함수를 제공하는 클래스 템플릿을 만들려고 한다면
    ,
    이런 함수는 클래스 템플릿 안에 프렌드 함수로서 정의합시다.
  2. 템플릿 인자 추론 과정에서는 암시적 타입 변환이 고려되지 않는다.
    프렌드 함수를 사용하여 이 문제를 우회할 수 있다.
  3. 프렌드 함수는 클래스가 인스턴스화될 때 함께 선언되므로, 템플릿 인자 추론 없이 사용할 수 있다.
    이를 통해 암시적 타입 변환이 가능해진다.
  4. 복잡한 로직의 경우 프렌드 함수에서 도우미 함수를 호출하는 패턴을 사용하자.
    이렇게 하면 인라인 문제를 피하면서도 타입 변환의 이점을 누릴 수 있다.

Cpp 카테고리 내 다른 글 보러가기

댓글남기기