본문 바로가기
이론/[책]Effective C++

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

by 사과잼빵 2017. 12. 26.

템플릿을 사용하면 생김새나 하는 일이 비슷한 클래스의 코드를 일일이 작성해주지 않아도 된다.

* 몰랐던 내용인데 클래스 템플릿의 멤버 함수는 실제로 사용될 때에만 암시적으로 인스턴스화 된다고 한다.(나는 클래스가 인스턴스화될 때 들고있는 모든 멤버함수들도 인스턴스화 되는 줄 알고 있었다...)


하지만 이 템플릿도 아무 생각 없이 사용하다가는 템플릿의 적 코드 비대화(code bloat)를 초래 할 수 있다.

고정 크기의 정방행렬을 나타내는 클래스 템플릿으로 위 설명의 예를 보여드리겠습니다.


template<typename T, std::size_t n>

class SquareMatrix 

{

public:

  void invert();   //주어진 행렬을 역행렬로 만드는 함수

}


위으 SquareMatrix 템플릿 클래스는 T라는 타입 매개변수와 size_t라는 비타입 매개변수 n을 받도록 되어있습니다.

위의 템플릿 클래스는 아래와 같이 사용될 것입니다.


SquareMatrix<double, 5> sm1;

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


SquareMaxrix<double, 10> sm2;

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


두 타입 SqureMatrix<double, 5>, SqureMatrix<double, 10>에 각각의 invert 함수가 인스턴스화 됩니다. 만들어진 두 개의 함수는 행과 열의 크기를 제외하면 완전히 똑같은 함수이기 때문에 이런 현상이 코드를 비대화 시킬 수 있습니다.


위의 코드 비대화 문제를 해결하기 위해서는 기본 클래스 템플릿에 행렬의 크기를 매개변수로 받는 함수를 만들어서 파생 클래스 템플릿 객체에서 위로 호출하게 하면 됩니다.

코드는 아래와 같이 작성하면됩니다.

template<typename T>

class SquareMatrixBase

{

protected:

  void invert(std::size_t matrixSize);

}


template <typename T, std::size_t n>

class SqureMatrix : private SqureMatrixBase<T>

{

  using SquareMatrixBase<T>::invert;  //기본 클래스의 invert가 가려지는 것을 막기                                                     위함(항목 43)

public:

  void invert() { this->invert(n); }

}


위의 코드와 같이 코드를 작성하면 SquareMatrix<double, 5>, SquareMatrix<double, 10> 객체에서 invert를 호출하면 SquareMatrixBase<double>::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;

}

1. 행렬 값들을 담는 데이터를 객체 안에 두는 방법

template <typename T, std::size_t n>

class SquareMatrix : private SquareMatrixBase<T>

{

public:

  SquareMatrix()

  : SquareMatrixBase<T>(n, data)

    {}

private:

  T data[n*n];

};


2. 행렬 값들을 담는 데이터를 힙에 두는 방법

template <typename T, std::size_t n>

class SquareMatrix : private SquareMatrixBase<T>

{

public:

  SquareMatrix()

  : SquareMatrixBase<T>(n, nullptr),

    pData(new T[n*n])

    {

       this->setDataPtr(pData.get());

    }

private:

  boost::scoped_array<T> pData;  // shared_ptr<T> pData; 정도로 변경하면될것같다.

}


위와 같이 코드의 중복을 최대한 피하도록 설계를 하게되면 손해를 보게되는 부분도 있습니다.

행렬 크기별 타입이 만들어지는 경우 컴파일 단계에서 미리 가지고 있는 상수 값이므로 상수 전파(constant propagation) 등의 최적화가 적용될 가능성이 높습니다.

- 상수 전파

컴파일 단계에서 상수값을 이용해 계산될 수 있는 값들은 미리 계산된 값으로 치환되는 것


반면 여러 크기의 행렬에 대해 한 가지 버전의 invert를 두도록 만들면 실행 코드의 크기가 작아지면서 프로그램의 작업 세트 크기가 줄어들어 캐시 내의 참조 지역성이 향상되기 때문에 앞의 상수 전파 같은 최적화 효과를 얻지 못한 것에 대한 보상을 받고도 남을 수 있습니다. 

어떤 효과를 더 우선시 할지는 사용 플랫폼 및 데이터 집합에 따라 다를 수 있기 때문에 둘다 적용해보고 결과를 관찰해봐야 합니다.


- 작업 세트

한 프로세스에서 자주 참조하는 페이지의 집합. 간단히 프로세스가 현재 사용하는 메모리 양을 지칭할 때도 쓴다.


타입 매개변수 또한 코드 비대화의 원인이 될 수 있습니다.

int와 long은 이진 표현구조가 동일한데 vector<int>와 vector<long>의 멤버함수는 동작과 코드가 똑같게 나올 수 있습니다. 

포인터 타입을 매개변수로 취하는 동일 계열의 템플릿들은 이진 수준에서 보면 멤버 함수 집합을 한 벌만 써도 됩니다. 따라서 T*를 써서 동작하는 멤버 함수를 구현 할 때는 void*로 동작하는 버전을 호출하는 식으로 만들기도 합니다. (c++ 표준 라이브러리 중 일부는 이런식으로 되어있습니다.)



결론

템플릿이 오히려 코드 비대화의 원인이 될 수 있으므로 템플릿 설계 시에도 코드 비대화를 고려해야한다.