[C++] 항목 44: 매개변수에 독립적인 코드는 템플릿으로부터 분리시키자
카테고리: Cpp
태그: Cpp
이 글은 아래의 책을 자세히 정리한 후, 정리한 글을 GPT에게 요약을 요청하여 작성되었습니다.
이펙티브 C++ 제3판, 스콧 마이어스 저자, 곽용재 번역
📦 7. 템플릿과 일반화 프로그래밍
👉🏻 항목 44: 매개변수에 독립적인 코드는 템플릿으로부터 분리시키자
⚠️ 문제: 템플릿 코드 비대화 (Code Bloat)
템플릿을 사용하면 코드 비대화가 초래될 수 있다:
- 똑같거나 거의 똑같은 내용의 코드와 데이터가 이진 파일로 구워진다
해결 원칙: 공통성 및 가변성 분석
- 공통 부분: 새로운 함수에 옮긴 후, 클래스 상속 혹은 객체 합성을 사용하여 원래의 클래스들이 공통 부분을 공유하도록 한다
- 다른 부분(고유 부분): 원래의 함수에 남겨둔다
✅ 문제가 있는 코드
template<typename T, size_t n>
class SquareMatrix {
public:
...
void invert();
};
int main() {
SquareMatrix<double,5> sm1;
...
// SquareMatrix<double,5>::invert 호출
sm1.invert();
SquareMatrix<double,10> sm2;
...
// SquareMatrix<double,10>::invert 호출
sm2.invert();
}
문제:
invert의 사본 2개가 인스턴스화된다- 템플릿 매개변수가 각각 다르기 때문
- 행렬 크기만 다르고 타입은 같은데도 불구하고 코드가 중복 생성됨
✅ 해결: 공통 부분과 고유 부분 분리
template<typename T>
class SquareMatrixBase {
protected:
...
void invert(size_t matrixsize);
};
template<typename T, size_t n>
class SquareMatrix: private SquareMatrixBase<T> {
private:
// 기본 클래스의 invert가 가려지는 것을 막기 위함
using SquareMatrixBase<T>::invert;
public:
...
// invert의 기본 클래스 버전에 대해 인라인 호출 실행
// this->를 붙인 이유: invert가 가려지는 것을 막기 위함
void invert() { this->invert(n); }
};
설계 분석:
1. SquareMatrixBase: 공통 부분
template
class SquareMatrixBase { ... };
- 타입에 대해서만 템플릿화되어 있다
- 행렬의 크기를 템플릿 매개변수로 받지 않는다
→ 이젠, 같은 타입이라면 SquareMatrixBase 클래스, invert 함수를 공유한다.
2. SquareMatrixBase::invert
protected:
void invert(size_t matrixsize);
- 코드 복제를 피할 목적으로
SquareMatrixBase안에 공통 부분으로 마련 - public이 아닌 protected 멤버
- 외부에서 쓰면 안 되기에, 파생 클래스만 내부적으로 쓸 수 있게 함
3. 파생 클래스의 invert
SquareMatrixBase::invert
- 기본 클래스의 invert 함수를 호출하는 인라인 함수
- 호출에 드는 추가 비용은 없어야 한다
4. using과 this-> 사용
(1) using SquareMatrixBase
::invert; (2) this->invert(n);
- 기본 클래스의
invert함수가 가려지는 것을 막기 위함 (항목 43) - 동일한 일을 하기에 둘 중 하나만 해도 된다
5. private 상속
class SquareMatrix: private SquareMatrixBase
{ … }
- private 상속이 되어 있다
- 기본 클래스는 파생 클래스의 구현을 돕기 위한 것일 뿐, 의미가 없다 (항목 39)
🚨 중요한 개념: 인스턴스화의 의미
주의점:
- 객체 인스턴스화가 아닌 템플릿 인스턴스화로, 이진 코드를 공유한다는 뜻이다
- 예로, 객체 2개를 생성하면 동일한 이진 코드에서 나오겠지만, 메모리는 독립적이다
헷갈리면 [C++] 인스턴스화? 페이지를 참고하자.
📌 행렬 메모리 전달 방법
이제 SquareMatrixBase::invert 함수가 다룰 행렬 메모리를 전달해줄 방법을 알아보자.
방법 1: 클래스에 행렬 메모리 저장 (스택 메모리)
template<typename T>
class SquareMatrixBase {
protected:
// 행렬 크기와 포인터를 저장하는 생성자
SquareMatrixBase(size_t n, T* pMem)
: size(n), pData(pMem) {}
// pData에 행렬 포인터를 재대입하는 함수
void setDataPtr(T* ptr) { pData = ptr; }
...
private:
size_t size; // 행렬 크기
T* pData; // 행렬 포인터
};
template<typename T, size_t n>
class SquareMatrix: private SquareMatrixBase<T> {
public:
// 행렬 크기와 포인터를 기본 클래스로 보낸다.
SquareMatrix()
: SquareMatrixBase<T>(n, data) {}
...
private:
T data[n*n]; // 행렬
};
장점:
- 메모리 할당 방법의 결정 권한은 파생 클래스에 있다
- 동적 메모리 할당이 필요 없다
- 상수 전파 등의 최적화에 좋다
방법 2: 힙에 행렬 메모리 저장
template<typename T>
class SquareMatrixBase { ... }
template<typename T, size_t n>
class SquareMatrix: private SquareMatrixBase<T> {
public:
// 행렬 크기와 포인터를 기본 클래스로 보낸다.
SquareMatrix()
: SquareMatrixBase<T>(n, 0), pData(new T[n*n])
{ this->setDataPtr(pData.get()); }
...
private:
// scoped_array에 대해선 항목 13을 참조하자.
boost::scoped_array<T> pData;
};
특징:
- 동적 메모리 할당 사용
- 큰 행렬에 적합
✨ 이 설계의 장점
1. 코드 재사용
- SquareMatrix의 멤버 함수 중 상당수는
기본 클래스 버전을 호출하는 단순 인라인 함수가 될 수 있다 - 기본 클래스 버전의 코드를 공유한다
2. 타입 안전성 유지
- SquareMatrix 객체는 타입, 크기에 따라 각자의 고유 타입을 가지고 있다
3. 성능 향상
- 여러 행렬 크기에 대해 한 가지 버전의 invert를 갖게 된다
- 실행 코드의 크기가 작아진다
- 프로그램의 작업 세트 크기가 줄어든다
- 명령어 캐시 내의 참조 지역성이 향상된다
- 프로그램 실행 속도가 더 빨라질 수 있다
📌 포인터 타입 최적화 기법
핵심 아이디어:
포인터 타입을 매개변수로 취하는 템플릿들은,
이진 수준에서 보면 멤버 함수 집합을 하나만 써도 된다.
예시:
list<int*>list<const int*>list<SquareMatrix<long,3>*>
실제 활용:
- C++ 표준 라이브러리 중에서
vector,deque,list등의 템플릿에 대해 위와 같이 하고 있다 - 즉, void* 포인터로 동작하는 버전을 호출하는 방식으로 만든다
📊 코드 비대화 방지 전략 정리
| 원인 | 문제 | 해결 방법 |
|---|---|---|
| 비타입 매개변수 | 크기만 다른 코드 중복 | 매개변수를 함수 인자나 멤버 변수로 변경 |
| 타입 매개변수 | 동일한 이진 표현의 타입들 | 공통 기본 클래스 사용 |
| 포인터 타입 | 포인터 타입별 중복 | void* 기반 구현 공유 |
🧐 정리
- 템플릿을 사용하면 비슷한 클래스와 함수가 여러 벌 만들어진다.
따라서 템플릿 매개변수에 종속되지 않은 템플릿 코드는 비대화의 원인이 된다. - 비타입 템플릿 매개변수로 생기는 코드 비대화의 경우,
템플릿 매개변수를 함수 매개변수 혹은 클래스 데이터 멤버로 대체함으로써
비대화를 종종 없앨 수 있다. - 타입 매개변수로 생기는 코드 비대화의 경우,
동일한 이진 표현구조를 가지고 인스턴스화되는 타입들이
한 가지 함수 구현을 공유하게 만듦으로써 비대화를 감소시킬 수 있다. - 공통 부분을 기본 클래스로 분리하고, 고유 부분만 파생 클래스에 남긴다.
private 상속을 사용하여 구현 세부사항을 숨길 수 있다.
댓글남기기