KoreanFoodie's Study

Effective C++ | 항목 43 : 템플릿으로 만들어진 기본 클래스 안의 이름에 접근하는 방법을 알아 두자 본문

Tutorials/C++ : Advanced

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> 클래스에 들어가 있지 않다는 것을 컴파일러가 알 수 있기 때문이다.
본질적인 논점은, 기본 클래스의 멤버에 대한 참조가 무효한지를 컴파일러가 진단하는 과정이 미리(파생 클래스 템플릿의 정의가 구문분석될 때) 들어간다는 것이다! (파생 클래스 템플릿이 특정한 템플릿 매개변수를 받아 인스턴스화될 때가 아님)

Comments