43: 템플릿으로 만들어진 기본 클래스 안의 이름에 접근하는 방법을 알아 두자
몇 개의 회사에 메시지를 전달하는 프로그램을 만드는 상황이라고 가정하자. 어느 회사에 보낼지를 컴파일 도중에 결정할 수 있다면 템플릿 기반의 방법을 쓸 수 있다.
class CompanyA {
public:
void sendCleartext(const std::string& msg);
void sendEncrypted(const std::string& msg);
};
class CompanyB {
public:
void sendCleartext(const std::string& msg);
void sendEncrypted(const std::string& msg);
};
class MsgInfo {};
template<typename Company>
class MsgSender {
public:
void sendClear(const MsgInfo& info)
{
std::string msg;
Company c;
c.sendCleartext(msg);
}
void sendSecret(const MsgInfo& info)
{
std::string msg;
Company c;
c.sendEncrypted(msg);
}
};
여기에 메시지를 보낼 때마다 로그를 남기는 기능을 추가해보자. 파생 클래스를 사용하면 이 기능을 쉽게 붙일 수 있고, 그렇게 해 주는 게 맞을 것 같다.
template<typename Company>
class LoggingMsgSender : public MsgSender<Company> {
public:
void sendClearMsg(const MsgInfo& info)
{
// 전송 전 정보를 로그에 기록한다.
sendClear(info); // 기본 클래스의 함수를 호출하지만, 컴파일 되지 않는다!
// 전송 후 정보를 로그에 기록한다.
}
};
위는 기본 클래스로부터 물려받은 이름을 파생 클래스에서 가리는 문제(항목 33)는 물론이고 상속받은 비가상 함수를 재정의하는 문제(항목 36)를 일으키지 않는다. 그것까진 정말 좋지만, 이 코드는 컴파일되지 않는다.
'sendClear 함수가 존재하지 않는다'라는 것이 컴파일이 안 되는 이유이다. 컴파일러는 기본 클래스를 들여다보려고 하지도 않는 것이다. 문제는 간단하다. 컴파일러가 LogginMsgSender 클래스 템플릿의 정의와 마주칠 때, 대체 이 클래스가 어디서 파생된 것인지를 모른다는 것이다. 이 템플릿 매개변수는 나중(LoggingMsgSender가 인스턴스로 만들어질 때)까지 무엇이 될지 알 수 없다.
문제를 구체적으로 알기 위해 가정을 하나 하자. CompanyZ라는 클래스가 있고, 이 클래스는 암호화된 통신만을 사용해야 한다.
class CompanyZ {
public:
void sendEncrypted(const std::string& msg);
};
일반형 MsgSender 템플릿을 쓰기에는 좀 그렇다. sendClear 함수를 제공하지 않기 때문이다. 이 부분을 바로잡기 위해, CompanyZ를 위한 MsgSender의 특수화 버전을 만들 수 있다.
template<>
class MsgSender<CompanyZ> {
public:
void sendSecret(const MsgInfo& info)
{
std::string msg;
Company c;
c.sendEncrypted(msg);
}
};
template<>는 이건 템플릿도 아니고 클래스도 아니다라는 것이다. 템플릿 매개변수가 CompanyZ일 때 쓸 수 있도록 특수화한 버전이다. 이런 특수화를 완전 템플릿 특수화(total template specialization)이라고 한다. 이제 앞의 LogginMsgSender로 돌아와 보자.
template<typename Company>
class LoggingMsgSender : public MsgSender<Company> {
public:
void sendClearMsg(const MsgInfo& info)
{
// 전송 전 정보를 로그에 기록한다.
sendClear(info); // Comapny == CompanyZ라면 이 함수는 있을 수조차 없다!
// 전송 후 정보를 로그에 기록한다.
}
};
기본 클래스가 MsgSender<CompanyZ>라면 이 코드는 말이 되지 않는다. sendClear 함수가 없기 때문이다. 이런 일이 생길 수 있기 때문에 위와 같은 함수 호출을 C++가 받아주지 않는 것이다.
이걸 해결하기 위해서는 C++의 "난 템플릿화된 기본 클래스는 멋대로 안 뒤질 거야" 동작이 발현되지 않도록 해야 한다. 방법은 세 가지이다. 첫째, 기본 클래스 함수에 대한 호출문 앞에 "this->"를 붙인다.
template<typename Company>
class LoggingMsgSender : public MsgSender<Company> {
public:
void sendClearMsg(const MsgInfo& info)
{
// 전송 전 정보를 로그에 기록한다.
this->sendClear(info);
// 전송 후 정보를 로그에 기록한다.
}
};
둘째, using 선언을 사용한다.
template<typename Company>
class LoggingMsgSender : public MsgSender<Company> {
public:
void sendClearMsg(const MsgInfo& info)
{
// 컴파일러에게 sendClear함수가 기본 클래스에 있다고 가정하라고 알려준다.
using MsgSender<Company>::sendClear;
// 전송 전 정보를 로그에 기록한다.
sendClear(info);
// 전송 후 정보를 로그에 기록한다.
}
};
마지막 방법은, 호출할 함수가 기본 클래스의 함수라는점을 명시적으로 지정하는 것이다.
template<typename Company>
class LoggingMsgSender : public MsgSender<Company> {
public:
void sendClearMsg(const MsgInfo& info)
{
// 전송 전 정보를 로그에 기록한다.
MsgSender<Company>::sendClear(info);
// 전송 후 정보를 로그에 기록한다.
}
};
마지막 방법은 추천하지 않는다. 호출되는 함수가 가상 함수라면, 가상 함수 바인딩이 무시되기 때문이다.
위의 세 가지 방법 모두 동작 원리는 같다. 기본 클래스 템플릿이 이후에 어떻게 특수화되더라도 원래의 일반형 템플릿에서 제공하는 인터페이스를 그대로 제공할 것이라고 컴파일러에게 약속을 하는 것이다.
본질적인 논점은 이렇다. 기본 클래스의 멤버에 대한 참조가 무효한지를 컴파일러가 진단하는 과정이 미리(파생 클래스 템플릿의 정의가 구문분석될 때) 들어가느냐, 아니면 나중에(파생 클래스 템플릿이 특정한 템플릿 매개변수를 받아 인스턴스화 될 때) 들어가느냐가 바로 이번 항목의 핵심이다. 여기서 C++는 이른 진단을 선호하는 정책으로 결정한 것이다.
이것만은 잊지 말자!
- 파생 클래스 템플릿에서 기본 클래스 템플릿의 이름을 참조할 때는 "this->"를 접두사로 붙이거나 기본 클래스 한정문을 명시적으로 써 주는 것으로 해결하자.
'C++ > Effective C++' 카테고리의 다른 글
[Effective C++] 45: "호환되는 모든 타입"을 받아들이는 데는 멤버 함수 템플릿이 직방! (0) | 2023.11.15 |
---|---|
[Effective C++] 44: 매개변수에 독립적인 코드는 템플릿으로부터 분리시키자 (1) | 2023.11.13 |
[Effective C++] 42: typename의 두 가지 의미를 제대로 파악하자 (1) | 2023.11.11 |
[Effective C++] 41: 템플릿 프로그래밍의 천릿길도 암시적 인터페이스와 컴파일 타임 다형성부터 (0) | 2023.11.10 |
[Effective C++] 40: 다중 상속은 심사숙고해서 사용하자 (2) | 2023.11.09 |