44: 매개변수에 독립적인 코드는 템플릿으로부터 분리시키자
템플릿은 코딩 시간 절약, 코드 중복 회피의 두 마리 토끼를 한번에 잡아 주는 참으로 기막힌 물건이다. 하지만 코드 비대화(code bloat)가 초래될 수 있다. 똑같은 내용의 코드와 데이터가 여러 벌로 중복되어 이진 파일로 구워진다는 뜻이다. 목적 코드가 아주 커진다.
우선적으로 써 볼 수 있는 방법은 공통성 및 가변성 분석(commonality and variability analysis)다. 공통 부분을 다른 곳에 옮기고 , 고유 부분은 원래 위치로 남겨 두는 것이다. 템플릿이 아닌 코드에서는 코드 중복이 눈으로 보이지만, 템플릿 코드에서는 코드 중복이 암시적이다. 소스 코드에는 템플릿이 하나밖에 없기 때문에, 어떤 템플릿이 여러 번 인스턴스화될 때 발생할 수 있는 코드 중복을 감각으로 알아채야 한다.
고정 크기의 정방행렬을 나타내는 클래스 템플릿을 만든다고 하자. 이 클래스 템플릿은 역행렬 만들기 연산을 지원한다.
template<typename T, std::size_t n>
class Squarematrix {
public:
void invert();
};
Squarematrix<double, 5> sm1;
sm1.invert();
Squarematrix<double, 10> sm2;
sm2.invert();
이때 invert의 사본이 인스턴스화되는데, 만들어지는 사본의 개수가 두 개이다. 이 둘은 행과 열의 크기가 다르기에, 같은 함수일 수가 없다. 그렇지만 행과 열의 크기를 나타내는 상수만 빼면 두 함수는 완전히 똑같다.
이 크기를 매개변수로 받는 별도의 함수를 만들고, 그 함수에 5와 10을 매개변수로 넘겨서 호출하게 만들려는 자세가 진정한 개발자의 혼이다.
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::invert;
public:
void invert() { this->invert(n); }
};
행렬의 크기를 매개변수로 받도록 바뀐 invert 함수가 기본 클래스인 SquareMatrix-Base에 들어 있는 것이 눈에 보일 것이다. SquareMatrixBase가 템플릿인 것은 SquareMatrix와 마찬가지이지만, 행렬의 원소가 갖는 타입에 대해서만 템플릿화되어 있을 뿐이고 행렬의 크기는 매개변수로 받지 않는다는 것은 SqureMatrix와 다르다. 따라서 같은 타입의 객체를 원소로 갖는 모든 정방행렬은 오직 한 가지의 SqureMatrixBase 클래스를 공유하게 된다. 다시 말해, 같은 원소 타입의 정방행렬이 사용하는 기본 클래스 버전의 invert 함수도 오직 한 개의 사본이란 뜻이다.
아직 해결하지 못한 문제가 있다. 정방행렬의 크기는 매개변수로 받기에 쉽게 알 수 있지만, 진짜 행렬을 저장한 데이터가 어디에 있는지는 알 수 없다. 지금 이 정보를 아는 쪽은 파생 클래스뿐이다. 그렇다면, 기본 클래스 쪽에서 역행렬을 만들 수 있도록 '정방행렬의 메모리 위치'를 파생 클래스가 기본 클래스로 넘겨주면 될 것 같다.
한 가지 방법은 SqureMatrixBase::invert 함수가 매개변수를 하나 더 받도록 만드는 것이다. 하지만 이런 함수가 invert 하나만 있지 않다면, 함수마다 매개변수를 하나씩 더 달아 주어야 하고 결국 이것은 SqureMatrixBase에게 똑같은 정보를 되풀이해서 알려 주는 꼴이다.
다른 방법은 행렬 값을 담는 메모리에 대한 포인터를 SuqreMatrixBase가 저장하게 하는 것이다.
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:
boost::scoped_array<T> pData;
};
이제 코드 비대화의 측면에서 아주 중요한 성과를 얻었다. SqureMatrix에 속해 있는 멤버 함수 중 상당수가 기본 클래스 버전을 호출하는 단순 인라인 함수가 될 수 있으며, 똑같은 타입의 데이터를 원소로 갖는 모든 정방행렬들이 행렬 크기에 상관없이 이 기본 클래스 버전의 사본 하나를 공유한다는 것이다. 이와 동시에, 행렬 크기가 다른 SqureMatrix 객체는 저마다 고유의 타입을 갖고 있다는 점도 아주 중요하다.
이것만은 잊지 말자!
- 템플릿을 사용하면 비슷비슷한 클래스와 함수가 여러 벌 만들어진다. 따라서 템플릿 매개변수에 종속되지 않은 템플릿 코드는 비대화의 원인이다.
- 비타입 템플릿 매개변수로 생기는 코드 비대화의 경우, 템플릿 매개변수를 함수 매개변수 혹은 클래스 데이터 멤버로 대체함으로써 비대화를 종종 없앨 수 있다.
- 타입 매개변수로 생기는 코드 비대화의 경우, 동일한 이진 표현구조를 가지고 인스턴스화되는 타입들이 한 가지 함수 구현을 공유하게 만듦으로써 비대화를 감소시킬 수 있다.
'C++ > Effective C++' 카테고리의 다른 글
[Effective C++] 46: 타입 변환이 바람직할 경우에는 비멤버 함수를 클래스 템플릿 안에 정의해 두자 (0) | 2023.11.16 |
---|---|
[Effective C++] 45: "호환되는 모든 타입"을 받아들이는 데는 멤버 함수 템플릿이 직방! (0) | 2023.11.15 |
[Effective C++] 43: 템플릿으로 만들어진 기본 클래스 안의 이름에 접근하는 방법을 알아 두자 (0) | 2023.11.13 |
[Effective C++] 42: typename의 두 가지 의미를 제대로 파악하자 (1) | 2023.11.11 |
[Effective C++] 41: 템플릿 프로그래밍의 천릿길도 암시적 인터페이스와 컴파일 타임 다형성부터 (0) | 2023.11.10 |