본문 바로가기
서적 정리/Effective C++

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

by 민돌이2 2021. 12. 24.

템플릿은 코딩 시간 절약, 코드 중복 회피가 가능하게 해주는 기능이다.

하지만, 아무생각없이 템플릿을 사용하면 코드 비대화(code bloat)가 초래될 수 있다. 똑같은 내용의 코드와 데이터가 여러 개로 중복되어 이전 파일로 생성될 수 있다는 뜻이다. 소스 코드만 보면 단정해 보이지만, 목적 코드는 비대해 질 수 있다. 우리는 템플릿으로 인해 코드 비대화를 예방할 방법을 숙지해야 한다.

 

 

공통성 및 가변성 분석(commonality and variability analysis)

만약 현재 만들고 있는 클래스의 어떤 부분이 다른 클래스의 어떤 부분과 똑같다는 사실을 발견한다면, 그 공통 부분을 양쪽에 두지 않는 것이 합리적인 코딩이다. 즉, 공통 부분을 별도의 새로운 클래스에 옮긴 후, 클래스 상속 혹은 객체 합성(32,장 38장, 39장 참고)을 사용하여 원래의 클래스들이 공통 부분을 공유하도록 한다. 원래의 두 클래스가 각각 갖고 있는 고유 부분은 원래의 위치에 남아 있게 된다.

 

템플릿도 똑같은 방법으로 코드 중복을 예방하면 된다.

하지만, 템플릿이 아닌 코드에서는 코드 중복이 발생한다. 두 함수 혹은 클래스 사이의 똑같은 부분은 눈으로 찾을 수 있지만, 템플릿 코드에서는 코드 중복이 눈으로 찾을 수 없는 암시적이다. 눈으로 보이는 소스 코드에는 템플릿이 하나 뿐이기 때문이다.

즉, 어떤 템플릿이 여러 번 인스턴스화될 때 발생할 수 있는 코드 중복을 감각으로 알아채야 한다

template<typename T, size_t n> class SquareMatrix { public: void Invert(); //역행렬 }; int main() { ‌SquareMatrix<double, 5> sm1; ‌SquareMatrix<double, 10> sm2; ‌sm1.Invert(); //SquareMatrix<double, 5>::Invert 호출 ‌sm2.Invert(); //SquareMatrix<double, 10>::Invert 호출 system("Pause"); return 0; }

위 코드의 템플릿은 타입 매개변수로 T를 받고 size_t 타입의 비타입 매개변수(non-type parameter)인 n도 받고 있다.

sm1은 5x5 행렬이고, sm2는 10x10 행렬이다. 이때 Invert의 사본이 인스턴스화 되는데, 만들어지는 사본의 개수가 두 개이다.

하지만, 분명 행렬의 수가 다른 다른 행렬이지만 행과 열의 크기를 나타내는 상수만 빼면 두 함수는 완전히 똑같다. 이런 현상이 바로 템플릿을 포함한 프로그램이 코드 비대화를 일으키는 일반적인 형태 중 하나이다.

 

어떻게 하면 코드 비대화를 막을 수 있을까?

행렬의 크기만 다르고 나머지가 모두 같은 코드라면, 행렬의 크기를 매개변수로 넘겨서 호출하게 만들면 된다.

template<typename T> class SquareMatrixBase { protected: void Invert(size_t matrixSize); //역행렬 구현 }; template<typename T, size_t n> class SquareMatrix : private SquareMatrixBase<T> { private: using SquareMatrixBase<T>::Invert; //템플릿 기본 클래스의 Invert 호출 인지시킴 public: void Invert() { this->Invert(n); } //기본 클래스의 Invert 호출 };

사실 가시적으로 차이가 보이지 않기에 잘 이해가 안된다.

설명하자면, SquareMatrix 템플릿은 템플릿 매개변수 n으로 인해 함수의 사본이 여러개가 될 수 있다.

반면 SquareMatrixBase 템플릿은 원소의 타입에 대해서만 템플릿 매개변수로 받는다. 즉, SquareMatrixBase 템플릿의 Invert 함수는 복수의 사본이 생성될 일이 없다. 연쇄적으로 SquareMatrix 템플릿은 기본 템플릿인 SquareMatrixBase의 Invert 함수를 호출하므로 복수의 사본이 생성될 일이 없다.

 

이전에 책에서 공부했던 것을 적어보자면 파생 클래스인 SquareMatrixBase의 Invert 함수는 암시적 인라인(30장 참고)이 될 것이고 this-> 키워드를 붙이고, using SquareMatrix::Invert를 선언함으로써 기본 클래스의 함수를 호출하고 있다(43장 참고).

 

파생클래스인 SquareMatrix은 실제 행렬 데이터를 갖고 있지만, SquareMatrixBase::Invert 함수는 자신이 상대할 데이터가 어디에 있는지 모른다.

 

그렇다면 파생 클래스의 행렬의 메모리에 대한 포인터를 기본클래스인 SquareMatrixBase가 갖게 하면 될 것이다.

template<typename T> class SquareMatrixBase { protected: SquareMatrixBase(size_t n, T* pMem) ‌‌: size(n), pData(pMem) {} void SetDataPtr(T* ptr) { pData = ptr; } void Invert(size_t matrixSize); private: size_t size; ‌T* pData; //파생 클래스의 행렬의 데이터의 주소 }; template<typename T, size_t n> class SquareMatrix : private SquareMatrixBase<T> { private: using SquareMatrixBase<T>::Invert; public: SquareMatrix() ‌‌: SquareMatrixBase<T>(n, data) //기본 클래스 생성 시 행렬의 포인터 보냄 {} ​​​​ void Invert() { this->Invert(n); } private: ‌T data[n*n]; //실제 행렬 데이터 };

이렇게 구현하면 파생 클래스는 동적 메모리 할당이 필요 없는 객체가 되지만, 객체 자체의 크기가 커질 수 있다.

 

객체 크기가 커지는 것이 싫으면 동적 할당하여 행렬을 힙에 둘 수도 있다.

template<typename T, size_t n> class SquareMatrix : private SquareMatrixBase<T> { private: using SquareMatrixBase<T>::Invert; public: SquareMatrix() ‌‌: SquareMatrixBase<T>(n, NULL), //기본 클래스의 포인터를 NULL로 ‌‌pData(new T[n*n]) //pData 생성 후 기본 클래스로 포인터 보냄 { ‌‌this->SetDataPtr(pData.get()); //기본 클래스로 포인터 보냄 } void Invert() { this->Invert(n); } private: ‌boost::scoped_array<T> pData; };

 

위 두 가지 방법으로 세 가지 장점을 얻을 수 있다.

1.파생 클래스에 속해 있는 멤버 함수 중 대부분이 기본 클래스 버전을 단순 인라인 호출 할 수 있다.

 

2.기본 클래스의 사본 하나를 공유한다.

기본 클래스의 템플릿 매개변수는 자료형 한 개 뿐이다. 즉, <double, 5>와 <double, 10>은 double형 기본 클래스 하나를 공유한다.

 

3.파생 클래스의 객체는 각각 고유의 타입을 갖고 있다.

위 코드에서는 <double, 5>와 <double , 10>이 같은 double 형일 지라도 이 둘의 타입은 다르기 때문에, <double, 10>을 사용하는 함수가 <double ,5> 객체를 사용할 수 없다는 뜻이다.

 

 

코드 비대화(code bloat)

이번 챕터는 비타입 템플릿 매개변수로 인한 코드 비대화가 주제였다.

얼핏 보면 타입 매개변수는 코드 비대화의 원인이라고 느낄 수 있다. 원인이 되는 것은 사실이지만, 코드 비대화의 예중 하나 일 뿐이다. 상당수의 플랫폼에서 int와 long은 이진 표현구조가 동일하다.  int와 long에 대해 인스턴스화 되는 템플릿들은 상당수의 플랫폼에서는 코드 비대화를 일으킬 수 있다.

비슷한 예로 포인터 타입의 경우이다. 대부분의 플랫폼에서 포인터 타입은 똑같은 이진 표현구조를 갖고 있기 때문에, 포인터 타입을 매개변수로 취하는 list<int*>, list<const int*>, list<SqureMatrix<long, 3>*> 등은 이진 수준에서 보면 멤버 함수 집합을 하나만 써도 되어야 한다. 타입 제약이 엄격한 포인터(T* 포인터)를 사용하는 멤버 함수를 구현할 떄는 하단에서 타입 미정(untyped) 포인터(void* 포인터)로 동작하는 버전을 호출하는 식으로 만든다. 실제로 C++ 표준 라이브러리의 vector, deque, list 등의 템플릿에 대해 이런 식으로 구현했다.

 

 

이것만은 잊지 말자

템플릿을 사용하면 비슷한 클래스와 함수가 여러 개 만들어진다. 따라서 템플릿 매개변수에 종속되지 않은 템플릿 코드는 비대화의 원인이 된다.

비타입 템플릿 매개변수로 생기는 코드 비대화의 경우, 템플릿 매개변수를 함수 매개변수 혹은 클래스 데이터 멤버로 대체함으로써 비대화를 종종 없앨 수 있다.

타입 매개변수로 생기는 코드 비대화의 경우, 동일한 이진 표현구조로 갖고 인스턴스화되는 타입들이 한 가지 함수 구현을 공유하게 만듦으로써 비대화를 감소시킬 수 있다.

728x90

댓글