이펙티브 C++
https://www.yes24.com/Product/Goods/17525589
페이지 306p ~ 312p
이펙티브 C++(306p ~ 312p)
요약
항목 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;
// .. info로부터 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); // 기본 클래스의 함수를 호출. 컴파일되지 않음!
// 메시지 전송 후 정보를 로그에 기록
}
}
- 파생 클래스의 함수 이름을
sendClearMsg
로 하여 기본 클래스로부터 물려받은 이름을 파생 클래스에서 가리는 문제를 해결하였고, 상속받은 비가상 함수 재정의 문제가 없어 보이는 코드이지만, 컴파일되지 않는 코드이다. - 이유는
sendClear
함수가 존재하지 않는 것이다. - 컴파일러가
LoggingMsgSender
클래스 템플릿의 정의와 마주칠 때, 컴파일러는 이 클래스가 어디서 파생된 것인지 모른다.MsgSender<Company>
인 것은 분명하지만, Comapny는 템플릿 매개변수이고, 이 템플릿 매개변수는LoggingMsgSender
가 인스턴스화될 때까지 무엇이 될지 알 수 없다.- 따라서 Company가 정확히 무엇인지 모르는 상황에서 기본 클래스인
MsgSender<Company>
클래스가 어떤 형태인지 알 수 있는 방법이 없다. 그래서sendClear
함수가 들어 있는지 없는지 알아낼 방법이 없는 것이다.
- 구체적으로 살펴보기 위해 새로운 회사를 추가한다. 이 회사는 암호화된 통신만을 사용한다.
// sendCleartext를 제공하지 않는 암호화된 통신만 사용하는 클래스
class CompanyZ {
public:
void sendEncrypted(const std::string& msg);
}
- 이 상황에서는 MsgSender 템플릿은 CompanyZ 클래스에 바로 사용할 수는 없다.
sendClear
에서는 암호화되지 않은 평서문을 보내기 때문이다. - 이 부분을 바로잡기 위해 CompanyZ를 위한 MsgSender의 특수화 버전을 만들 수 있다.
// MsgSender 템플릿의 완전 특수화 버전
// sendClear 함수가 빠진 것만 제외하면 일반형 템플릿과 동일
template<>
class MsgSender<CompanyZ>
{
public:
void sendSecret(const MsgInfo& info)
{
// ..
}
}
template<>
구문처럼 괄호 안에 아무것도 없는 template은템플릿도 아니고 클래스도 아니다
라는 의미를 가진다.- 정확히 말하면, 위의 코드는 MsgSender 템플릿을 템플릿 매개변수가 아니라
CompanyZ
일 때 쓸 수 있도록 특수화한 버전이다. - 이 경우 완전 템플릿 특수화라고 부른다. MsgSender 템플릿이 CompanyZ 타입에 대해 특수화되었고, 이때 이 템플릿의 매개변수들이 하나도 빠짐없이 구체적인 타입으로 정해진 상태라는 뜻이다.
- 따라서 타입 매개변수가 CompanyZ로 정의된 이상 이 템플릿(특수화된)의 매개변수로는 다른 것이 올 수 없게 된다.
- 정확히 말하면, 위의 코드는 MsgSender 템플릿을 템플릿 매개변수가 아니라
- 이제 다시 파생 클래스인
LoggingMsgSender
를 보자.
template<typename Company>
class LoggingMsgSender: public MsgSender<Company>
{
public:
// .. 생성자, 소멸자 등
void sendClearMsg(const MsgInfo& info)
{
// 메시지 전송 전 정보를 로그에 기록
sendClear(info); // Compnay가 CompanyZ라면 sendClear 함수는 있지 않다.
// 메시지 전송 후 정보를 로그에 기록
}
}
- 주석처럼 기본 클래스가
MsgSender<CompanyZ>
라면 이 코드는 말이 되지 않는다.- 특수화된
MsgSender<CompanyZ>
클래스에는sendClear
라는 함수가 없기 때문이다.
- 특수화된
- 이런 일이 생길 수 있기 때문에 위와 같은 함수 호출을 C++에서 받아주지 않는다.
- 기본 클래스 템플릿은 언제라도 특수화될 수 있고, 이런 특수화 버전에서 제공하는 인터페이스가 원래의 일반형 템플릿과 꼭 같으리란 법은 없다.
- 문제 해결을 위해선 C++이 템플릿화된 기본 클래스를 확인하지 않는 동작을 피해야 한다.
방법1: 기본 클래스 함수에 대한 호출문 앞에 this->
붙이기
template<typename Company>
class LoggingMsgSender: public MsgSender<Company>
{
public:
// .. 생성자, 소멸자 등
void sendClearMsg(const MsgInfo& info)
{
// 메시지 전송 전 정보를 로그에 기록
// sendClear가 상속되는 것으로 가정
this->sendClear(info);
// 메시지 전송 후 정보를 로그에 기록
}
}
방법2: using 선언을 사용
- using을 사용하면 파생 클래스에 의해 가려진 기본 클래스의 이름을 파생 클래스의 유효범위로 끌어올 수 있다.
- 이 경우에는 유효범위로 끌어온 것은 아니고 정확히는 템플릿화된 기본 클래스의 유효범위를 뒤지라고 컴파일러에게 알려주는 것이다.
template<typename Company>
class LoggingMsgSender: public MsgSender<Company>
{
public:
// 컴파일러에게 sendClear 함수가 기본 클래스에 있다고 가정하라고 알려줌
using MsgSender<Company>::sendClear;
// .. 생성자, 소멸자 등
void sendClearMsg(const MsgInfo& info)
{
// 메시지 전송 전 정보를 로그에 기록
// sendClear가 상속되는 것으로 가정
sendClear(info);
// 메시지 전송 후 정보를 로그에 기록
}
}
방법3: 호출할 함수가 기본 클래스의 함수라는 점을 명시적으로 지정
- 이 방법을 사용하면 기본 템플릿으로 만들어진 기본 클래스의 함수에 접근할 수 있지만, 호출되는 함수가 가상 함수인 경우에는 가상 함수 바인딩이 무시된다는 단점이 있다.
template<typename Company>
class LoggingMsgSender: public MsgSender<Company>
{
public:
// .. 생성자, 소멸자 등
void sendClearMsg(const MsgInfo& info)
{
// 메시지 전송 전 정보를 로그에 기록
// sendClear 함수가 상속되는 것으로 가
MsgSender<Company>::sendClear(info);
// 메시지 전송 후 정보를 로그에 기록
}
}
- 방법1, 2, 3은 이름에 대한 가시성을 조작한다는 측면에서 모두 동작 원리가 같다.
- 핵심은 C++의 컴파일러에게 기본 클래스 템플릿이 이후에 어떻게 특수화되더라도 원래의 일반형 템플릿에서 제공하는 인터페이스를 그대로 제공할 것이라고 약속을 하는 것이다.
- 이런 약속은 LoggingMsgSender 등의 파생 클래스 템플릿을 컴파일러가 구문분석하는 데 반드시 필요하지만, 그 약속이 거짓이었다는 것이 들통나면 이후의 컴파일 과정에서 응징이 들어간다.
LoggingMsgSender<CompanyZ> zMsgSender;
MsgInfo msgData;
// 컴파일 에러 발생
zMsgSender.sendClearMsg(msgData);
- 위의 코드에서는 기본 클래스가 CompanyZ에 대한 특수화된 템플릿이라는 사실을 컴파일러가 알고 있고,
sendClearMsg
함수가 호출하는sendClear
함수는MsgSender<CompanyZ>
에는 없다는 사실도 컴파일러가 알고 있다. - 본질적으로는 기본 클래스의 멤버에 대한 참조가 무효한지를 컴파일러가 진단하는 과정이 미리(파생 클래스 템플릿의 정의가 구문분석될 때) 들어가느냐, 아니면 나중에(파생 클래스 템플릿이 특정한 템플릿 매개변수를 받아 인스턴스화될 때) 들어가느냐의 차이이다.
- 여기에서 C++의 컴파일러가 이른 진단을 통해 파생 클래스가 템플릿으로부터 인스턴스화될 때 컴파일러가 기본 클래스의 내용에 대해 아무것도 모르는 것으로 가정하는 이유를 찾을 수 있다.
메모
항목 43: 템플릿으로 만들어진 기본 클래스 안의 이름에 접근하는 방법을 알아 두자
- 파생 클래스 템플릿에서 기본 클래스 템플릿의 이름을 참조할 때는
this->
를 접두사로 붙이거나 기본 클래스 한정문을 명시적으로 써 주는 것으로 해결할 수 있다.
'책 > 이펙티브 C++' 카테고리의 다른 글
이펙티브 C++(312p ~ 320p) (1) | 2024.02.08 |
---|---|
이펙티브 C++(320p ~ 331p) (1) | 2024.02.08 |
이펙티브 C++(295p ~ 306p) (0) | 2024.02.05 |
이펙티브 C++(279p ~ 290p) (1) | 2024.02.03 |
이펙티브 C++(270p ~ 279p) (0) | 2024.02.02 |