(전체가 아니라 C#과 차이가 있는 부분을 중심으로 요약 정리)

템플릿 소개

절차형 프로그래밍 패러다임에서는 프로시저나 함수 단위로 프로그램을 작성한다. 그중에서 특히 함수를 많이 사용하는데, 알고리즘을 작성할 때 특정한 값에 의존하지 않게 구현해두면 나중에 임의의 값에 대해 얼마든지 재사용이 가능하기 때문이다. 이렇게 함수를 작성하는 것을 매개변수화 한다고 표현한다.

객체지향 프로그래밍 패러다임은 객체란 개념도 도입했는데, 객체란 데이터와 동작을 하나로 묶은 것으로 함수나 메서드에서 값을 매개변수화하는 방식과는 별개다.

템플릿은 매개변수화 개념을 더욱 발전시켜 값 뿐만 아니라 타입에 대해서도 매개변수화한다. C++에서 기본으로 제공하는 int, double 같은 기본 타입뿐만 아니라 SpreadsheetCell이나 CherryTree처럼 사용자가 정의한 클래스에 대해서도 매개변수화할 수 있다. 템플릿을 이용하면 주어진 값뿐만 아니라 그 값의 타입에 대해서도 독립적인 코드를 ㅈ가성할 수 있다.

템플릿이 제공하는 기능이 뛰어나지만 C++ 템플릿 문법이 상당히 복잡해서 템플릿을 직접 정의하지 않는 프로그래머가 많다.

(C#에서 제네릭과 유사하다)

클래스 템플릿

클래스 템플릿(class template)은 멤버 변수 타입, 메서드의 매개변수 또는 리턴 타입을 매개변수로 받아서 클래스를 만든다. 클래스 템플릿은 주로 객체를 저장하는 컨테이너나 데이터 구조에서 많이 사용한다.

클래스 템플릿 작성법

템플릿 없이 구현한 Grid 클래스

(템플릿에 대한 내용이 주요하므로 그 외 부분에 대한 설명은 생략)

클래스 정의

class GamePiece
{
  public:
    virtual std::unique_ptr<GamePiece> clone() const = 0;
};

class ChessPiece : public GamePiece
{
  public:
    virtual std::unique_ptr<GamePiece> clone() const override;
};

std::unique_ptr<GamePiece> ChessPiece::clone() const
{
  // 복제 생성자를 호출해서 이 인스턴스를 복제한다.
  return std::make_unique<ChessPiece>(*this);
}

class GameBoard
{
  public:
    explicit GameBoard(size_t width = kDefaultWidth, size_t height = kDefaultHeight);
    GameBoard(const GameBoard& src); // 복제 생성자
    virtual ~GameBoard() = default; // 가상 디폴트 소멸자
    GameBoard& operator=(const GameBoard& rhs); // 대입 연산자

    // 이동 생성자와 대입 연산자를 명시적으로 디폴트로 지정한다.
    GameBoard(GameBoard&& src) = default;
    GameBoard& operator=(GameBoard&& src) = default;

    std::unique_ptr<GamePiece>& at(size_t x, size_t y);
    const std::unique_ptr<GamePiece>& at(size_t x, size_t y); const

    size_t getHeight() const { return mHeight; }
    size_t getWidth() const { return mWidth; }

    static const size_t kDefaultWidth = 10;
    static const sizt_t kDefaultHeight = 10;

    friend void swap(GameBoard& first, GameBoard& second) noexcept;

  private:
    void verifyCoordinate(size_t x, sizt_y y) const;

    std::vector<std::vector<std::unique_ptr<GamePiece>>> mCells;
    size_t mWidth, mHeight;
};

메서드 정의

GameBoard::GameBoard(size_t width, size_t height)
  : mWidth(width), mHeight(height)
{
  mCells.resize(mWidth);

  for (auto& column : mCells)
  {
    column.resize(mHeight);
  }
}

GameBoard::GameBoard(const GameBoard& src)
  : GameBoard(src.mWidth, src.mHeight)
{
  // 여기 나온 생성자 이니셜라이저는 먼저 적절한 크기의 메모리를 할당하는 작업을 비복제 생성자에 위임한다.

  // 그리고 나서 데이터를 복제한다.
  for (size_t i = 0; i < mWidth; i++)
  {
    for (size_t j = 0; j < mHeight; j++)
    {
      if (src.mCells[i][j])
      {
        mCells[i][j] = src.mCells[i][j]->clone();
      }
    }
  }
}

void GameBoard::verifyCoordinate(size_t x, size_t y) const
{
  if (x >= mWidth || y >= mHeight)
  {
    throw std::out_of_range("");
  }
}

void swap(GameBoard& first, GameBoard& second) noexcept
{
  using std::swap;

  swap(first.mWidth, second.mWidth);
  swap(first.mHeight, second.mHeight);
  swap(first.mCells, second.mCells);
}

GameBoard& GameBoard::operator=(const GameBoard& rhs)
{
  if (this == &rhs)
  {
  return *this;
  }

  GameBoard temp(this);
  swap(*this, temp);
  return *this;
}

const unique_ptr<GamePiece>& GameBoard::at(size_t x, size_t y) const
{
  verifyCoordinate(x, y);
  return mCells[x][y];
}

unique_ptr<GamePiece>& GameBoard::at(size_t x, size_t y)
{
  return const_cast<unique_ptr<GamePiece>&>(as_const(*this).at(x, y));
}

사용 방법

GameBoard chessBoard(8, 8);
auto pawn = std::make_unique<ChessPiece>();
chessBoard.at(0, 0) = std::move(pawn);
chessBoard.at(0, 1) = std::make_unique<ChessPiece>();
chessBoard.at(0, 1) = nullptr;

템플릿으로 구현한 Grid 클래스

앞선 클래스 정의는 아쉬운 점이 몇 가지 있는데 다음과 같다.