6 minute read

‘전문가를 위한 C++ - Marc Gregoire 지음, 남기혁 옮김’ 책을 참고하여 작성한 포스트입니다.


제네릭 프로그래밍(generic programming)의 목적은 재사용 가능한 코드를 작성하는 것이다. 이를 지원하기 위한 c++의 핵심 기능은 템플릿(template)이다.

템플릿 소개

  • 값 뿐만 아닌 타입(type)에 대해서도 매개변수화할 수 있게 해주는 것이 템플릿이다.

클래스 템플릿(class template)

  • 클래스 템플릿은 멤버 변수의 타입, 메서드의 리턴 타입, 메서드의 매개변수 타입 등을 매개변수로 받아 클래스를 정의한다.
  • 주로 컨테이너나 데이터 구조에서 많이 사용된다.

클래스 템플릿 작성법

  • 클래스 템플릿을 이용하면 클래스가 특정한 타입에 종속되지 않게 만들 수 있다.
  • 원하는 타입에 맞게 이 클래스를 인스턴스화해 사용하면 된다.
  • 이를 제네릭 프로그래밍이라 한다.
  • 제네릭 프로그래밍의 가장 큰 장점은 타입 안전성이다.
  • 다형성을 이용하면 추상 베이스 클래스로 정의해야 하고, 자식 클래스의 고유 기능을 활용하려면 다운캐스트 해야 한다 (나중에 정리)

Grid 클래스 정의

  • int 같은 기본 타입도 지원하려면 값 전달 방식으로 구현하는 것이 좋다.
  • 값 전달 방식은 포인터 기반과 달리 완전히 빈 셀을 만들 수 없어, 컨테이너 구현 시 std::optional 을 이용해주자.

템플릿 지정자(template specifier)

template <typename T>
  • 템플릿은 타입을 매개변수로 받는다.
  • T가 템플릿 매개변수 이름이다.
  • typename 대신 class 로 이름을 바꾸어도 된다.
  • 보드 컨테이너를 에 대한 vector의 vector로 정의하면 컴파일러가 생성해주는 복제 생성자와 대입 연산자로 충분하다.
  • 포인터에 대한 vector의 vector로 정의했다면 직접 포인터 관리 코드를 구현해 주어야 한다.
  • 클래스 내에서는 Grid를 Grid로 처리하지만, 명시적으로 적어줘도 된다.
  • 클래스 정의 밖에서는 반드시 Grid로 적어 주어야 한다.
  • Grid는 템플릿 이름이지 클래스 이름은 아니다.
  • Grid 클래스 템플릿으로 인스턴스화한 실제 클래스를 가리킬 때는 템플릿 ID인 Grid로 표현해야 한다.

Grid 클래스 메서드 정의

  • 메서드 정의할 때 반드시 템플릿 지정자인 template 를 앞에 적어주어야 한다.
template <typename T>
Grid<T>::Grid(size_t Width, size_t Height)
	: m_Width {Width}, m_Height {Height}
{
	m_Cells.resize(m_Width);
	for (auto& Column : m_Cells)
	{
		Column.resize(m_Height);
	}
}
  • 클래스 템플릿의 메서드 정의는 모든 사용자가 볼 수 있어야 하므로 보통 클래스 템플릿 정의와 같은 파일에 적는다.(뒤에서 여러 파일로 나누는 방법 알려줌)
  • 메서드나 static 데이터 멤버를 정의하는 코드는 반드시 클래스 이름을 Grid와 같이 표기해야 한다.
  • T{}는 해당 객체의 디폴트 생성자를 호출한다.

Grid 템플릿 사용법

  • 클래스 템플릿에 특정한 타입을 지정해서 구체적인 클래스를 만드는 것을 템플릿 인스턴스화(template instantiation)라고 한다.
  • 포인터 타입 객체도 저장 가능하다.
  • 다른 템플릿 타입도 지정 가능하다.
  • 프리스토어에 동적 생성도 가능하다.
// 'use of class template requires template argument list' error
Grid Test; 

// 'too few template arguments' error
Grid<> Test;


컴파일러에서 템플릿을 처리하는 방식

  • 컴파일러는 템플릿 메서드 정의 코드를 발견하면 컴파일 하지 않고 문법 검사만 한다. 어떤 타입일지 몰라서!
  • 이후 인스턴스화하는 코드를 발견하면 주어진 타입에 대한 인스턴스를 생성한다.
  • 특정한 타입에 대해 인스턴스화를 전혀하지 않으면 컴파일되지 않는다.

선택적 인스턴스화

  • 암묵적인 클래스 템플릿 인스턴스화(implicit class template instantiation) 코드는 다음과 같은 코드다.
Grid<int> MyIntGrid;
  • 컴파일러는 위 코드를 보면 그 클래스 템플릿에 있는 가상 메서드에 대한 코드를 생성한다.
  • 하지만 virtual로 선언하지 않은 메서드는 코드에서 실제로 호출하는 것만 컴파일 한다!
  • 게터, 세터, 복제 생성자, 대입 연산자 등의 메서드들을 사용하지 않으면 해당 코드는 생성하지 않는데, 이를 선택적 인스턴스화(selective instantiation)라고 한다.
  • 컴파일러가 코드를 컴파일하지 않으면 그 속에 숨은 에러를 못찾는 경우가 있는데,
  • 명시적 템플릿 인스턴스화(explicit template instantiation)를 통해 다음 코드와 같이 해결 가능하다.
  • 비 virtual 메서드에 대해서도 무조건 코드를 생성하게 만든다.
template class Grid<int>;

템플릿에 사용할 타입의 요건

  • 인스턴스화할 때 템플릿 내 연산을 모두 지원해야 하겠죠. 아니면 선택적 인스턴스화로 빗겨나가기.
  • c++20 부터는 콘셉트(concepts) 기능이 추가 되어, 템플릿 매개변수에 대한 요구사항을 컴파일러가 해석하고 검증할 수 있음.


템플릿 코드를 여러 파일로 나누기

  • 컴파일러는 소스 파일을 컴파일 하는 과정에서 클래스 템플릿과 메서드를 사용하는 부분이 나오면 이에 대한 정의 코드를 참조한다.

클래스 템플릿 정의에 메서드 정의 함께 적기

  • 메서드 정의 코드를 클래스 템플릿 정의 파일에 작성하는 방법.
  • 이 파일만 임포트하면 템플릿관련 모든 코드를 참조할 수 있겠죠

메서드 정의를 다른 파일에 적기

  • 클래스 템플릿 정의와 클래스 템플릿의 메서드 정의 코드를 각각 다른 파티션 파일에 적는 방법.

템플릿 매개변수

  • 꺾쇠기호 안에 매개변수 리시트를 지정한다.
  • 타입 대신 디폴트값을 지정해도 됨

비타입 템플릿 매개변수(non-type parameter)

  • 정수계열 타입, 열거 타입, 포인터, 레퍼런스, std::nullptr_t, auto, auto&, auto* 등만 비타입 매개변수로 사용 가능하다.
  • 이를 사용하면 코드 컴파일 전에 값을 알 수 있는 장점이 있다.
template <typename T, size_t WIDTH, size_t HEIGHT>
...

Grid<int, 10, 10> MyGrid;
...
  • 위 코드처럼 하면 기존 정적 배열로 만든 코드도 동적으로 조절 가능해진다.
  • 물론 이 경우 width, height에 무조건 상수가 들어가야 한다.
  • 아니면 const를 통해 상수로 정의한 변수?
  • 또는 리턴 타입을 정확히 지정한 constexpr 함수도 가능하다.
const size_t height = 10;
constexpr size_t width() { return 10; }
Grid<int, width(), height> MyGrid;
  • 이렇게 비타입 템플릿 매개변수를 사용하는 경우, 각 값마다 다른 타입으로 지정되어 서로 호환이 되지 않는 단점이 있다.

타입 매개변수의 디폴트값

  • 생성자와 비슷하게 디폴트값 설정이 가능하다.
  • 오른쪽 끝에서 왼쪽방향으로 중간에 건너뛰지 않게!
  • 그러면 메서드 정의하는 코드에서는 디폴트값을 생략해도 된다.
  • 인스턴스할 때 생략된 부분은 생성자처럼 디폴트값이 대체한다.
  • 모두 생략하더라도 꺾쇠기호는 적어주어야 한다.

생성자에 대한 템플릿 매개변수 유추 과정

  • 컴파일러는 CTAD(class template argument deduction, 클래스 템플릿 인수 추론)를 통해 생성자에 전달된 인수를 보고 템플릿 매개변수를 알아낸다.
pair<int, double> pair1 = {1, 2.3};
auto pair2 = std::make_pair(1, 2.3);
//CTAD
pair pair3 {1, 2.3};

//error
// 초기자가 없음
pair pair4;
  • 클래스 템플릿에 매개변수의 디폴트값을 모두 지정했거나 생성자에서 모든 매개변수를 사용하도록 작성된 경우에만 CTAD가 적용된다.
  • 초기자가 없으면 작동하지 않는다.
  • unique_ptr, shared_ptr 에 대해서는 타입 추론기능이 꺼져 있다.

사용자 정의 유추 방식

  • 템플릿 매개변수 추론 방식을 직접 정의할 수도 있다.
    template_name(params) -> template_deducted;
    

메서드 템플릿(method template)

  • 일반 클래스 안에 정의해도 된다.
  • 클래스 템플릿에 복제 생성자와 대입 연산자를 정의하는데 특히 유용하다.
  • 가상 메서드와 소멸자는 메서드 템플릿으로 만들 수 없다.
  • 복제 생성자와 대입 연산자를 예시로 보자.
Grid(const Grid& src);
Grid& operator=(const Grid& rhs);

template <typename E>
Grid(const Grid<E>& src);

template <typename E>
Grid& operator=(const Grid& rhs);
...

template <typename T>
template <typename E>
Grid<T>::Grid(const Grid<E>& src)
	: Grid { src.GetWidth(), src.GetHeight() }
{
	...
}
  • 컴파일러는 E와 T가 같은 경우 원본 복제 생성자와 복제 대입 연산자를 사용할 것이고,
  • 다른 경우 새로 템플릿화한 복제 생성자와 복제 대입 연산자를 사용한다.
  • 이렇게 정의하면 한 타입의 Grid객체를 다른 타입의 Grid로 복제 가능하다.
  • T와 E를 한 문장에 적으면 안된다.
  • 두 타입은 다르다는 걸 잊지 말자

비타입 매개변수를 사용하는 메서드 템플릿

  • 위와 비슷하게 다른 tyepename을 이용하면 타입이 다른 비타입 매개변수를 사용하는 템플릿 클래스 간에 호환이 가능해진다.

클래스 템플릿의 특수화(template specialization)

  • 특정한 타입에 대해서만 다르게 구현할 수도 있다.
  • c 스타일 스트링을 c++ string으로 변환해서 저장하게 구현해 보자
// 기존의 Grid 템플릿 클래스 include

template<>
class Grid<const char*>
{
	// string으로 저장하는 코드
}
  • 클래스 이름은 특수화할 때만 중복해서 사용 가능하다. 이를 알려주는 것이 template<> 이다.
  • 템플릿 특수화는 상속과는 달라 클래스 전체를 완전히 새로 구현해야 한다.
  • 그래서 메서드 이름과 동작을 일치시킬 필요가 없다.
  • 그리고 특수화 버전의 메서드에는 template<> 구문을 메서드 앞에 적지 않아도 된다.

클래스 템플릿 상속하기

  • 상속한 파생 클래스도 템플릿이어야 한다.
  • 특정 타입으로 인스턴스한 클래스를 상속할 때는 파생 클래스가 템플릿이 아니어도 된다.
  • 곧바로 상속하는 것은 아니고 각 타입에 대한 인스턴스마다 해당 타입에 대한 부모 인스턴스를 상속하는 것이다.
  • 템플릿 상속에 대한 이름 조회 규칙(name lookup rule)에 따르면 부모 클래스 템플릿에 있는 데이터 멤버나 메서드 가리킬 때 this 포인터나 Grid::를 붙여야 한다.
template <typename T>
class GameBoard : public Grid<T>
{
	...
};

앨리어스 템플릿

  • 타입 앨리어스와 typedef를 이용하면 특정 타입을 다른 이름으로 부를 수 있다.
using OtherName = MyClassTemplate<int, double>;

// 타입 매개변수 중 일부만 지정도 가능하다.(alias template)
template <typename T1>
using OtherName2 = MyTemplateClass<T1, double>;



함수 템플릿

  • CTAD도 된다.

함수 템플릿 오버로딩

  • 함수 템플릿도 특수화 가능하다. 하지만 이 경우 특수화한 함수는 오버로딩 결정 과정에 포함되지 않아 예상치 못한 문제가 발생 가능하다.
  • 비템플릿(non-template) 함수로 오버로딩 가능하다.

클래스 템플릿의 프렌드 함수 템플릿

// 전방 선언
template <typename T> class Grid;

template <typename T>
Grid<T> operator+(const Grid<T>& lhs, const Grid<T>& rhs);

template <typename T>
class Grid
{
public:
	friend Grid operator+<T>(const Grid<T>& lhs, const Grid<T>& rhs);
	...
};

함수 템플릿의 리턴 타입

  • 함수 템플릿의 경우 리턴 타입이 매개변수의 타입에 따라 달라질 것이다.
  • 이런 경우 auto 나 decltype(auto)를 사용해주면 된다.

축약 함수 템플릿 구문(c++20)

  • abbreviated function template syntax

변수 템플릿(variable template)

template <typename T>
constexpr T pi { T { 3.1415926543...}};

float piFloat = pi<float>;
auto piLongDouble = pi<long double>;
  • 각각 지정한 타입으로 표현할 수 있는 범위에 가장 가까운 파이값을 구할 수 있다.

콘셉트 (c++20)

Leave a comment