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

업데이트:     Updated:

카테고리:

태그:

이 글은 아래의 책을 자세히 정리한 후, 정리한 글을 GPT에게 요약을 요청하여 작성되었습니다.
이펙티브 C++ 제3판, 스콧 마이어스 저자, 곽용재 번역

📦 7. 템플릿과 일반화 프로그래밍

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

✅ 문제 상황: 컴파일러가 기본 클래스를 탐색하지 못함

다른 회사들에 비암호화/암호화 메시지를 전송할 수 있도록 만든 프로그램 예시:

class CompanyA {
public:
	...
	void sendCleartext(const string& msg);
	void sendEncrypted(const string& msg);
	...
};

class CompanyB {
public:
	...
	void sendCleartext(const string& msg);
	void sendEncrypted(const string& msg);
	...
};

... // 다른 회사를 나타내는 클래스들

// 메시지 생성에 사용되는 정보를 담기 위한 클래스
class MsgInfo { ... };

template<typename Company>
class MsgSender {
public:
	... // 생성자, 소멸자 등등
	// 기본 클래스의 함수 이름과 다르게 하여,
	// 비가상 함수 재정의 문제를 피함
	void sendClear(const MsgInfo& info) {
		string msg;

		Company c;
		c.sendCleartext(msg);
	}

	// c.sendEncrypted를 호출하는 함수
	void sendSecret(const MsgInfo& info) { ... }
};

문제가 있는 파생 클래스

template<typename Company>
class LoggingMsgSender: public MsgSender<Company> {
public:
	... // 생성자, 소멸자 등등
	void sendClearMsg(const MsgInfo& info) {
		"메시지 전송 전" 정보를 로그에 기록;

		sendClear(info); // ❌ 컴파일 에러!

		"메시지 전송 후" 정보를 로그에 기록;
	}
};

문제:

  • 이 코드는 실행되지 않는다
  • 컴파일러는 MsgSender<Company>에서 Company가 무엇인지 정확히 알 수 없다

📌 왜 이런 문제가 발생하는가?

시나리오: 암호화 통신만 사용하는 CompanyZ

class CompanyZ {
public:
	...
	void sendEncrypted(const string& msg);
	...
};

// 기존 MsgSender 클래스
template<typename Company>
class MsgSender {
public:
	...
	void sendClear(const MsgInfo& info) { ... }
	void sendSecret(const MsgInfo& info) { ... }
};

// 완전 템플릿 특수화
// MsgSender의 템플릿 매개변수가 CompanyZ일 때의 버전이다.
template<>
class MsgSender<CompanyZ> {
public:
	...
	void sendSecret(const MsgInfo& info) { ... }
	// sendClear 함수가 없음!
};

완전 템플릿 특수화:

template<> class MsgSender { ... }

  • 템플릿 매개변수가 CompanyZ일 때 사용되는 클래스이다
  • CompanyZ는 암호화된 통신만을 사용한다고 가정
  • MsgSender 템플릿은 sendClear 함수를 가지고 있으므로, 특수화 버전에서는 제거

⚠️ 컴파일러의 딜레마

template<typename Company>
class LoggingMsgSender: public MsgSender<Company> {
public:
	...
	void sendClearMsg(const MsgInfo& info) {
		"메시지 전송 전" 정보를 로그에 기록;

		// Company == CompanyZ라면, 이 함수는 존재하지 않는다!
		sendClear(info);

		"메시지 전송 후" 정보를 로그에 기록;
	}
};

컴파일러의 고민:

“지금은 Company가 뭔지 모르니까,
MsgSender<Company> 안에 sendClear가 있는지도 아직 몰라.
sendClear(info)가 뭔지 모르니까 컴파일 에러를 내자.”

핵심 원리:

  • Company == CompanyZ라면, sendClear 함수가 존재하지 않는다
  • 이러한 경우가 있기에 코드가 실행되지 않는다
  • C++ 컴파일러는 기본 클래스 범위를 “미리” 탐색하지 않는다
  • 이 동작이 발현되지 않도록, 나중에 찾도록 해야 한다

✅ 해결 방법 1: this-> 접두사 사용

template<typename Company>
class LoggingMsgSender: public MsgSender<Company> {
public:
	...
	void sendClearMsg(const MsgInfo& info) {
		"메시지 전송 전" 정보를 로그에 기록;

		// sendClear가 상속되는 것으로 가정한다.
		this->sendClear(info);

		"메시지 전송 후" 정보를 로그에 기록;
	}
};

동작 원리:

  • this->를 붙이면 컴파일러에게 의존 이름임을 알림
  • 나중에(인스턴스화 시) 기본 클래스에서 찾도록 함

✅ 해결 방법 2: using 선언 사용

template<typename Company>
class LoggingMsgSender: public MsgSender<Company> {
public:
	// 컴파일러에게 sendClear 함수가
	// 기본 클래스에 있다고 가정하라고 알려준다.
	using MsgSender<Company>::sendClear;
	...
	void sendClearMsg(const MsgInfo& info) {
		"메시지 전송 전" 정보를 로그에 기록;

		// sendClear가 상속되는 것으로 가정
		sendClear(info);

		"메시지 전송 후" 정보를 로그에 기록;
	}
};

동작 원리:

  • using 선언으로 sendClear를 현재 클래스 범위로 가져옴
  • 컴파일러에게 기본 클래스에 있다고 가정하도록 지시

✅ 해결 방법 3: 명시적 한정 (권장하지 않음)

template<typename Company>
class LoggingMsgSender: public MsgSender<Company> {
public:
	...
	void sendClearMsg(const MsgInfo& info) {
		"메시지 전송 전" 정보를 로그에 기록;

		// sendClear 함수가 상속되는 것으로 가정
		MsgSender<Company>::sendClear(info);

		"메시지 전송 후" 정보를 로그에 기록;
	}
};

주의점:

이 방법은 권장하지 않는다.

이유:

  • 호출되는 함수가 가상 함수일 때 명시적 한정을 하면
  • 가상 함수 바인딩이 무시된다

📊 세 가지 해결 방법 비교

방법 장점 단점 권장도
this-> 간단하고 직관적 모든 호출에 붙여야 함 ⭐⭐⭐
using 선언 한 번만 선언하면 됨 클래스 범위 오염 가능 ⭐⭐⭐
명시적 한정 명확함 가상 함수 바인딩 무시

💡 세 가지 방법의 공통 원리

위의 해결 방법들은 동작 원리가 같다:

기본 클래스 템플릿이 특수화되더라도,
원래의 일반형 템플릿에서 제공하는 인터페이스를
그대로 제공할 것이라고 컴파일러에게 약속을 하는 것

컴파일러의 새로운 판단:

“sendClear는 템플릿 인자(Company)에 의존하는 이름이구나.
그럼 지금은 판단 보류.
인스턴스화될 때 실제 타입 기준으로 찾아보자.”


🚨 런타임 에러 발생 시점

... // 기존 코드

template<typename Company>
class LoggingMsgSender: public MsgSender<Company> {
public:
	...
	void sendClearMsg(const MsgInfo& info) {
		...
		// sendClear 함수가 상속되는 것으로 가정
		this->sendClear(info);
		...
	}
};

int main() {
	LoggingMsgSender<CompanyZ> zMsgSender;
	MsgInfo msgData;
	...
	// ❌ 에러! 컴파일되지 않음.
	zMsgSender.sendClearMsg(msgData);
}

실제 에러 발생:

  • 컴파일러는 기본(상속된) 클래스가 MsgSender<CompanyZ>템플릿 특수화 버전이라는 것을 알고 있다
  • sendClearMsg 함수 안의 sendClear 함수는 **MsgSender 클래스에 없다**는 것을 알게 된다
  • 인스턴스화 시점에 에러 발생

📌 핵심 개념: 이름 탐색 시점

일반 상속:

  • 컴파일러가 기본 클래스의 이름을 즉시 탐색

템플릿 상속:

  • 컴파일러가 기본 클래스의 이름을 나중에 탐색 (인스턴스화 시)

이유:

  • 템플릿 특수화로 인해 기본 클래스의 인터페이스가 달라질 수 있기 때문

🧐 정리

  1. 파생 클래스 템플릿에서 기본 클래스 템플릿의 이름을 참조할 때:
    • this->를 접두사로 사용
    • using 선언으로 기본 클래스 멤버를 현재 범위로 가져오기
    • 기본 클래스 한정문을 명시 (가상 함수에는 권장하지 않음)
  2. 기본 클래스의 멤버에 대한 참조가 무효한지를 컴파일러가 검사하는 시점이 핵심이다:
    • 미리: 파생 클래스 템플릿의 정의가 구문 분석할 때
    • 나중: 파생 클래스 템플릿이 특정한 템플릿 매개변수를 받아 인스턴스화될 때
  3. 템플릿 특수화로 인해 기본 클래스의 인터페이스가 달라질 수 있다.
    컴파일러는 이를 고려하여 이름 탐색을 지연시킨다.
  4. 가장 안전한 방법은 this-> 또는 using 선언을 사용하는 것이다.
    명시적 한정은 가상 함수 바인딩을 무시하므로 주의해야 한다.

Cpp 카테고리 내 다른 글 보러가기

댓글남기기