로또
43. 템플릿으로 만들어진 기본 클래스 안의 이름에 접근하는 방법을 알아 두자 본문
다루는 내용
기본 클래스 템플릿 함수에 접근할 때 발생할 수 있는 문제점과 그 해결 방법에 대해서 알아보자.
기본 클래스 템플릿의 함수 호출 문제
회사 별로 클래스를 두어 메세지를 전송하는 응용 프로그램의 예제이다. 각 Company 클래스에서 SendClearText 함수로 메세지 원문을 보낼 수도 있고, SendEncrypted 함수로 암호화된 메세지를 전송할 수도 있다.
#include <string>
using namespace std;
// 기업 별 클래스
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;
// info를 기반으로 string msg 생성
Company c;
c.SendClearText(msg);
}
void SendSecret(const MsgInfo& info)
{
string msg;
// info를 기반으로 string msg 생성
Company c;
c.SendEncrypted(msg);
}
};
// 메세지 전송 로그를 남기기 위한 클래스 템플릿
template<typename Company>
class LoggingMsgSender: public MsgSender<Company>{
public:
void SendClearMsg(const MsgInfo& info)
{
// 메세지 전송 전 정보를 로그에 기록한다.
SendClear(info); // 기본 클래스 템플릿의 함수 호출, 컴파일 에러
// 메세지 전송 후 정보를 로그에 기록한다.
}
};
int main(){
return 0;
}
LoggingMsgSender 클래스에서 기본 클래스(MsgSender<Company>)로부터 물려받은 이름을 가리지도 않고 상속받은 비가상 함수를 재정의하지도 않는다. 큰 문제가 없어 보이지만 다음과 같은 컴파일 에러가 발생한다.

에러 메세지를 해석해보면, template parameter에 의존하는 SendClear에 대한 arguments가 존재하지 않는다고 한다. 책에서는 SendClear 라는 함수가 존재하지 않아서 발생하는 오류라고 한다.
이러한 오류는 컴파일러가 LoggingMsgSender 클래스 템플릿 정의에 닿았을 때, 해당 클래스가 무엇으로부터 파생된 것인지 모르기 때문에 발생한다. 기본 클래스는 MsgSender<Company>로 명시되어 있지만 <Company> 는 템플릿 매개변수일 뿐, 실제로 Company가 정확히 무엇인지 모르는 상황에서는 MsgSender<Company> 또한 알 수 없을 것이다.
즉, 기본 클래스가 어떤 형태인지 알 수 없으므로 기본 클래스에 SendClear 함수가 존재하는지도 알 수 없을 것이다.
만약, 암호화된 메세지 전송만을 원하는 CompanyZ를 추가한다고 가정해보자.
// 암호화된 메세지만 전송하는 CompanyZ
class CompanyZ{
public:
void SendEncrypted(const string& msg){}
...
};
// MsgSender 템플릿의 CompanyZ에 대한 완전 템플릿 특수화
template<>
class MsgSender<CompanyZ>{
public:
void SendSecret(const MsgInfo& info){...}
};
완전 템플릿 특수화를 사용해 MsgSender 클래스의 템플릿 매개변수가 CompanyZ일 때 사용할 수 있도록 만들었다.
완전 템플릿 특수화는 "25. 예외를 던지지 않는 swap에 대한 지원도 생각해보자." 에서 다룬 적이 있다.
이처럼 특수화 버전이 추가된다면, LoggingMsgSender에서 SendClear 함수를 호출하는 것은 존재하지 않는 함수를 호출하는 것과 마찬가지이다.
기본 클래스 템플릿은 언제라도 특수화될 수 있고, 특수화 버전에서 제공하는 인터페이스가 항상 일반형 템플릿과 동일하지 않을 수 있기에 C++ 컴파일러는 템플릿으로 만들어진 기본 클래스에서 상속된 이름을 찾지 않는다.
기본 클래스 템플릿의 함수 호출 방법
1. 기본 클래스 함수에 대한 호출문 앞에 this 사용하기
// 메세지 전송 로그를 남기기 위한 클래스 템플릿
template<typename Company>
class LoggingMsgSender: public MsgSender<Company>{
public:
void SendClearMsg(const MsgInfo& info)
{
// 메세지 전송 전 정보를 로그에 기록한다.
this->SendClear(info); // SendClear가 상속되는 것으로 가정한다.
// 메세지 전송 후 정보를 로그에 기록한다.
}
};
2. using 선언 사용하기
// 메세지 전송 로그를 남기기 위한 클래스 템플릿
template<typename Company>
class LoggingMsgSender: public MsgSender<Company>{
public:
using MsgSender<Company>::SendClear; // 기본 클래스에서 SendClear 함수가 존재한다고 가정하라.
void SendClearMsg(const MsgInfo& info)
{
// 메세지 전송 전 정보를 로그에 기록한다.
SendClear(info);
// 메세지 전송 후 정보를 로그에 기록한다.
}
};
3. 기본 클래스의 함수라는 점을 명시적으로 지정하기
// 메세지 전송 로그를 남기기 위한 클래스 템플릿
template<typename Company>
class LoggingMsgSender: public MsgSender<Company>{
public:
void SendClearMsg(const MsgInfo& info)
{
// 메세지 전송 전 정보를 로그에 기록한다.
MsgSender<Company>::SendClear(info); // SendClear가 상속되는 것으로 가정한다.
// 메세지 전송 후 정보를 로그에 기록한다.
}
};
3번 방법은 호출되는 함수가 가상 함수일 경우에도 기본 클래스의 함수가 호출되므로 지양하도록 하자.
세 가지 방법은 모두 기본 클래스 템플릿이 이후에 어떻게 특수화되더라도 원래의 일반형 템플릿에서 제공하는 인터페이스를 그대로 제공할 것이라고 컴파일러에게 약속을 하는 것이다.
물론, CompanyZ 클래스처럼 완전 템플릿 특수화를 사용한 경우에는 다음과 같이 컴파일 에러가 발생할 것이다.
// 암호화된 메세지만 전송하는 CompanyZ
class CompanyZ{
public:
void SendEncrypted(const string& msg){}
};
// MsgSender 템플릿의 CompanyZ에 대한 완전 템플릿 특수화
template<>
class MsgSender<CompanyZ>{
public:
void SendSecret(const MsgInfo& info){}
};
int main(){
LoggingMsgSender<CompanyZ> z_msg_sender;
MsgInfo msg_data;
// msg_data에 대한 입력
z_msg_sender.SendClearMsg(msg_data); // 컴파일 에러
return 0;
}

컴파일러는 z_msg_sender의 기본 클래스가 MsgInfo<CompanyZ> 라는 것을 알 수 있기 때문에, SendClear 함수가 해당 기본 클래스에도 존재하지 않음을 알 수 있어 컴파일 에러를 발생시킬 수 있는 것이다.
기본 클래스의 멤버에 대한 참조가 무효한지를 컴파일러가 진단하는 과정이 파생 클래스 템플릿의 정의가 구문분석될 때 들어가느냐, 아니면 나중에 파생 클래스 템플릿이 특정한 템플릿 매개변수를 받아 인스턴스화 될 때 들어가느냐가 이번 항목의 핵심이라고 한다.
C++은 이른 진단(early diagnose)를 선호하여 파생 클래스가 템플릿으로부터 인스턴스화될 때 컴파일러가 기본 클래스의 내용에 대해 아무것도 모르는 것으로 가정한다.
이것만은 잊지 말자!
- 파생 클래스 템플릿에서 기본 클래스 템플릿의 이름을 참조할 때는, "this->"를 접두사로 붙이거나 using 선언, 기본 클래스 한정문을 명시적으로 써 주는 것으로 해결하자.
'책 > Effective C++' 카테고리의 다른 글
| 49. new 처리자의 동작 원리를 제대로 이해하자. (1) | 2023.12.28 |
|---|---|
| 44. 매개변수에 독립적인 코드는 템플릿으로부터 분리시키자 (0) | 2023.12.15 |
| 42. typename의 두 가지 의미를 제대로 파악하자. (0) | 2023.12.13 |
| 41. 템플릿 프로그래밍의 천릿길도 암시적 인터페이스와 컴파일 타임 다형성부 (1) | 2023.12.12 |
| 40. 다중 상속은 심사숙고해서 사용하자. (0) | 2023.12.09 |