[C++] 항목 43: 템플릿으로 만들어진 기본 클래스 안의 이름에 접근하는 방법을 알아 두자
카테고리: Cpp
태그: Cpp
이 글은 아래의 책을 자세히 정리한 후, 정리한 글을 GPT에게 요약을 요청하여 작성되었습니다.
이펙티브 C++ 제3판, 스콧 마이어스 저자, 곽용재 번역
📦 7. 템플릿과 일반화 프로그래밍
👉🏻 항목 43: 템플릿으로 만들어진 기본 클래스 안의 이름에 접근하는 방법을 알아 두자
✅ 문제 상황: 컴파일러가 기본 클래스를 탐색하지 못함
다른 회사들에 비암호화/암호화 메시지를 전송할 수 있도록 만든 프로그램 예시:
class CompanyA {
public:
	...
	void sendCleartext(const string& msg);
	void sendEncrypted(const string& msg);
	...
};
class CompanyB {
public:
	...
	void sendCleartext(const string& msg);
	void sendEncrypted(const string& msg);
	...
};
... // 다른 회사를 나타내는 클래스들
// 메시지 생성에 사용되는 정보를 담기 위한 클래스
class MsgInfo { ... };
template<typename Company>
class MsgSender {
public:
	... // 생성자, 소멸자 등등
	// 기본 클래스의 함수 이름과 다르게 하여,
	// 비가상 함수 재정의 문제를 피함
	void sendClear(const MsgInfo& info) {
		string msg;
		Company c;
		c.sendCleartext(msg);
	}
	// c.sendEncrypted를 호출하는 함수
	void sendSecret(const MsgInfo& info) { ... }
};
문제가 있는 파생 클래스
template<typename Company>
class LoggingMsgSender: public MsgSender<Company> {
public:
	... // 생성자, 소멸자 등등
	void sendClearMsg(const MsgInfo& info) {
		"메시지 전송 전" 정보를 로그에 기록;
		sendClear(info); // ❌ 컴파일 에러!
		"메시지 전송 후" 정보를 로그에 기록;
	}
};
문제:
- 이 코드는 실행되지 않는다
 - 컴파일러는 
MsgSender<Company>에서 Company가 무엇인지 정확히 알 수 없다 
📌 왜 이런 문제가 발생하는가?
시나리오: 암호화 통신만 사용하는 CompanyZ
class CompanyZ {
public:
	...
	void sendEncrypted(const string& msg);
	...
};
// 기존 MsgSender 클래스
template<typename Company>
class MsgSender {
public:
	...
	void sendClear(const MsgInfo& info) { ... }
	void sendSecret(const MsgInfo& info) { ... }
};
// 완전 템플릿 특수화
// MsgSender의 템플릿 매개변수가 CompanyZ일 때의 버전이다.
template<>
class MsgSender<CompanyZ> {
public:
	...
	void sendSecret(const MsgInfo& info) { ... }
	// sendClear 함수가 없음!
};
완전 템플릿 특수화:
template<> class MsgSender
{ ... } 
- 템플릿 매개변수가 CompanyZ일 때 사용되는 클래스이다
 - CompanyZ는 암호화된 통신만을 사용한다고 가정
 - MsgSender 템플릿은 sendClear 함수를 가지고 있으므로, 특수화 버전에서는 제거
 
⚠️ 컴파일러의 딜레마
template<typename Company>
class LoggingMsgSender: public MsgSender<Company> {
public:
	...
	void sendClearMsg(const MsgInfo& info) {
		"메시지 전송 전" 정보를 로그에 기록;
		// Company == CompanyZ라면, 이 함수는 존재하지 않는다!
		sendClear(info);
		"메시지 전송 후" 정보를 로그에 기록;
	}
};
컴파일러의 고민:
“지금은 Company가 뭔지 모르니까,
MsgSender<Company>안에 sendClear가 있는지도 아직 몰라.
sendClear(info)가 뭔지 모르니까 컴파일 에러를 내자.”
핵심 원리:
Company == CompanyZ라면, sendClear 함수가 존재하지 않는다- 이러한 경우가 있기에 코드가 실행되지 않는다
 - C++ 컴파일러는 기본 클래스 범위를 “미리” 탐색하지 않는다
 - 이 동작이 발현되지 않도록, 나중에 찾도록 해야 한다
 
✅ 해결 방법 1: this-> 접두사 사용
template<typename Company>
class LoggingMsgSender: public MsgSender<Company> {
public:
	...
	void sendClearMsg(const MsgInfo& info) {
		"메시지 전송 전" 정보를 로그에 기록;
		// sendClear가 상속되는 것으로 가정한다.
		this->sendClear(info);
		"메시지 전송 후" 정보를 로그에 기록;
	}
};
동작 원리:
this->를 붙이면 컴파일러에게 의존 이름임을 알림- 나중에(인스턴스화 시) 기본 클래스에서 찾도록 함
 
✅ 해결 방법 2: using 선언 사용
template<typename Company>
class LoggingMsgSender: public MsgSender<Company> {
public:
	// 컴파일러에게 sendClear 함수가
	// 기본 클래스에 있다고 가정하라고 알려준다.
	using MsgSender<Company>::sendClear;
	...
	void sendClearMsg(const MsgInfo& info) {
		"메시지 전송 전" 정보를 로그에 기록;
		// sendClear가 상속되는 것으로 가정
		sendClear(info);
		"메시지 전송 후" 정보를 로그에 기록;
	}
};
동작 원리:
using선언으로 sendClear를 현재 클래스 범위로 가져옴- 컴파일러에게 기본 클래스에 있다고 가정하도록 지시
 
✅ 해결 방법 3: 명시적 한정 (권장하지 않음)
template<typename Company>
class LoggingMsgSender: public MsgSender<Company> {
public:
	...
	void sendClearMsg(const MsgInfo& info) {
		"메시지 전송 전" 정보를 로그에 기록;
		// sendClear 함수가 상속되는 것으로 가정
		MsgSender<Company>::sendClear(info);
		"메시지 전송 후" 정보를 로그에 기록;
	}
};
주의점:
이 방법은 권장하지 않는다.
이유:
- 호출되는 함수가 가상 함수일 때 명시적 한정을 하면
 - 가상 함수 바인딩이 무시된다
 
📊 세 가지 해결 방법 비교
| 방법 | 장점 | 단점 | 권장도 | 
|---|---|---|---|
| this-> | 간단하고 직관적 | 모든 호출에 붙여야 함 | ⭐⭐⭐ | 
| using 선언 | 한 번만 선언하면 됨 | 클래스 범위 오염 가능 | ⭐⭐⭐ | 
| 명시적 한정 | 명확함 | 가상 함수 바인딩 무시 | ⭐ | 
💡 세 가지 방법의 공통 원리
위의 해결 방법들은 동작 원리가 같다:
기본 클래스 템플릿이 특수화되더라도,
원래의 일반형 템플릿에서 제공하는 인터페이스를
그대로 제공할 것이라고 컴파일러에게 약속을 하는 것
컴파일러의 새로운 판단:
“sendClear는 템플릿 인자(Company)에 의존하는 이름이구나.
그럼 지금은 판단 보류.
인스턴스화될 때 실제 타입 기준으로 찾아보자.”
🚨 런타임 에러 발생 시점
... // 기존 코드
template<typename Company>
class LoggingMsgSender: public MsgSender<Company> {
public:
	...
	void sendClearMsg(const MsgInfo& info) {
		...
		// sendClear 함수가 상속되는 것으로 가정
		this->sendClear(info);
		...
	}
};
int main() {
	LoggingMsgSender<CompanyZ> zMsgSender;
	MsgInfo msgData;
	...
	// ❌ 에러! 컴파일되지 않음.
	zMsgSender.sendClearMsg(msgData);
}
실제 에러 발생:
- 컴파일러는 기본(상속된) 클래스가 
MsgSender<CompanyZ>로 템플릿 특수화 버전이라는 것을 알고 있다 - sendClearMsg 함수 안의 sendClear 함수는 **MsgSender
클래스에 없다**는 것을 알게 된다  - 인스턴스화 시점에 에러 발생
 
📌 핵심 개념: 이름 탐색 시점
일반 상속:
- 컴파일러가 기본 클래스의 이름을 즉시 탐색
 
템플릿 상속:
- 컴파일러가 기본 클래스의 이름을 나중에 탐색 (인스턴스화 시)
 
이유:
- 템플릿 특수화로 인해 기본 클래스의 인터페이스가 달라질 수 있기 때문
 
🧐 정리
- 파생 클래스 템플릿에서 기본 클래스 템플릿의 이름을 참조할 때:
    
this->를 접두사로 사용using선언으로 기본 클래스 멤버를 현재 범위로 가져오기- 기본 클래스 한정문을 명시 (가상 함수에는 권장하지 않음)
 
 - 기본 클래스의 멤버에 대한 참조가 무효한지를 컴파일러가 검사하는 시점이 핵심이다:
    
- ❌ 미리: 파생 클래스 템플릿의 정의가 구문 분석할 때
 - ✅ 나중: 파생 클래스 템플릿이 특정한 템플릿 매개변수를 받아 인스턴스화될 때
 
 - 템플릿 특수화로 인해 기본 클래스의 인터페이스가 달라질 수 있다.
컴파일러는 이를 고려하여 이름 탐색을 지연시킨다. - 가장 안전한 방법은 this-> 또는 using 선언을 사용하는 것이다.
명시적 한정은 가상 함수 바인딩을 무시하므로 주의해야 한다. 
댓글남기기