KoreanFoodie's Study
Effective C++ | 항목 43 : 템플릿으로 만들어진 기본 클래스 안의 이름에 접근하는 방법을 알아 두자 본문
Effective C++ | 항목 43 : 템플릿으로 만들어진 기본 클래스 안의 이름에 접근하는 방법을 알아 두자
GoldGiver 2022. 10. 25. 16:31
C++ 프로그래머의 필독서이자 바이블인, 스콧 마이어스의 Modern Effective C++ 를 읽고 기억할 내용을 요약하고 있습니다. 꼭 읽어보시길 추천드립니다!
항목 43 : 템플릿으로 만들어진 기본 클래스 안의 이름에 접근하는 방법을 알아 두자
핵심 :
파생 클래스 템플릿에서 기본 클래스 템플릿의 이름을 참조할 때는, "this->" 를 접두사로 붙이거나 기본 클래스 한정문을 명시적으로 써 주는 것으로 해결하자
템플릿 클래스 상속하는 다음 예제 코드를 보자.
class CompanyA
{
public:
void sendCleartext(const std::string& msg);
void sendEncrypted(const std::string& msg);
};
// CompanyB, CompanyC 클래스들 선언
...
// 메시지 생성에 사용되는 정보를 담기 위한 클래스
class MsgInfo { ... };
template<typename Company>
class MsgSender
{
public:
void sendClear(const MsgInfo& info)
{
std::string msg;
// info 로부터 msg 를 생성
...
Company c;
c.sendCleartext(msg);
}
void sendSecret(const MsgInfo& info)
{
std::string msg;
// info 로부터 msg 를 생성
...
Company c;
c.sendEncrypted(msg);
}
};
이제 메시지를 보낼 때마다 로그를 남기는 코드를 파생 클래스를 이용해 만들어 보자.
template<typename Company>
class LoggingMsgSender: public MsgSender<Company>
{
public:
void sendClearMsg(const MsgInfo& info)
{
...
// 기본 클래스의 함수 호출
// 컴파일이 안됨!
sendClear(info);
...
}
};
MsgSender 에는 sendClear 함수가 있는데 왜 컴파일이 안될까?
문제는 간단하다. 컴파일러가 LoggingMsgSender 클래스 템플릿의 정의와 마주칠 떄, 컴파일러는 대체 이 클래스가 어디서 파생된 것인지를 모른다. MsgSender<Company> 에서, Company 는 템플릿 매개변수이고, Company 가 정확이 무엇인지 모르는 상황에서는 MsgSender<Company> 클래스가 어떤 형태인지(sendClear 함수가 있는지 없는지) 알 수가 없다.
좀 더 구체적인 예시를 위해, 암호화된 통신만을 사용하는 CompanyZ 클래스가 있다고 하자.
class CompanyZ
{
public:
void sendEncrypted(const std::string& msg);
};
// 완전 템플릿 특수화 (total template specialization)
template<>
class MsgSender<CompanyZ>
{
public:
...
void sendSecret(const MsgInfo& info) { ... }
};
만약 Company 에 CompanyZ 타입이 들어오면, LoggingMsgSender 에서는 sendClear(info) 함수가 존재할 수도 없다!
즉, 기본 클래스 템플릿은 언제라도 특수화될 수 있고, 이런 특수화 버전에서 제공하는 인터페이스가 원래의 일반형 템플릿과 같으리라는 없다. 그래서 C++ 컴파일러는 템플릿으로 만들어진 기본 클래스를 뒤져서 상속된 이름을 찾는 것을 거부한다.
C++ 의 "난 템플릿화된 기본 클래스는 멋대로 안 뒤질거야" 동작을 발현되지 않도록 하려면 다음과 같은 3가지 방법을 사용하면 된다.
첫째, 기본 클래스 함수에 대한 호출문 앞에 "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:
// 컴파일러에게 sendClear 함수가 기본 클래스에 있다고 가정하라고 알려준다
using MsgSender<Company>::sendClear;
void sendClearMsg(const MsgInfo& info)
{
...
sendClear(info);
...
}
};
셋째, 호출할 함수가 기본 클래스의 함수라는 점을 명시적으로 지정한다
template<typename Company>
class LoggingMsgSender: public MsgSender<Company>
{
public:
void sendClearMsg(const MsgInfo& info)
{
...
// sendClear 함수가 상속되는 것으로 가정
MsgSender<Company>::sendClear(info);
...
}
};
이 방식은 호출되는 함수가 가상 함수인 경우 가상 함수 바인딩이 무시될 수 있어 추천하지 않는다.
물론 다음과 같은 코드는 컴파일 되지 않는다.
LoggingMsgSender<CompanyZ> zMsgSender;
MsgInfo msgData;
...
// 에러! 컴파일 되지 않는다
zMsgSender.sendClearMsg(msgData);
이는 템플릿 특수화 버전을 명시했고, sendClear 함수는 MsgSender<CompanyZ> 클래스에 들어가 있지 않다는 것을 컴파일러가 알 수 있기 때문이다.
본질적인 논점은, 기본 클래스의 멤버에 대한 참조가 무효한지를 컴파일러가 진단하는 과정이 미리(파생 클래스 템플릿의 정의가 구문분석될 때) 들어간다는 것이다! (파생 클래스 템플릿이 특정한 템플릿 매개변수를 받아 인스턴스화될 때가 아님)
'Tutorials > C++ : Advanced' 카테고리의 다른 글
Effective C++ | 항목 45 : "호환되는 모든 타입"을 받아들이는 데는 멤버 함수 템플릿이 직방! (0) | 2022.10.25 |
---|---|
Effective C++ | 항목 44 : 매개변수에 독립적인 코드는 템플릿으로부터 분리시키자 (0) | 2022.10.25 |
Effective C++ | 항목 42 : typename 의 두 가지 의미를 제대로 파악하자 (0) | 2022.10.25 |
Effective C++ | 항목 41 : 템플릿 프로그래밍의 천릿길도 암시적 인터페이스와 컴파일 타임 다형성부터 (0) | 2022.10.25 |
Effective C++ | 항목 40 : 다중 상속은 심사숙고해서 사용하자 (0) | 2022.10.25 |