[전문가를 위한 C++] 템플릿으로 제네릭 코드 만들기
‘전문가를 위한 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>;
- 각각 지정한 타입으로 표현할 수 있는 범위에 가장 가까운 파이값을 구할 수 있다.
Leave a comment