Notice
Recent Posts
Recent Comments
Link
«   2026/05   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31
Archives
Today
Total
관리 메뉴

로또

44. 매개변수에 독립적인 코드는 템플릿으로부터 분리시키자 본문

책/Effective C++

44. 매개변수에 독립적인 코드는 템플릿으로부터 분리시키자

아롱로또 2023. 12. 15. 17:57

다루는 내용

템플릿의 사용으로 인해 발생할 수 있는 문제인 코드 비대화에 대해 알아보고, 이를 해결하는 방법에 대해 알아보자.

템플릿과 코드 비대화(Code Bloat)

템플릿의 사용은 코딩 시간 절약과 코드 중복 회피를 가능하게 해준다. 하지만 주의를 기울이지 않고 템플릿을 사용할 경우 코드 비대화를 초래할 수 있다.

코드 비대화란, (거의) 똑같은 내용의 코드와 데이터가 중복되어 이진 파일로 생성되는 것을 말한다. 소스 코드에서 우리가 눈으로 확인할 수 있는 명시적인 중복이 없어도 목적 코드에서 템플릿의 사용으로 인한 암시적인 중복은 존재할 수 있다.

 

코드 비대화 문제를 해결하기 위해 공통성 및 가변성 분석(Commonality and Variability Analysis)을 하여야 한다.

이는 코드에서 중복되는 부분을 찾아내어 해당 코드를 별도로 뽑아내고 필요한 곳에서 상속 혹은 객체 합성 등 여러 방법으로 중복을 제거하는 것이다.

 

템플릿에서의 공통성 및 가변성 분석

고정 크기의 정방 행렬을 나타내는 클래스 템플릿 코드이다.

template<typename T, std::size_t n>
class SquareMatrix{
public:
	void Invert(); // 주어진 행렬을 역행렬로 만든다.
	...
};

이 템플릿은 타입 매개변수 T와 함께, size_t 타입의 비타입 매개변수(non-type parameter)인 n도 받도록 되어있다.

SquareMatrix<double, 5> sm1;
sm1.Invert(); // SquareMatrix<double, 5>::Invert() 호출

SquareMatrix<double, 10> sm2;
sm2.Invert(); // SquareMatrix<double, 10>::Invert() 호출

이 때, Invert의 사본이 인스턴스화 되는데, 이 둘은 각각 5*5, 10*10 행렬에 대해 동작하는 함수로, 만들어지는 사본의 개수가 두 개이다. 즉, 행과 열의 크기를 나타내는 상수만 빼면 완전히 동일한 두 함수가 만들어지는 것이다. 이러한 현상은 템플릿을 포함한 프로그램이 코드 비대화를 일으키는 일반적인 형태 중 하나이다.

 

이에 대해, 다음과 같이 중복을 제거할 수 있다.

template<typename T>
class SquareMatrixBase{
protected:
	void Invert(std::size_t matrix_size); // 주어진 행렬을 역행렬로 만든다.
	...
};

template<typename T, std::size_t n>
class SquareMatrix: private SquareMatrixBase{
private:
	using SquareMatrixBase<T>::Invert; // 이름 가려짐 방지
public:
	void Invert(){ this->Invert(n); } // SquareMatrixBase::Invert 호출
	...
};

파생 클래스 SquareMatrix::Invert()는 인라인 함수를 사용해 기본 클래스 SquareMatrixBase::Invert()를 호출한다.

 

실제 Invert 동작을 수행하는 함수를 기본 클래스로 옮기고, 행렬의 크기를 매개변수로 받도록 동작하였다.

SquareMatrixBase는 분명 템플릿이지만 행렬의 원소가 갖는 타입에 대해서만 템플릿화되어 있을 뿐이고 행렬의 크기는 템플릿 매개변수로 들어있지 않다. 이 차이로 인해 같은 타입의 객체를 원소로 갖는 모든 정방행렬들이 오직 한 가지의 SquareMatrixBase 클래스를 공유할 수 있게 된다.

 

또한, 다음과 같은 몇 가지 포인트가 숨어 있다.

  1. SquareMatrixBase::Invert 함수는 파생 클래스에서 코드 복제를 피할 목적으로 존재하므로 protected 멤버에 속한다.
  2. SquareMatrixBase::Invert 함수의 호출에 드는 추가 비용은 없어야 하므로 SquareMatrix::Invert 함수를 암시적 인라인 함수로 만들었다. (클래스 내 선언)
  3. SquareMatrixBase::Invert 함수의 이름이 SquareMatrix::Invert 함수에 의해 가려지지 않도록 using 선언과 this를 사용하였다. (둘 중 하나만 사용하여도 된다.)
  4. SquareMatrixBase와 SquareMatrix는 private 상속 관계이다. 이는 SquareMatrixBase가 단순히 SquareMatrix의 구현을 돕기 위한 것 외엔 아무 관계도 가지지 않음을 말한다. 

아직 끝이 아니다. SquareMatrixBase::Invert 함수가 실제로 역행렬을 만들기 위한 연산을 수행할 때, 원래 행렬에 대한 정보를 필요로 할 것이다. 때문에 기본 클래스에 정보를 전달할 수 있는 방법을 떠올려보자.

 

1. SquareMatrixBase::Invert 함수에 행렬의 포인터를 매개변수로 전달하자.

이러한 방법은 분명 올바르게 동작하겠지만 SquareMatrixBase에 Invert와 같은 다른 함수를 추가할 때마다 매개변수를 추가적으로 전달해주어야 할 것이다. 하지만 이러한 방식은 SquareMatrixBase에게 똑같은 정보를 되풀이해서 알려주는 것이므로, 다른 방식을 떠올려 보자.

 

2. SquareMatrixBase 클래스에 행렬의 포인터를 멤버 변수로 저장하자.

template<typename T>
class SquareMatrixBase{
protected:
	SquareMatrixBase(std::size_t n, T *p_mem)
	:size(n), p_data(p_mem){}

	void SetDataPtr(T* ptr){ p_Data = ptr; }

private:
	std::size_t size;
	T *p_data;
};

template<typename T, std::size_t n>
class SquareMatrix: private SquareMatrixBase{
public:
	SquareMatrix()
	:SquareMatrixBase<T>(n, data){}

private:
	T data[n*n];
};

이러한 방법은 코드 비대화를 효과적으로 해결할 수 있다. SquareMatrix에 속해 있는 멤버 함수의 상당수가 기본 클래스의 함수를 호출하는 단순 인라인 함수로 변할 것이며, 똑같은 타입의 데이터를 원소로 갖는 모든 정방행렬들이 행렬 크기에 상관없이 기본 클래스 버전의 사본 하나를 공유할 수 있다. 이와 동시에 행렬 크기가 다른 SquareMatrix 객체는 저마다 고유의 타입을 갖고 있다.

 

각각의 장단점

처음의 방법(행렬 크기를 미리 정하여 별도의 버전이 만들어지는 Invert)의 경우, 행렬 크기가 함수 매개변수로 넘겨지거나 객체에 저장하여 다른 파생 클래스가 공유하는 방법보다 더 좋은 코드를 생성할 가능성이 높다.

 

전자의 경우, 행렬 크기가 컴파일 타임에 투입되는 상수이므로 상수 전파(constant propagation - 상수 값을 사용하는 연산에서, 컴파일러가 해당 연산을 미리 계산하여 코드에 적용하는 기법) 등의 최적화가 적용되기 좋고, 생성되는 기계 명령어에 대해 이 크기 값이 즉치 피연산자(immediate operand)로 적용되는 것도 이런 종류의 최적화 중 하나이다.

 

후자의 경우, 실행 코드의 크기가 작아진다. 이는 프로그램의 working set 크기를 감소시켜 명령어 캐시 내의 참조 지역성을 향상시킬 수 있어 프로그램 실행 속도를 빠르게 한다.

기본 클래스가 행렬 데이터 포인터 변수를 가진다는 것은 캡슐화를 해치며, 객체의 크기 또한 늘어나게 한다. 물론 깊은 고민을 통해 이를 해결할 수도 있겠지만 그럴수록 복잡해져 코드 중복을 조금 허용하는 것이 나을수도 있다.

 

이것만은 잊지 말자!

  • 템플릿을 사용하면 비슷비슷한 클래스와 함수가 여러 벌 만들어진다. 따라서 템플릿 매개변수에 종속되지 않은 템플릿 코드는 비대화의 원인이 된다.
  • 비타입 템플릿 매개변수로 생기는 코드 비대화의 경우, 템플릿 매개변수를 함수 매개변수 혹은 클래스 데이터 멤버로 대체함으로써 비대화를 종종 없앨 수 있다.
  • 타입 매개변수로 생기는 코드 비대화의 경우, 동일한 이진 표현구조를 가지고 인스턴스화되는 타입들이 한 가지 함수 구현을 공유하게 만듦으로써 비대화를 감소시킬 수 있다.