책/이펙티브 C++

이펙티브 C++(312p ~ 320p)

readme.md 2024. 2. 8. 23:18

이펙티브 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); }
}
  • 먼저 행렬의 크기를 매개변수로 받을 수 있도록 SquareMatrixBaseinvert 함수를 바꿔준다.
    • 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: 매개변수에 독립적인 코드는 템플릿으로부터 분리시키자

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