template(템플릿)
모두의 코드 내용을 공부하고 정리한 내용입니다.
template
- 클래스 탬플릿에 인자를 전달해서 실제 코드를 생성하는 것을 클래스 템플릿 인스턴스화라 한다.
- 인스턴스화 되지 않는다면 컴파일시 아무런 코드로 변환하지 않는다.
- 인스턴스화가 되어야만 컴파일러가 실제 코드를 생성한다.
- 클래스 템플릿과 달리 함수 템플릿를 호출할 때<> 없이 컴파일이 된다 인수의 타입을 보고 알아서 <> 작동시켜 인스턴스화를 해준다.
- 전달되는 인자를 보고 컴파일러가 타입을 정한다.
템플릿 특수화
- 어떤 특정한 상황에서 별도로 작업을 수행하기 위해 특별히 따로 템플릿을 만들어줌
template <typename A, typename B, typename C>
class test {};
- 여기서 A가 int형이고 C가 double형일 때 다른 방식으로 처리하고 싶다면
template <typename B>
class test<int, B, double> {};
- 위 처럼 특수화를 시키기 특수화 하려는 타입을 전달하고 위에는 일반적인 템플릿을 받으면 된다.
- 만약 템플릿 인자가 없더라도 특수화를 하고 싶으면 template<>빈칸 이라도 남겨줘야한다
포인터에 대한 템플릿 특수화
- 포인터로 구체화할 때 특수하게 간접 참조 값을 출력하고 싶을 때 사용
- 포인터 타입들로 구체화할 때 다르게 동작
template <typename T>
class A
{
private:
T data;
public:
A(const T& create) : data(create) {}
void print()
{
cout << data << endl;
}
};
template <typename T>
class A<T*>
{
private:
T* data;
public:
A(T* create) : data(create) {}
void print()
{
cout << *data << endl;
}
};
int main()
{
A<int> a_int(777);
a_int.print();
int tmp = 369;
A<int*> a_int_ptr(&tmp); // 포인터에 대한 템플릿 특수화를 사용하지 않으면 주소값이 출력됨됨
a_int_ptr.print();
}
class A<T*>
는T*
에 대한 특수화여서 모든 타입의 포인터 타입에 대한 특수화를 가르킨다.- 따라서 T를 생략하면 안되기에 제대로 명시해줘야 한다.
타입이 아닌 템플릿 인자 (non-type template arguments)
- 템플릿 인자로 타입만 받을 수 있는 것은 아니다.
template <typename T, int num>
T add_num(T t) {
return t + num;
}
int main() {
int x = 3;
std::cout << "x : " << add_num<int, 5>(x) << std::endl;
}
- template 인자로 T를 받고, 추가적으로 함수의 인자처럼
int num
을 또 받고 있다.
- 타입이 아닌 템플릿 인자로 전달할 수 있는 타입들은 제한되어있다.여기 참조
- 정수 타입
- 포인터 타입
- enum 타입
- 널포인터터
디폴트 템플릿 인자
- 함수에 디폴트 인자를 지정하는 것 처럼 템플릿도 디폴트 인자를 지정할 수 있다.
template <typename T, int num = 5> // num에 디폴트로 5가 전달된다.
T add_num(T t) {
return t + num;
}
int main() {
int x = 3;
std::cout << "x : " << add_num(x) << std::endl;
}
- 또한 타입도 디폴트로 지정이 가능하다.
template <typename T, typename C = MyTemplateClass<T>>
T Func(T a, T b){ ... }
- C에 디폴드 인자로
MyTemplateClass<T>
가 전달된다.
가변 길이 템플릿
- 임의의 개수의 인자를 받는 함수를 만들 수 있다.
template <typename T>
void print(T arg) {
std::cout << arg << std::endl;
}
template <typename T, typename... Types>
void print(T arg, Types... args) {
std::cout << arg << ", ";
print(args...);
}
int main() {
print(1, 3.1, "abc");
print(1, 2, 3, 4, 5, 6, 7);
}
- typename 뒤에
...
으로 오는 것을 parameter pack이라고 한다.- 템플릿 파라미터 팩, 함수 파라미터 팩
- 0개 이상의 템플릿 인자와 0개 이상의 함수 인자를 표현
- 차이점은 템플릿 경우 타입 앞에
...
이 오고, 함수의 경우 타입 뒤에...
이 온다.
- 차이점은 템플릿 경우 타입 앞에
- 위 print 템플릿 함수 예시로
printf(1, 2.2, "3");
을 호출하면 인자가 여러개여서 가변 길이 템플릿 함수가 호출되고, - T에는 첫 번째 매개변수인 1의 int타입 추론되고 arg는 1이 된다.
- 그리고 나머지는 뒤의 args가 된다.
- 그 후 재귀적으로 다시 함수가 호출되면서 args값이 전달된다.
- 만약 인자가 1개이면 c++규칙상 파라미터 팩이 없는 함수의 우선순위가 더 높아서 평범한 함수가 호출된다.
여기서 print 함수의 위치를 바꾸면 컴파일 오류가 발생!!
template <typename T, typename... Types>
void print(T arg, Types... args) {
std::cout << arg << ", ";
print(args...);
}
template <typename T>
void print(T arg) {
std::cout << arg << std::endl;
}
int main() {
print(1, 3.1, "abc");
print(1, 2, 3, 4, 5, 6, 7);
}
- 이유는 C++ 컴파일러는 함수를 컴파일 시 자신의 앞에 정의되어 있는 함수들 밖에 보지 못하기 때문이다.
- 그래서
void print(T arg, Types... args)
를 컴파일할 때void print(T arg)
함수의 존재를 모른다.
- 그래서
- 하지만 이런 가변 길이 템플릿은 아주 편리하지만 1가지 단점인 재귀 함수 형태로 구성해야 한다는 점이다.
- 재귀를 종료시킬 함수를 따로 만들어 줘야한다.
- 귀찮고 복잡해짐
- 재귀를 종료시킬 함수를 따로 만들어 줘야한다.
- C++17에 생긴 Fold형식을 사용하면 재귀형식을 사용하지 않고 간단하게 표현할 수 있다.
Fold expressions
template <typename... Ints>
int sum_all(Ints... nums) {
return (... + nums);
}
return (... + nums);
이것이 Fold형식으로, 컴파일은return ((((1 + 4) + 2) + 3) + 10);
으로 해석한다.- 위 형태를 단항 좌측 Fold (Unary left fold)라고 한다.
- C++17에는 4가지 형태의 Fold 표현식이 존재한다. init는 초기값 pack은 파라미터 펙으로 전달된 인자.
- op에는 이항 연산자들이 들어가고 1가지 중요한 점은 ()로 감싸주지 않으면 컴파일 오류가 난다.
- 위 표의 4번째, 즉 이항 좌측 Fold의 예를 알아보면
template <typename Int, typename... Ints>
Int diff_from(Int start, Ints... nums) {
return (start - ... - nums);
}
- start가 초기값이고 nums가 파라미터 팩 부분이다.
개인 공부 기록용 블로그입니다.
틀린 부분 있으다면 지적해주시면 감사하겠습니다!!
댓글남기기