본문 바로가기

Effective C++/7. 템플릿과 일반화 프로그래밍

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

회사에서 메세지를 전송하는 프로그램을 만들고 있다고 가정해보자. 메세지는 암호회될 수도 있고, 비가공텍스트(비암호화) 형태가 수도 있다. 만약 어떤 메세지가 어떤 회사로 전송될지를 "컴파일 도중" 결정할 있는 충분한 정보가 있다면, 주저 없이 템플릿 기반의 방법을 있을 것이다.

 

 

 

여기에 덧붙여서 메세지를 보낼 때마다 관련 정보를 로그로 남기고 싶다. 이럴땐 파생 클래스를 사용하면 쉽게 기능을 붙일 있다.

 

파생 클래스를 자세히 보면, 메시지 전송 함수의 이름(sendClearMsg) 기본 클래스에 있는 (sendClear) 다르다는 것을 있다. 이러한 설계는 다음과 같은 장점을 가진다.

  • 기본 클래스로부터 물려받은 이름을 파생 클래스에서 가리는 문제(항목 33 참고)

 

하지만 코드는 컴파일되지 않는데, 이유는 다음과 같다.

  • sendClear() 함수가 존재하지 않습니다…

 

??? 우리 눈에는 기본 클래스의 sendClear 버젓이 보이는데도 컴파일러는 기본 클래스를 쳐다도 보지 않는다. 왜일까?

 

문제는 간단하다. 컴파일러가 LoggingMsgSender 클래스 템플릿의 정의와 마주칠 , 컴파일러는 대체 클래스가 어디서 파생된 것인지 모른다는 것이다. MsgSender<Company> 것은 분명한데, Company 템플릿 매개변수이고, 템플릿 매개변수는 나중(LoggingMsgSender 인스턴스로 만들어질 )까지 무엇이 없다.

 

Company 무엇인지 정확히 모르기 때문에, MsgSender<Company> 클래스가 어떤 형태인지 방법이 없다는 것이다. 이래서 sendClear() 클래스 내에 있는지 없는지 없는 것이다.

 


 

문제를 구체적으로 알고 싶으면, 가정을 하나 해보자. CompanyZ라는 클래스가 있고, 클래스는 암호화된 통신만 사용해야 한다.

 

 

조금 전에 보았던 MsgSender 템플릿을 그대로 CompanyZ 클래스에 쓰기엔 그렇다. 템플릿은 CompanyZ 객체의 설계 의도와 맞지 않는 sendClear() 함수를 제공하기 때문이다. 부분을 바로 잡을려면 CompanyZ 위한 MsgSender 특수화 버전을 만들 수 있다.

 

 

 

여기서 클래스 정의의 머리에 삐죽나와 있는 "template<>" 구문을 보자. 괄호 안에 아무것도 없는 template 뜻은 '이것은 템플릿도 아니고 클래스도 아니다' 라는 것을 의미한다.

 

정확히 말하면, 위의 코드는 MsgSender 템플릿을 탬플릿 매개변수가 CompanyZ 있도록 특수화한 버전이다. 특히 지금의 특수화는 완전 템플릿 특수화(total template specialization)라고 한다. MsgSender 템플릿이 CompanyZ 타입에 대해 특수화되었고, 이때 템플릿의 매개변수들이 하나도 빠짐없이(완전) 구체적인 타입으로 정해진 상태라는 뜻이다. , 일단 타입 매개변수가 CompanyZ 정의된 이상 템플릿(특수화버전) 매개변수로는 다른 것이 올 수 없게 된다는 뜻이다.

 

그리고 다시 MsgSender 파생 클래스인 LoggingMsgSender 돌아와보자.

 

 

주석문에도 나와 있듯이, 기본 클래스가 MsgSender<CompanyZ>라면 코드는 말이 안된다. MsgSender<CompanyZ>에는 sendClear() 없기 때문이다. 바로 이런 상황 때문에 위와 같은 함수 호출을 C++ 받아주지 않는 것이다. 기본 클래스 템플릿은 언제든지 특수화될 있고, 이런 특수화 버전에서 제공되는 인터페이스가 원래의 일반형 템플릿과 같으리란 법이 없다는 것이다.

 

이렇기 때문에, C++ 컴파일러는 템플릿으로 만들어진 기본 클래스를 뒤져서 상속된 이름을 찾는 것을 거부한다. 어떤 의미로 보면, C++ 하위 언어들 부분인 객체지향 C++에서 템플릿 C++(항목 1 참고) 옮겨 상속 매커니즘이 끊긴다는 것이다.

(일반 클래스에서의 상속 매커니즘과 템플릿 클래스에서의 상속 매커니즘이 다르다는 )


그럼 어떻게 해야 컴파일러가 템플릿화된 기본 클래스를 참고할까? 3가지 방법이 있다.

1. 기본 클래스 함수에 대한 호출문 앞에 "this->" 붙인다.

 

2. using 선언을 사용한다. 항목 33에서 가려진 기본 클래스의 이름을 파생 클래스의 유효범위에 끌어오는 용도로 using 선언을 이용하는 방법이 나왔었다. 이것을 활용하여 기본 클래스(템플릿화된) 유효 범위를 참고하라고 컴파일러에게 알려줄 있다.

 

3. 호출할 함수가 기본 클래스의 함수라는 것을 명시적으로 지정한다. (비추천)

 

방식을 비추천하는 이유는 호출되는 함수가 가상 함수인 경우에는, 이런식으로 명시적 한정을 해버리면 가상 함수 바인딩이 무시되기 때문이다.


이름에 대한 가시성을 조작한다는 면에서 보면 3가지 방법은 모두 동작 원리가 같다. 기본 클래스 템플릿이 이후에 어떻게 특수화되더라도 원래의 일반형 템플릿에서 제공하는 인터페이스를 그대로 제공할 것이라고 컴파일러에게 약속을 하는 것이다. 그래서 만약 약속을 어긴다면 다음과 같이 것이다.

 

 

당연히 컴파일되지 않는다. 지금 상황에서는 기본 클래스가 MsgSender<CompanyZ>(템플릿 특수화 버전)이라는 사실을 컴파일러가 알고 있고, sendClearMsg() 호출할려면 sendClear() MsgSender<CompanyZ> 없다는 사실도 컴파일러가 알아챈 후이기 때문이다.

 

본질적인 논점은, 기본 클래스의 맴버에 대한 참조가 무효한지 컴파일러가 진단하는 과정이

  1. 미리(파생 클래스 템플릿의 정의가 구문분석될 ) 들어가느냐
  2. 나중에(파생 클래스 템플릿이 특정한 템플릿 매개변수를 받아 인스턴스화 ) 들어가느냐

 

차이를 아는 것이 이번 항목의 핵심이다. 여기서는 C++ 이른바 '이른진단(ealry diagnose)' 선호하는 정책으로, 1 정책으로 결정한 것이다. 파생 클래스가 템플릿으로부터 인스턴스화될 컴파일러가 기본 클래스의 내용에 대해 아무것도 모르는 것을 가정하는 이유도 이제 이해할 있을 것이다.


  • 파생 클래스 템플릿에서 기본 클래스 템플릿의 이름을 참조할 때는 "this->" 접두사로 붙이거나 기본 클래스 한정문을 명시적으로 주는 것으로 해결하자.