이펙티브 C++
https://www.yes24.com/Product/Goods/17525589
페이지 312p ~ 320p
요약
항목 44: 매개변수에 독립적인 코드는 템플릿으로부터 분리시키자
- 템플릿을 통해 코딩 시간 절약, 코드 중복 회피를 잡을 수 있다.
- 클래스 템플릿의 멤버 함수는 이들이 실제로 사용될 때만 암시적으로 인스턴스화 된다.
- 그러나 아무 생각 없이 템플릿을 사용하면 코드 비대화가 초래될 수 있다.
- 똑같거나 거의 비슷한 내용의 코드와 데이터가 여러 별로 중복되어 이진 파일로 구워진다는 의미이다.
- 따라서 이진 코드가 템플릿으로 인해 불어 터지는 현상을 미연에 방지할 방법을 알아둬야 한다.
공통성 및 가변성 분석
- 두 함수를 분석해서 공통적인 부분과 다른 부분을 찾은 후에 공통 부분은 새로운 함수에 옮기고 다른 부분은 원래의 함수에 남겨둔 것이라 할 수 있다.
- 클래스의 경우도 비슷하게 지금 만들고 있는 클래스의 어떤 부분이 다른 클래스의 어떤 부분과 똑같다는 사실을 발견한다면, 공통 부분을 양쪽에 두지 않는 것이다.
- 템플릿을 작성하는 경우에도 똑같은 분석을 하고 똑같은 방법으로 코드 중복을 막으면 된다.
- 그런데 템플릿 코드에서는 코드 중복이 암시적으로 발생한다.
- 소스 코드에는 템플릿이 하나밖에 없지만, 어떤 템플릿이 인스턴스화될 때 발생할 수 있는 코드 중복을 따로 알아채야 한다.
// T 타입의 객체를 원소로 하는 n행, n열의 행렬을 나타내는 템플릿
template<typename T, std::size_t n>
class SquareMatrix {
public:
// 주어진 행렬을 그 저장공간에서 역행렬로 만든다.
void invert();
}
- 위의 템플릿은 T라는 타입 매개변수도 받지만, size_t 타입의 비타입 매개변수인 n도 받도록 되어 있다.
- 비타입 매개변수는 다음과 같이 사용할 수 있다.
SquareMatrix<double, 5> sm1;
sm1.invert();
SquareMatrix<double, 10> sm2;
sm2.invert();
- 이때
invert
의 사본이 인스턴스화되는데, 만들어지는 사본의 개수가 두 개이다.- 이 둘은 같은 함수가 될 수 없다. 한쪽은 5X5 행렬에 대해 동작하고, 다른 쪽은 10X10 행렬에 대해 동작하는 함수이기 때문이다.
- 하지만 행과 열의 크기를 나타내는 상수만 제외하면 두 함수는 완전히 똑같다.
- 이런 현상이 템플릿을 포함한 프로그램이 코드 비대화를 일으키는 일반적인 형태 중 하나다.
- 사용하는 값이 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:
// 기본 클래스의 invert가 가려지는 것을 막기 위한 문장
using SquareMatrixBase<T>::invert;
public:
// invert의 기본 클래스 버전에 대해 인라인 호출 수행
void invert() { this->invert(n); }
}
- 먼저 행렬의 크기를 매개변수로 받을 수 있도록
SquareMatrixBase
의invert
함수를 바꿔준다.SquareMatrixBase
는 행렬의 원소가 갖는 타입에 대해서만 템플릿화되어 있다.- 이렇게 하면 같은 타입의 객체를 원소로 갖는 모든 정방행렬은 오직 한 가지의
SquareMatrixBase
클래스만 공유하게 된다. - 다시 말해, 같은 원소 타입의 정방행렬이 사용하는 기본 클래스 버전의
invert
함수도 오직 한 개의 사본만 생긴다.
- 추가적으로
SquareMatrixBase::invert
함수는 파생 클래스에서 코드 복제를 피할 목적으로만 마련한 장치이기 때문에, protected 멤버로 되어 있고, 함수 호출에 필요한 추가 비용을 없애기 위해 파생 클래스인SquareMatrix
에서는 인라인 함수로invert
를 호출한다.this->
표기는 템플릿화된 기본 클래스의 멤버 함수 이름이 파생 클래스에서 가려지는 문제를 피해가기 위한 것인데,using
선언이 있으므로 반드시 필요한 부분은 아니다.private
으로 기본 클래스를 상속 받고 있는데, 이는 기본 클래스가 파생 클래스의 구현을 돕기 위해 사용하기 때문이다.
- 아직 해결하지 못한 문제가 하나 남아있다.
SquareMatrixBase::invert
함수는 자신이 상대할 데이터가 어떤 것인지를 어떻게 알 수 있을까?- 정방행렬의 크기는 매개변수로 받으니까 쉽게 알 수 있다.
- 진짜 행렬을 저장한 데이터가 어디에 있는지는 어떻게 알 수 있나?
- 이 정보를 아는 쪽은 파생 클래스 밖에 없다.
- 그렇기 때문에 기본 클래스에서 역행렬을 만들 수 있도록 정방행렬의 메모리 위치를 파생 클래스에서 기본 클래스로 넘겨주는 방법을 생각해볼 수 있다.
- 이를 위한 한가지 방법은 매개변수로 전달해주는 것이다.
- 여기서 고민해봐야 할 부분은
SquareMatrix
의 함수 중에invert
처럼 행렬 크기에 상관없는 동작방식을 원하기 때문에SquareMatrixBase
로 옮겨 놓아야 하는 함수가 더 있을 수 있다는 부분이다.(오로지 한 개의 인스턴스만 만들어야 이진 코드 효율성이 좋아진다!) - 각 함수마다 매개변수를 추가하는 방법도 생각해볼 수 있지만, 이는 기본 클래스에 똑같은 정보를 되풀이해서 알려 주는 모양이 된다.
- 차라리 행렬 값을 담는 메모리에 대한 포인터를 기본 클래스가 가지고 있는 방법도 생각할 수 있다.
- 행렬의 포인터도 저장하는 김에 행렬의 사이즈도 기본 클래스에 포함시킬 수 있다.
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; // 행렬 값에 대한 포인터
};
- 기본 클래스는 행결 값에 대한 포인터를 전달 받기 때문에 행렬 값 저장을 위한 메모리 할당 방법의 결정 권한은 파생 클래스 쪽으로 넘어가게 된다.
- 여기에서는 행렬 데이터를
SquareMatrix
객체 안에 데이터 멤버로 직접 넣는 것으로 결정했다.
- 여기에서는 행렬 데이터를
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:
// 기본 클래스의 포인터를 null로 설정하고, 행렬 값의 메모리를 할당하고,
// 파생 클래스의 포인터에 그 메모리를 물려 놓은 후에 이 포인터의 사본을 기본 클래스로 올려보냄
SquareMatrix() : SquareMatrixBase<T>(n, 0), pData(new T[n*n])
{
this->setDataPtr(pData.get());
}
private:
boost::scoped_array<T> pData;
};
- 어느 메모리에 데이터를 저장하느냐에 따라 설계가 달라지긴 하지만, 코드 비대화의 측면에서 아주 중요한 성과를 얻을 수 있다는 점은 같다.
SquareMatrix
에 속해 있는 멤버 함수 중 상당수가 기본 클래스 버전을 호출하는 단순 인라인 함수가 될 수 있다.- 똑같은 타입의 데이터를 원소로 갖는 모든 정방행렬들이 행렬 크기에 상관없이 이 기본 클래스 버전의 사본 하나를 공유한다.
- 행렬 크기가 다른
SquareMatrix
객체는 서로 다른 고유의 객체가 되기 때문에SquareMatrix<double, 10>
을 취하는 함수는SquareMatrix<double, 5>
를 매개변수로 취할 수 없다.(컴파일 에러 발생)
메모
항목 44: 매개변수에 독립적인 코드는 템플릿으로부터 분리시키자
- 템플릿을 사용하면 비슷비슷한 클래스와 함수가 여러 벌 만들어진다. 따라서 템플릿 매개변수에 종속되지 않은 템플릿 코드는 비대화의 원인이 된다.
- 비타입 템플릿 매개변수로 생기는 코드 비대화의 경우, 템플릿 매개변수를 함수 매개변수 혹은 클래스 데이터 멤버로 대체함으로써 비대화를 종종 없앨 수 있다.
- 타입 매개변수로 생기는 코드 비대화의 경우, 동일한 이진 표현구조를 가지고 인스턴스화되는 타입들이 한 가지 함수 구현을 공유하게 만듦으로써 비대화를 감소시킬 수 있다.
'책 > 이펙티브 C++' 카테고리의 다른 글
이펙티브 C++(340p ~ 350p) (1) | 2024.02.10 |
---|---|
이펙티브 C++(332p ~ 340p) (1) | 2024.02.09 |
이펙티브 C++(320p ~ 331p) (1) | 2024.02.08 |
이펙티브 C++(306p ~ 312p) (1) | 2024.02.06 |
이펙티브 C++(295p ~ 306p) (0) | 2024.02.05 |