7. 템플릿과 일반화 프로그래밍
41) 템플릿 프로그래밍의 천릿길도 암시적 인터페이스와 컴파일 타임 다형성부터
클래스와 템플릿은 모두 인터페이스와 다형성을 지원한다.
명시적 인터페이스 : 소스 코드에 명시적으로 드러나는 인터페이스
런타임 다형성 : 실제 호출이 동적 타입 기반으로 런타임에 결정되는 경우
암시적 인터페이스 : 제대로 컴파일 되기 위해 필요한 ‘유효한’ 표현식
컴파일 타임 다형성 : 컴파일 도중 인스턴스화가 진행되는 함수 템플릿에 어떤 템플릿 매개 변수가 들어가느냐에 따라 호출되는 함수가 달라지는 경우
클래스의 경우 인터페이스는 명시적이고 함수의 시그니처를 중심으로 구성되어 있다.
다형성은 런타임에 가상함수를 통해 나타난다.
템플릿 매개변수의 경우 인터페이스는 암시적이고 유효 표현식을 기반으로 구성된다.
다형성은 컴파일 타임에 템플릿 인스턴스화와 함수 오버로딩 모호성 해결을 통해 나타난다.
42) typename의 두가지 의미
아래 두 문장은 같은 뜻을 가진다.
template<class T> class Widget;
template<typename T> class Widget;
템플릿 매개변수를 선언할 때에는 class 로 쓰든 typename으로 쓰든 차이가 없다.
단 반드시 typename 키워드를 앞에 붙여줘야 하는 경우가 있다.
의존 이름 : 템플릿 매개변수에 종속된 이름
비의존 이름 : 템플릿 매개변수와 관계없는 타입 ( ex: int )
중첩 의존 타입 이름 : 클래스 안에 중첩된 의존 이름. 템플릿 매개변수에 종속된 이름.
을 의미하는데, 이 중첩 의존 이름이 혼란을 불러올 때가 있다.
template<typename C>
void print(const C& container)
{
C::const_iterator * x;
}
언뜻 보면 C::const_iterator 타입의 포인터를 선언하고 있는것처럼 보이지만, 이 C::const_iterator가 타입이 아니라면 그냥 곱셈 연산으로도 해석될 수 있다.
컴파일러는 기본적으로 중첩 의존 이름을 만났을 때 그 이름이 타입이 아니라고 가정하게끔 설계되어 있다.
따라서 다음과 같이 typename 키워드를 붙여 준다.
template<typename C>
void print(const C& container)
{
if(container.size() >= 2)
{
typename C::const_iterator iter(container.begin());
}
}
여기서 예외가 있는데, 이 중첩 의존 타입 이름이 기본 클래스의 리스트에 있거나 멤버 초기화 리스트 내의 기본 클래스 식별자로서 있을 경우에는 typename 키워드를 붙여줘서는 안된다.
template<typename T>
class Derived : public Base<T>::Nested{ // 상속되는 기본 클래스 리스트라 typename X
public:
explicit Derived(int x) : Base<T>::Nested(x) // 멤버 초기화 리스트에 있어서 typename X
{
typename Base<T>::Nested temp; // 중첩 의존 타입 이름이며 두 경우 모두 아니므로 typename O
}
};
이외에 현업에서 반복자에 관련된 다음과 같은 코드를 흔히 보게될 수 있으니 눈에 익혀 두자.
template<typename T>
void WorkWithIterator(IterT iter)
{
typedef typename std::iterator_traits<IterT>::value_type value_type;
value_type tmp(*iter);
...
}
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;
Company c;
c.sendCleartext(msg);
}
void sendSecret(const MsgInfo& info) { ... }
};
// 아래 코드를 추가하면 컴파일되지 않는다
<typename Company>
class LoggingMsgSender : public MsgSender<Company>{
public:
void sendClearMsg(const MsgInfo& info)
{
sendClear(info);
}
...
};
분명히 기본 클래스인 MsgSender에 sendClear 함수가 있음에도 불구하고 템플릿 매개변수인 Company가 무엇인지 모르는 상황에서는 MsgSender<Company>가 어떤 클래스인지 알 수가 없기 때문에 컴파일이 되지 않는다.
아래와 같이 특수화를 통해 sendClear 함수를 가지고 있지 않은 MsgSender<Company>가 존재할 수 있기 때문이다.
template<>
class MsgSender<CompanyZ>{
public:
void sendSecret(const MsgInfo& info) { ... }
};
해결 방법은 세가지가 있다.
- 기본 클래스 함수를 호출할 때 this-> 를 붙인다
- using 선언을 사용해 기본 클래스의 함수임을 알린다 using BaseClass<>::Func();
- 명시적으로 기본 클래스의 함수임을 지정한다 BaseClass<>::Func();
44) 매개변수에 독립적인 코드는 템플릿으로부터 분리시키자
아무생각 없이 템플릿을 남발하게 되면 코드 비대화가 일어난다.
템플릿이 여러번 인스턴스화될 때 일어날 코드 중복을 알아채고 공통적인 부분을 분리해서 템플릿으로부터 빼내야한다.
비타입 템플릿 매개변수로 생기는 코드 비대화는 템플릿 매개변수를 함수 매개변수 혹은 클래스 데이터 멤버로 대체함으로써 비대화를 종종 없앨 수 있습니다. 해당 데이터를 함수 매개변수로 전달하고 해당 데이터의 포인터를 가지는 기본 클래스를 만들어 파생 클래스가 함수를 호출하도록 합니다.
타입 매개변수로 생기는 코드 비대화의 경우, 동일한 이진 표현구조를 가지고 인스턴스화되는 타입들이 한가지 함수 구현을 공유하게 만듦으로써 비대화를 감소시킬 수 있습니다.
45) 호환되는 모든 타입을 받아들이는 데는 멤버함수 템플릿이 직방
같은 템플릿 클래스들은 눈으로 보기에 비슷하지만 컴파일러에게는 완전히 다른 클래스들이다.
이러한 클래스들 사이에 어떠한 변환을 하고싶다면 그 변환에 대한 생성자를 만들어줘야 할 것이다.
이때 템플릿을 활용할 수 있다.
template<typename T>
class SmartPtr{
public:
template<typename U>
SmartPtr(const SmartPtr<U>& other);
};
이렇게 하면 일반화된 복사 생성자를 만드는 데 활용할 수 있다.
위 코드를 말로 풀어낸다면 모든 T 타입 및 모든 U 타입에 대해서 SmartPtr<T>객체가 SmartPtr<U>로부터 생성될 수 있다는 뜻이 된다.
그러나 이렇게 선언하면 가능한 타입 변환이 너무 많아지기 때문에 이것을 제한해야 한다.
또한 보통의 복사 생성자와 복사 대입 연산자는 여전히 직접 선언해야 한다.
'C++ > Effective C++' 카테고리의 다른 글
Effective C++ (51) ~ (55) (0) | 2021.08.29 |
---|---|
Effective C++ (46) ~ (50) (0) | 2021.08.28 |
Effective C++ (36) ~ (40) (0) | 2021.08.25 |
Effective C++ (31) ~ (35) (0) | 2021.08.24 |
Effective C++ (26) ~ (30) (0) | 2021.08.22 |
댓글