KoreanFoodie's Study
Effective C++ | 항목 44 : 매개변수에 독립적인 코드는 템플릿으로부터 분리시키자 본문
Effective C++ | 항목 44 : 매개변수에 독립적인 코드는 템플릿으로부터 분리시키자
GoldGiver 2022. 10. 25. 16:31
C++ 프로그래머의 필독서이자 바이블인, 스콧 마이어스의 Modern Effective C++ 를 읽고 기억할 내용을 요약하고 있습니다. 꼭 읽어보시길 추천드립니다!
항목 44 : 매개변수에 독립적인 코드는 템플릿으로부터 분리시키자
핵심 :
1. 템플릿을 사용하면 비슷비슷한 클래스와 함수가 여러 벌 만들어진다. 따라서 템플릿 매개변수에 종속되지 않은 템플릿 코드는 비대화의 원인이 된다.
2. 비타입 템플릿 매개변수로 생기는 코드 비대화의 경우, 템플릿 매개변수를 함수 매개변수 혹은 클래스 데이터 멤버로 대체함으로써 비대화 종종 없앨 수 있다.
3. 타입 매개변수로 생기는 코드 비대화의 경우, 동일한 이진 표현구조를 가지고 인스턴스화되는 타입들이 한 가지 함수 구현을 공유하게 만듦으로써 비대화를 감소시킬 수 있다.
템플릿 코드는 코드 중복을 회피할 수 있지만, 코드 비대화(code bloat) 문제를 초래할 수 있다. 다음 예시를 보자.
template<typename T, std::size_t n>
class SquareMatrix
{
public:
...
void invert();
};
...
SquareMatrix<double, 5> sm1;
// SquareMatrix<double, 5>::invert 호출
sm1.invert();
SquareMatrix<double, 10> sm2;
// SquareMatrix<double, 10>::invert 호출
sm2.invert();
"std::size_t n" 처럼 비타입 매개변수(non-type paramter) 인 n 을 받는 SquareMatrix 클래스를 정의했다.
sm1 과 sm2 는 타입이 다르므로, 완전히 같은 동작을 함에도 invert 함수를 중복해서 생성할 것이다.
이를 막기 위해, SquareMatrixBase 클래스를 만들어 볼 수 있다.
template<typename T>
class SquareMatrixBase
{
protected:
// 주어진 크기의 행렬을 역행렬로 만든다
void invert(std::size_t matrixSize);
}
template<typename T, std::size_t n>
class SquareMatrix: private SquareMatrixBase<T>
{
private:
using SquareMatrixBase<T>::invert;
public:
...
// invert 기본 클래스 버전에 대해 인라인 호출을 수행
void invert() { this->invert(n); }
};
이렇게 되면, 모든 정방행렬이 사용하는 기본 클래스 버전의 invert 함수는 오직 한 개의 사본이 된다.
행렬의 크기는 인자로 쉽게 전달할 수 있지만, 실질적인 메모리를 어떻게 참조할 수 있을까? 다음과 같이 구현해 보자.
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:
// 행렬 크기와 데이터 포인터를 기본 클래스로 올려보낸다
SquareMatrix()
:SquareMatrixBase<T>(n, data) {}
private:
T data[n*n];
};
이렇게 파생 클래스를 만들면 동적 메모리 할당이 필요 없는 객체가 되지만, 객체 자체의 크기가 좀 커질 수 있다. 각 행렬의 데이터를 힙에 두는 방법도 있다.
template<typename T, std::size_t n>
class SquareMatrix: private SquareMatrixBase<T>
{
public:
// 행렬 크기와 데이터 포인터를 기본 클래스로 올려보낸다
SquareMatrix()
:SquareMatrixBase<T>(n, 0), pData(new T[n*n])
{
this->setDataPtr(pData.get());
}
private:
// 항목 13 참고
boost::scoped_array<T> pData;
};
boost::scoped_array 는 new [ ] 처럼 동적으로 할당되지만, 스마트 포인터처럼 영역에서 빠져나갈 때 자동으로 메모리가 해제되는 객체이다.
어쨌든, 위 두 가지 방식 전부 코드 비대화를 줄일 수 있다. 물론 위의 두 가지 방식보다는, 행렬 크기가 미리 녹아든 상태로 별도의 버전이 만들어지는 invert 가 더 좋은 코드를 생성할 수도 있다. 상수 전파(constant propagation) 등의 최적화가 먹혀들어가기 좋기 때문이다.
반면, 여러 행렬 크기에 대해 한 가지 버전의 invert 를 두도록 만들면(위의 두 가지 방식을 써서) 프로그램의 작업 세트(working set) 크기가 줄어들면서 명령어 캐시 내의 참조 지역성(locality of reference) 가 향상될 수도 있다. 물론 SquareMatrixBase 에 추가적인 포인터 하나가 더 커져서 객체의 크기가 늘어날 수도 있다. 결론은, 여러 가지 가능성과 문제를 잘 고려하면서 만들어야 한다는 것이다.
vector<int> 나 vector<long> 멤버 함수도, 사실 똑같이 생겼다. list<int*>, list<const int*>, list<SquareMatrix<long, 3>*> 은 이진 수준에서 보면 멤버 함수 집합을 달랑 한 벌만 써도 되어야 한다. 즉, void* 로 동작하는 버전을 호출하는 식으로 만든다는 것이다!
'Tutorials > C++ : Advanced' 카테고리의 다른 글
Effective C++ | 항목 46 : 타입 변환이 바람직할 경우에는 비멤버 함수를 클래스 템플릿 안에 정의해 두자 (0) | 2022.10.25 |
---|---|
Effective C++ | 항목 45 : "호환되는 모든 타입"을 받아들이는 데는 멤버 함수 템플릿이 직방! (0) | 2022.10.25 |
Effective C++ | 항목 43 : 템플릿으로 만들어진 기본 클래스 안의 이름에 접근하는 방법을 알아 두자 (0) | 2022.10.25 |
Effective C++ | 항목 42 : typename 의 두 가지 의미를 제대로 파악하자 (0) | 2022.10.25 |
Effective C++ | 항목 41 : 템플릿 프로그래밍의 천릿길도 암시적 인터페이스와 컴파일 타임 다형성부터 (0) | 2022.10.25 |