- 파생 클래스 템플릿에서 기본 클래스의 이름을 참조할 때는, this-> 를 접두사로 붙이거나 기본 클래스 한정문을 명시적으로 써 주면 된다.
템플릿으로 만들어지는 기본 클래스의 이름에 접근하는 법
메시지 전송 응용프로그램을 만든다고 가정해 보자.
전송용 메시지는 암호화될 수도 있고 비가공텍스트 형태일 수 있다.
만약 어떤 메시지가 어디로 전송될지를 컴파일 도중에 결정할 수 있는 충분한 정보가 있다면, 템플릿 기반의 방법을 쓸 수 있다.
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 sentEncrypted(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) { ... } // 암호화하여 전송
};
메시지를 보낼 때마다 관련 정보를 로그로 남기고 싶을 수 있다.
파생 클래스를 사용하면 이 기능을 쉽게 붙일 수 있다.
template<typename Company>
class LoggingMsgSender: public MsgSender<Company> {
public:
...
void sendClearMsg(const MsgInfo& info)
{
// 전송 전 로그
sendClear(info); // 기본 클래스 함수 호출, 컴파일 Error
// 전송 후 로그
}
...
};
기본 클래스로부터 물려받은 이름을 파생 클래스에서 가리는 문제와 상속받은 비가상 함수를 재정의하는 문제를 일으키지 않기 위해 기본 클래스의 함수와 이름을 다르게 설계하였다.
하지만, 이 코드는 sendClear 함수가 존재하지 않아 컴파일되지 않는다.
기본 클래스에 sendClear 함수가 존재하지만, 컴파일러는 기본 클래스를 참조하지 못한다.
컴파일러가 LoggingMsgSender 클래스 템플릿의 정의와 마주칠 때, 컴파일러는 이 클래스가 어디서 파생된 것인지 모른다.
MsgSender<Company>인 것은 분명하지만 Company는 템플릿 매개변수이고, 이 템플릿 매개변수는 LoggingMsgSender가 인스턴스로 만들어질 때까지 무엇이 될지 알 수 없다.
Company가 무엇인지 모르는 상황에서는 MsgSender<Company> 클래스가 어떤 형태인지 알 수 없다.
그러니, sendClear 함수가 들어 있는지 없는지 알 수 없다.
추가로 하나의 가정을 해보자.
class CompanyZ { // Clear text를 전송하지 않는 클래스
public:
...
void sendEncrypted(const std::string& msg);
...
};
일반형 MsgSender 템플릿은 그대로 CompanyZ 클래스에 쓰기엔 부적절하다.
이 템플릿은 CompanyZ 객체의 설계와 맞지 않는 sendClear 함수를 제공하고 있기 때문이다.
CompanyZ를 위한 Msgsender의 특수화 버전을 만들어야 한다.
template<>
class MsgSender<CompanyZ> { // MsgSender 템플릿의 완전 특수화
public:
...
void sendSecret(const MsgInfo& info) { ... }
};
template<>를 보면 괄호 안에 아무것도 없다.
이것은 MsgSender 템플릿을 템플릿 매개변수가 CompanyZ일 때 쓸 수 있도록 특수화한 버전이다.
이것을 특수화는 완전 템플릿 특수화라고 한다.
MsgSender 템플릿이 CompanyZ 타입에 대해 특수화되었고, 템플릿의 매개변수들이 하나도 빠짐없이 구체적인 타입으로 정해진 상태라는 뜻이다.
MsgSender 템플릿이 CompanyZ에 대해 특수화된 상태에서 파생 클래스인 LoggingMsgSender를 봐보자.
template<typename Company>
class LoggingMsgSender: public MsgSender<Company> {
public:
...
void sendClearMsg(const MsgOnfo& info)
{
// 전송 전 로깅
sendClear(info);
// 전송 후 로깅
}
};
기본 클래스가 MsgSender<CompanyZ>이면 이 코드를 실행할 수 없다. MsgSender<CompanyZ> 클래스에는 sendClear 함수가 없기 때문이다.
이런 일이 생길 수 있기 때문에 위와 같은 함수 호출은 허용되지 않는다.
기본 클래스 템플릿은 언제라도 특수화될 수 있고, 이런 특수화 버전에서 제공하는 인터페이스가 원래의 일반형 템플릿과 다를 수 있다. 그렇기 때문에, 컴파일러는 템플릿으로 만들어진 기본 클래스에서 상속된 이름을 찾는 것을 거부한다.
즉, C++에서는 템플릿화된 기본 클래스는 함부로 참조하지 않는다.
이것을 해결하는 방법은 세 가지가 있다.
해결 방법
- 기본 클래스 함수에 대한 호출문 앞에 this-> 를 붙인다.
template<typename Company>
class LoggingMsgSender: public MsgSender<Company> {
public:
...
void sendClearMsg(const MsgInfo& info)
{
// 전송 이전 로깅
this->sendClear(info); // sendClear가 상속되는 것으로 가정
// 전송 이후 로깅
}
};
- using 선언을 사용한다.
가려진 기본 클래스의 이름을 파생 클래스의 유효범위로 끌어오는 용도로 using 선언을 이용할 수 있다.
지금의 경우는 이름이 가려진 문제가 아니라 기본 클래스의 유효범위를 사용하라고 컴파일러에게 알려주는 것이다.
template<typename Company>
class LoggingMsgSender: public MsgSender<Company> {
public:
using MsgSender<Company>::sendClear; // sndClear가 기본 클래스에 있다고 알려줌
...
void sendClearMsg(const MsgInfo& info)
{
...
sendClear(info); // 상속된 것이라 간주
...
}
};
- 호출할 함수가 기본 클래스의 함수라는 것을 명시적으로 지정한다.
template<typename Company>
class LoggingMsgSender: public MsgSender<Company> {
public:
...
void sendClearMsg(const MsgInfo& info)
{
...
MsgSender<Company>::sendClear(info); // 상속되는 것으로 간주
...
}
};
이 방법은 그리 좋은 방법은 아니다. 호출되는 함수가 가상함수인 경우에는, 이런 식으로 명시적 한정을 해 버리면 가상 함수 바인딩이 무시되기 때문이다.
이름에 대한 가시성을 조작한다는 면에서 보면 세 가지 방법 모두 동작 원리가 같다.
기본 클래스 템플릿이 이후에 어떻게 특수화되더라도 원래의 일반형 템플릿에서 제공하는 인터페이스를 그대로 제공할 것이라고 컴파일러에게 약속하는 것이다.
이런 약속은 LoggingMsgSender 등의 파생 클래스 템플릿을 구문분석하는 데 반드시 필요하지만, 지켜지지 않으면 문제가 생길 수 있다.
LoggingMsgSender<CompanyZ> zMsgSender;
MsgInfo msgDate;
...
zMsgSender.sendClearMsg(msgData); // Error
sendClearMsg 호출문은 컴파일되지 않는다.
기본 클래스가 MsgSender<CompanyZ>라는 사실을 컴파일러가 알고 있고, sendClear 함수가 호출하려고 하는 sendClear 함수는 MsgSender<CompanyZ> 클래스 안에 들어 있지 않기 때문이다.
기본 클래스의 멤버에 대한 참조가 무효한지를 컴파일러가 진단하는 과정이 파생 클래스 템플릿의 정의가 구문분석될 때 들어가느냐, 아니면 파생 클래스 템플릿이 특정한 템플릿 매개변수를 받아 인스턴스화될 때 들어가느냐가 핵심이다.
C++은 전자를 택했다.
파생 클래스가 템플릿으로부터 인스턴스화될 때 기본 클래스의 내용에 대해 모르는 것도 이러한 이유이다.