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

업데이트:     Updated:

카테고리:

태그:

이 글은 아래의 책을 자세히 정리한 후, 정리한 글을 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* 기반 구현 공유

🧐 정리

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

Cpp 카테고리 내 다른 글 보러가기

댓글남기기