본문 바로가기
프로그래밍언어/C++

[Effective C++] 항목 41~43

by 목가 2018. 4. 10.
반응형

항목 41. 템플릿 프로그래밍의 천릿길도 암시적 인터페이스와 컴파일 타임 다형성부터


class Widget {
public:
widget();
virtual ~Widget();
virtual std::size_t size() const;
virtual void normalize();
void swap(Widget& other);
...
}

void doProcessing(Widget& w)
{
if(w.size() > 10 && w != someNastyWidget)
{
Widget temp(w);
temp.normalize();
temp.swap(w);
}
}

* w 는 Widget 타입으로 선언되었기 때문에, w 는 Widget 인터페이스를 지원해야한다. 
* Widget 이 선언된 헤더파일등에서 어떤 형태인지 알수 있으므로 명시적 인터페이스라고 한다.
* Widget 함수 중 몇개는 가상 함수이므로 가상 함수에 대한 호출은 런타임 다형성에 의해 이루어진다.

template<typename T>
void doProcessing(T& w)
{
if(w.size() > 10 && w != someNastyWidget)
{
T temp(w);
temp.normalize();
temp.swap(w);
}
}

* w가 지원해야 하는 인터페이스는 이 템플릿 안에서 w에 대해 실행되는 연산이 결정한다. 
* size, normalize, swap 멤버함수 지원, 복사 생성자도 지원해야 함(temp 생성)
* 부등 비교를 위한 오퍼레이터 연산도 지원
* 이 템플릿이 컴파일되려면 몇 개의 표현식이 '유효'해야 하는데 이 표현식들은 바로 T 가 지원해야하는 암시적 인퍼테이스이다.
* w가 수반되는 함수 호출이 일어날 때, 예를들어 operator > or != 가 호출될 때 템플릿의 인스턴스화가 일어난다. 이러한 인스턴스화가 일어나는 시점은 컴파일 도중이다. 인스턴스화를 진행하는 함수 템플릿에 어떤 템플릿 매개변수가 들어가느냐에 따라 호출되는 함수가 달라지기 때문에, 이것을 가리켜 컴파일 타임 다형성이라고 한다.

명시적 인터페이스는 대개 함수 시그니처로 이루어진다. 
class Widget {
public:
widget();
virtual ~Widget();
virtual std::size_t size() const;
virtual void normalize();
void swap(Widget& other);
...
}

암시적 인터페이스는 함수 시그니처에 기반하고 있지 않다. 암시적 인터페이스를 이루는 요소는 유효 표현식이다. 
template<typename T>
void doProcessing(T& w)
{
if(w.size() > 10 && w != someNastyWidget)
{
....
}

}


T에서 제공될 암시적 인터페이스에는 다음과 같은 제약이 걸린다.
* 정수 계열의 값을 반환하고 이름이 size인 함수를 지원해야 한다.
* T 타입의 객체 둘을 비교하는 operator != 함수를 지원해야 한다.
* 위 if 표현식에 대한 제약은 결과 값이 bool 과 호환되어야 한다는 점이다. 

◆ 어떤 템플릿 안에서 어떤 객체를 쓰려고 할 때 그 템플릿에서 요구하는 암시적 인터페이스를 그 객체가 지원하지 않으면 사용이 불가능하다. (컴파일이 되지 않는다.)

* 클래스 및 템플릿은 모두 인터페이스와 다형성을 지원
* 클래스의 경우, 인터페이스는 명시적이며 함수의 시그니처를 중심으로 구성됨. 다형성은 프로그램 실행 중에 가상함수를 통해 나타남
* 템플릿 매개변수의 경우, 인터페이스는 암시적이며 유효 표현식에 기반을 두어 구성됨. 다형성은 컴파일 중에 템플릿 인스턴스화와 함수 오버로딩 모호성 해결을 통해 나타남

항목 42. typename의 두 가지 의미를 제대로 파악하자.

template<class T> class Widget;
template<typename T> class Widget;
위 에서 두 템플릿 선언문의 차이는 전혀없다. 

template<typename C>
void print2nd(const C& container)  // 컨테이너에 들어 있는 두 번째 원소를 출력
{
if(container.size() >=2)
{
C::const_iterator iter(container.begin());   // 첫째 원소에 대한 반복자를 얻음
++iter;                                        // iter 를 두 번째 원소로 이동
int value = *iter;                            // 이 원소를 다른 int 로 복사하여 출력
std::cout << value;
}
}

C::const_iterator 는 템플릿 매개변수에 종속되었으므로 의존 이름이라고 함
의존이름이 어떤 클래스 안에 중첩되어 있는 경우 중첩 의존 이름이라고 함

코드 안에 중첩 의존 이름이 있으면 컴파일러가 구문분석을 할 때 에러가 발생함
template<typename C>
void print2nd(const C& container)
{
C::const_iterator *x;
...
}

C::const_iterator 의 포인터인 지역변수로서 x 를 선언한 것처럼 보임.
const_iterator 라는 이름을 가진 정적 데이터 멤버가 C 에 있다고 가정하면 C::const_iterator 와 x 를 피연산자로 곱셈연산한 것으로 해석됨
* 일반적으로 중첩 의존 이름은 타입이 아닌 것으로 해석된다.

template<typename C>
void print2nd(const C& container)
{
typename C::const_iterator *x;
...
}
* typename 키워드는 중첩 의존 이름만 식별하는 데 써야한다.
단, 예외가 하나 있는데 중첩 의존 타입 이름이 기본 클래스의 리스트에 있거나 멤버 초기화 리스트 내의 기본 클래스 식별자로서 있을 경우에는 typename을 붙여주면 안된다.

template<typename T>
class Derived: public Base<T>::Nested            // 상속되는 기본 클래스 리스트: typename 쓰면 안됨
{
public:
explicit Derived(int x): Base<T>::Nested(x) // 멤버 초기화 리스트에 있는 기본 클래스 식별자: typename 쓰면 안됨
{
typename Base<T>::Nested temp;  // 중첩 의존 타입 이름이며 뭣도 아니므로 typename 필요
...
}
}

template<typename IterT>
void workWithIterator(IterT iter)
{
typename std::iterator_traits<IterT>::value_type temp(*iter);
...
}

위 코드에서 typedef 를 사용해서 이름을 사용하기 쉽게 변경

template<typename IterT>
void workWithIterator(IterT iter)
{
<b>typedef typename </b>std::iterator_traits<IterT>::value_type value_type;
value_type temp(*iter);
...
}

* 템플릿 매개변수를 선언할 때, class 및 typename은 서로 바꾸어 써도 무방하다.
* 중첩 의존 타입 이름을 식별하는 용도에는 반드시 typename을 사용한다. 단, 중첩 의존 이름이 기본 클래스 리스트에 있거나 멤버 초기화 리스트 내의 기본 클래스 식별자로 있는 경우에는 예외

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

template<typename Company>
class LoggingMsgSender: public MsgSender<Company>
{
public:
...
void sendClearMsg(const MsgInfo& info)
{
"메시지 전송 전" 정보를 로그에 기록
sendClear(info);                            // 기본 클래스의 함수를 호출하는데 컴파일 되지 않는다.
"메시지 전송 후" 정보를 로그에 기록
}
}

* 컴파일러가 LoggingMsgSender 클래스 템플릿의 정의와 마주칠 때, 컴파일러는 대체 이 클래스가 어디서 파생된 것인지를 모름

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

위처럼 CompanyZ 클래스에 쓰기엔 MsgSender 템플릿은 맞지않다. 이 부분을 바로 잡기 위해 CompanyZ를 위한 MsgSender의 특수화 버전을 만들 수 있다. 
template<>
class MsgSender<CompanyZ>
{                                            // MsgSender 켐플릿의 완전 특수화 버전. senderClear 함수가 빠짐
public:
...
void sendSecret(const MsgInfo& info)
{...}
}

* MsgSender 템플릿이 CompanyZ 타입에 대해 특수화되었고, 이러한 특수화를 완전 템플릿 특수화라고 한다. 

CompanyZ 가 특수화되었다고 가정하고 LoggingMsgSender 클래스로 돌아가보자 
template<typename Company>
class LoggingMsgSender: public MsgSendoer<Company>
{
public:
...
void sendClearMsg(const MsgInfo & info)
{
"메시지 전송 전" 정보를 로그에 기록
sendClear(info);                            // Company == CompanyZ 라면 이 함수는 있을 수 조차 없다.
"메시지 전송 후" 정보를 로그에 기록
}
}

* 기본 클래스가 MsgSender<CompanyZ>이면 에러가 발생한다. 클래스에 sendClear 함수가 없기 때문이다.
* C++ 컴파일러는 템플릿으로 만들어진 기본 클래스를 뒤져서 상속된 이름을 찾는 것을 거부함

위 문제를 해결하기 위한 3가지 방법
1. 기본 클래스 함수에 대한 호출문 앞에 "this->"를 붙인다.
void sendClearMsg(const MsgInfo & info)
{
    "메시지 전송 전" 정보를 로그에 기록
    this->sendClear(info);                            
    "메시지 전송 후" 정보를 로그에 기록
}

2. using 선언을 사용
template<typename Company>
class LoggingMsgSender: public MsgSendoer<Company>
{
public:
using MsgSender<Company>::sendClear;  // 컴파일러에게 sendClear 함수가 기본 클래스에 있다고 가정하라고 지정
...
}

3. 호출할 함수가 기본 클래스의 함수라는 점을 명시적으로 지정 (호출되는 함수가 가상함수인 경우 명시적 한정을 해 버리면 가상 함수 바인딩이 무시되기 때문에 추천하지 않는 방법)
void sendClearMsg(const MsgInfo & info)
{
    "메시지 전송 전" 정보를 로그에 기록
    MsgSender<Company>::sendClear(info);         // sendClear 함수가 상속되는 것으로 가정
    "메시지 전송 후" 정보를 로그에 기록
}


반응형

'프로그래밍언어 > C++' 카테고리의 다른 글

[Effective C++] 항목 47~49  (0) 2018.04.10
[Effective C++] 항목 44~46  (0) 2018.04.10
[Effective C++] 항목 38~40  (0) 2018.04.10
[Effective C++] 항목 35~37  (0) 2018.04.10
[Effective C++] 항목 29~31  (0) 2018.04.10

댓글