KoreanFoodie's Study

Effective C++ | 항목 44 : 매개변수에 독립적인 코드는 템플릿으로부터 분리시키자 본문

Tutorials/C++ : Advanced

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* 로 동작하는 버전을 호출하는 식으로 만든다는 것이다!

Comments