Item 41 : Understand implicit interfaces and compile-time polymorphism
템플릿 프로그래밍의 천릿길도 암시적 인터페이스와 컴파일 타입 다형성부터.
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);
}
}
클래스의 경우, 코드를 통해 interface를 확인할 수 있다.
즉, 클래스의 인터페이스가 소스코드에 명시되어 있으므로, explicit interface(명시적 인터페이스)라고 할 수 있다.
위 코드의 Widget 클래스의 멤버 함수들은 virtual이기 때문에, 실행 도중에 어떤 것이 호출될지 결정된다.
따라서, runtime polymorphism(런타임 다형성)을 가진다.
템플릿은 클래스와 근본적으로 다르다.
template<typename T>
void doProcessing(T& w){
if(w.size() > 10 && w != someNastyWidget){
T temp(w);
temp.normalize();
temp.swap(w);
}
}
위 코드에서 w가 지원하는 인터페이스는 템플릿에서 w가 수행하는 operation에 의해 결정된다.
w의 type(T)은 size, normalize, swap과 같은 멤버 함수들을 지원해야 한다.
템플릿이 컴파일되기 위해서 expression이 유효해야 하며 이것을 T가 지원해야 하는 implicit interface(암시적 인터페이스)라고 한다.
operator>와 operator!=는 instantiating template에 포함되어야 한다.
인스턴스화는 컴파일 시간에 발생한다.
왜냐하면 인스턴스화된 function template는 parameter 종류에 따라 다른 함수가 호출되기 때문이다.
이것을 compile-time polymorphism이라고 한다.
implicit interface에 대해 다시 살펴보자.
if( w.size() > 10 && w != someNastyWidget)
위 코드에서 size 함수는 integral type을 리턴할 필요가 없다.
만약, operator>가 임의의 X 타입 객체와 int와 함께 호출된다면, size는 X타입을 반환해주면 된다.
이때 암시적인 변환이 가능해야 한다.
정리하면, 클래스와 템플릿은 interface와 polymorphism을 공유한다.
클래스에서는, 인터페이스는 explicit이며 함수 signature에 집중하고, polymorphism은 virtual function을 통해 runtime에 발생한다.
template paramter에서는, 인터페이스는 implicit이며, valid expression을 기반으로 하고, polymorphism은 템플릿 인스턴스화와 함수 오버로딩 과정, 즉 compile time에 발생한다.
Item 42 : Understand the two meanings of typename
typename의 두 가지 의미를 제대로 파악하자.
template<Class T> class Widget;
template<typename T> class Widget;
template type parameter를 선언할 때, class와 typename은 정확히 같은 것을 의미한다.
몇몇 프로그래머들은 class을 사용하기 쉽기 때문에 모든 때에 선호한다.
typename은 매개변수가 클래스 유형일 필요가 없음을 시사하기 때문에 사용한다.
그러나, C++는 항상 class와 typename을 같게 보는것은 아니다.
때때로, typename을 사용해야만 한다.
template<typename C>
void print2nd(const C& container)
{
if(container.size() >= 2)
{
C::const_iterator iter(container.begin());
++iter;
int value = *iter;
std::cout << value;
}
}
위 코드에서 iter의 타입은 C::const_iterator이며, template parameter C에 의존한다.
C::const_iterator와 같이 template 매개변수에 종속된 템플릿의 이름을 dependent names(종속 이름)이라고 한다.
dependent name이 class안에 중첩되어 있는 경우, nested dependent name이라고 한다.
C::const_iterator는 nested dependent name(중첩 의존 이름)이다.
또 다른 지역 변수인 print2nd와 value는 template parameter에 의존적이지 않기 때문에, non-dependent name이라고 한다.
nested dependent name이 에러가 발생하는 경우를 살펴보자.
template<typename C>
void print2nd(const C& container)
{
C::const_iterator* x;
...
}
위 코드에서 에러가 발생할 수 있다.
C::const_iterator pointer 지역 변수로 x를 선언했다.
만약 C::const_iterator가 타입이 아니라면 무슨일이 발생하는가?
만약 C가 const_iterator라는 이름의 static data member를 가지거나, x가 global variable의 이름인 경우, 위의 코드는 local variable을 선언하지 않고, C::const_iterator와 x의 multiplication을 실행한다.
이 경우, C::const_iterator 앞에 typename이라는 키워드를 명시해야 한다.
일반적으로, template안의 nested dependent type name을 가리킬 때, 이름 앞에 typename을 명시해야 한다.
이는 다른 이름들에 대해서는 할 필요가 없다.
template<typename C>
void f(const C& container, typename C::iterator iter);
typename이 nested dependent type name앞에 명시되어야 한다는 규칙은 예외가 있다.
base class의 list또는 멤버 초기화 목록에서 base class의 식별자일 때는 typename을 명시하면 안된다.
정리하면, template 파라미터를 선언할 때, class와 typename은 교환가능하다.
Nested dependent type name을 식별하기 위해 반드시 typename을 사용해야 한다. 단, Nested dependent type name이 base class의 리스트, 멤버 초기화 리스트내에 기본 클래스 식별자로 있을 경우는 예외이다.
Item 43 : Know how to access names in templatized base classes
템플릿으로 만들어진 기본 클래스 안의 이름에 접근하는 방법을 알아두자.
template<typename Company>
class MsgSender{
public:
...
void sendClear(const MsgInfo& info)
{
std::string msg;
create msg from info;
Company c;
c.sendCleartext(msg);
}
void sendSecret(const MsgInfo& info){ ... }
};
template<typename Company>
class LoggingMsgSender: public MsgSender<Company>{
public:
...
void sendClearMsg(const MsgInfo& info)
{
write "before sending" info to the log;
sendClear(info);
write "after sending" info to the log;
}
...
};
위 코드는 컴파일되지 않는다.
컴파일러는 sendClear라는 함수가 존재하지 않는다고 불평할 것이다.
sendClear는 base class에 있지만, 컴파일러는 그곳을 볼 수 없다.
컴파일러는 LoggingMsgSender 클래스 템플릿을 정의할 때, 무엇으로부터 상속되었는지 알 수 없다.
Company는 template parameter이고 LoggingMsgSender가 인스턴스화 될 때까지 Company에 대해 알 수 없다.
Company의 값을 모르면, MsgSender<Company>가 어떻게 생겼는지 알 길이 없다.
따라서 sendClear라는 함수를 사용할 수 없다.
이를 해결하기 위해 MsgSender의 specialized version을 만들 수 있다.
template<>
class MsgSender<CompanyZ>{
public:
...
void sendSecret(const MsgInfo& info){ ... }
};
template<>은 이것이 template도 standalone class도 아님을 뜻한다.
template argument가 CompanyZ일때 사용되는 MsgSender template의 specialized version이다.
이것을 total template specialization이라고 부른다.
위와 같은 total template specialization을 상속받는 템플릿을 만들면 C++는 base class에 있는 함수 호출을 거부한다.
MsgSender<CompanyZ>가 sendClear함수를 제공하지 않기 때문이다.
C++는 base class template이 특수화될 수 있으며, 이러한 특수화가 일반 템플릿과 동일한 인터페이스를 제공하지 않을 수 있음을 인식한다.
결과적으로 일반적으로 templatized base class 에서 상속된 이름을 찾는 것을 거부한다.
이를 해결하기 위해서는 C++가 templatized base class를 보지 않는 행동을 하지 않도록 만들어야 한다.
여기서는 3가지 방법을 제시한다.
1. base class function에 대한 호출 앞에 "this->"를 명시한다.
template<typename Company>
class LoggingMsgSender: public MsgSender<Company>{
public:
...
void sendClearMsg(const MsgInfo& info)
{
write "before sending" info to the log;
this->sendClear(info);
write "after sending" info to the log;
}
...
};
2. using declaration을 사용한다.
using declaration은 숨겨진 base class의 name을 derived class의 scope로 가져온다.
template<typename Company>
class LoggingMsgSender: public MsgSender<Company>{
public:
using MsgSender<Company>::sendClear;
...
void sendClearMsg(const MsgInfo& info)
{
write "before sending" info to the log;
sendClear(info);
write "after sending" info to the log;
}
...
};
3. base class에서 호출할 함수를 명시적으로 지정한다.
이것은 추천하지 않는 방법이다.
만약 호출되는 함수가 가상함수인 경우, 명시적 한정을 하면 가상 함수 바인딩이 무시된다.
template<typename Company>
class LoggingMsgSender: public MsgSender<Company>{
public:
...
void sendClearMsg(const MsgInfo& info)
{
write "before sending" info to the log;
MsgSender<Company>::sendClear(info);
write "after sending" info to the log;
}
...
};
name visibility의 관점에서 위의 방법들은 같다.
기본 클래스 템플릿이 특수화되더라도 원래의 일반형 템플릿에서 제공하는 인터페이스를 제공할 것이라 컴파일러에게 약속하는 것이다.
정리하면, derived class template에서 base class template의 이름을 참조할 때는, "this->"를 붙이거나, 기본 클래스 한정문을 명시적으로 써야 한다.
Item 44 : Factor parameter-independent code out of templates
매개변수에 독립적인 코드는 템플릿으로부터 분리하자.
템플릿은 코드 반복을 피하고 시간 절약에 유용하다.
하지만, 때때로 템플릿을 사용하는 것은 code bloat(코드 팽창) 문제를 발생시킬 수 있다.
이를 해결하기 위해 name commonality(이름 공통성), variability analysis(가변성 분석)를 사용한다.
공통성 및 가변성 분석은 함수의 측면에서 살펴보면,
두 함수에서 공통적으로 사용되는 부분은 새로운 함수로 만들어, 그 함수를 두 함수에서 호출해주는 방식이다.
템플릿을 사용할 때, 같은 방법을 사용할 수 있다.
하지만 문제가 있는데, non-template code에서는 replication이 explicit인 반면에, template code에서는 replication이 implicit이다.
즉, non-template code에서는 두 함수 또는 클래스 사이의 반복을 직접 확인할 수 있지만, template code에서는 template 소스 코드 복사가 한번만 일어나며 template 인스턴스화는 여러번 발생할 수 있다.
template<typename T, std::size_t n>
class SqaureMatrix{
public:
void invert();
};
SquareMatrix<double, 5> sm1;
sm1.invert();
SquareMatrix<double, 10> sm2;
sm2.invert();
위 코드는 T라는 type 매개변수와, size_t라는 비타입 매개변수를 받는다.
코드의 실행결과 두개의 invert 함수 복사본이 인스턴스화 되며 이 함수들은 동일하지 않다.
이 결과 template 코드에서 code bloat가 발생한다.
사이즈를 파라미터 값으로 받는 함수를 만듦으로써 이 문제를 해결할 수 있다.
template<typename T>
class SquareMatrixBase{
protected:
void invert(std::size_t matrixSize); // invert matrix of the given size
};
template<typename T, std::size_t n>
class SquareMatrix: private SquareMatrixBase<T>{
private:
using SquareMatrixBase<T>::invert; // avoid hiding base version of invert
public:
void invert(){ this->invert(n); } // make inline call to base class
};
Parameterized version의 invert는 base class에 있다.
SquareMatrix 템플릿은 오직 matrix 객체 타입만 템플릿화 한다.
따라서, 같은 타입을 가지는 모든 matrix들은 하나의 SqaureMatrixBase 클래스를 공유한다.
SquareMatrixBase::invert는 derived class가 코드 복제를 피하기 위한 방법일 뿐이므로 public 대신 protected로 상속된다.
derived class가 base class의 invert함수를 inline function으로 호출하기 때문에, 호출에 필요한 추가적인 비용은 없다.
Base class 템플릿 상속시, derived class에서 숨겨지기 때문에, using키워드를 사용하여 문제를 해결했다.
상속또한 private이기 때문에, is-a 관계를 가지지 않고, 특정 기능을 가져와 사용한다.
하지만, SquareMatrixBase::invert 함수가 어떤 데이터를 다뤄야 하는가?
파라미터로 matrix의 size에 대한 정보를 주지만, 특정한 matrix를 위한 data가 어디에 있는지 함수는 알 수 없다.
SquareMatrixBase::invert함수가 매개변수로 행렬의 포인터를 받음으로써 data의 위치를 전달할 수 있다.
그러나, 이 방법은 함수들의 매개변수로 포인터가 추가되어야 하므로 비효율적이다.
따라서 SquareMatrixBase 클래스 자체에 matrix 값의 포인터를 저장하도록 한다.
template<typename T>
class SquareMatrixBase{
protected:
SquareMatrixBase(std::size_t n, T* pMem)
: size(n), pData(pMem){}
void setDataPtr(T* ptr){ pData = ptr; }
private:
std::size_t size;
T* pData;
};
template<typename T, std::size_t n>
class SquareMatrix: private SquareMatrixBase<T>{
public:
SqureaMatrix()
: SquareMAtrixBase<T>(n, data){}
private:
T data[n*n];
};
이렇게 함으로써 SquareMatrix에 속해 있는 멤버 함수 중 상당수가 Base class 버전을 단순 인라인 호출할 수 있다.
또한, 다른 size를 가지더라도 Base class의 복사본을 공유한다.
<double, 5>와 <double, 10>이 <double>멤버 함수를 사용하더라도, 사실상 타입이 다르기 때문에, <double, 5>는 <double, 10>의 함수를 사용할 수 없다.
지금까지 non-type template parameter에 대한 bloat를 살펴봣지만, type parameter도 bloat의 원인이 될 수 있다.
정리하면, 템플릿을 사용하면 수많은 클래스와 함수들이 생성된다.
따라서 template parameter에 종속되지 않는 template 코드는 bloat의 원인이 된다.
비타입 템플릿 매개변수로 생기는 bloat의 경우, 템플릿 매개변수를 함수 매개변수, 클래스 데이터 멤버로 대체함으로써 문제를 해결할 수 있다.
타입 매개변수로 생기는 코드 비대화의 경우, 동일한 이진 표현구조를 가지고 인스턴스화되는 타입들이 한 가지 함수 구현을 공유하게 만듦으로써 bloat를 감소시킬 수 있다.
'C++ > Effective C++' 카테고리의 다른 글
[Effective C++] Chapter 8: Customizing new and delete (0) | 2022.07.13 |
---|---|
[Effective C++] Chapter 7: Templates and Generic Programming(2) (0) | 2022.07.13 |
[Effective C++] Chapter 6: Inheritance and Object-Oriented Design(2) (0) | 2022.07.08 |
[Effective C++] Chapter 6: Inheritance and Object-Oriented Design(1) (0) | 2022.07.06 |
[Effective C++] Chapter 5: Implementations(2) (0) | 2022.07.06 |