Ch 06. 템플릿
함수 템플릿
void swap(int& x, int& y)
{
int temp = x;
x = y;
y = temp;
}
위 함수는 간단한 swap 함수이다. 하지만 int로 정의되어 있어 다른 자료형의 데이터는 받을 수 없다. 다른 자료형을 받으려면 오버로딩 해야 하는데 매우 번거롭다. 이를 해결하기 위한 툴이 함수 템플릿이다.
template<typename T>
void swap(T& x, T& y)
{
int temp = x;
x = y;
y = temp;
}
int main()
{
int x = 10, y = 20;
swap<int>(x, y); // 타입 명시
swap(x, y); // 타입 추론
위 코드는 함수가 아니라 함수 템플릿이다. 함수를 호출하는 순간에 템플릿을 기반으로 함수가 만들어진다. 템플릿은 타입에 의존하지 않고 하나의 값이 여러 타입을 가질 수 있는 일반화 프로그래밍을 가능하게 한다.
위 코드가 템플릿의 파라메터로 타입을 받았다면, 아래 코드는 템플릿의 파라미터로 값을 받고 있다.
// 템플릿의 파라메터로 int 값을 받는다
template<int N>
int func(int (&nums)[N])
{
return N;
}
// 타입 파라메터와 값 파라메터를 같이 받는 템플릿
template<int N, typename T>
int getSize(T(&nums)[N])
{
return N;
}
int main()
{
{
int nums[] = { 1, 2, 3 };
cout << func<3>(nums) << endl; // 값 명시
cout << func(nums) << endl;
// 3을 추론한다. 즉 배열의 사이즈를 추론을 통해서 구할 수 있다
// 컴파일 타임에 추론하기 때문에 정적 배열만 가능하다
}
{
int nums[] = { 1, 2, 3 };
cout << getSize(nums) << endl;
char chars[] = {'a'};
cout << getSize(chars);
}
}
클래스 템플릿
템플릿을 활용하면 타입에 의존하지 않는 Stack을 구현할 수 있다. 다른 클래스 템플릿으로 클래스 템플릿을 넘겨줄 수 도 있다. Stack → vector
#include <iostream>
#include <vector>
using std::cout;
using std::endl;
template<typename T>
class Stack
{
private:
std::vector<T> _items;
public:
Stack();
void push(T item);
void pop();
T& top();
};
template<typename T>
Stack<T>::Stack()
_items{}
{
}
template<typename T>
void Stack<T>::push(T item)
{
_items.push_back(item);
}
template<typename T>
void Stack<T>::pop()
{
if (_size == 0)
{
throw std::out_of_range("underflow");
}
_items.pop_back();
}
template<typename T>
T& Stack<T>::top()
{
if (_size == 0)
{
throw std::out_of_range("underflow");
}
return _items.back();
}
템플릿 특수화
특정 클래스나 타입에 대해서만 템플릿 함수를 특수화 할 수 있다.
template <typename T>
void swap(T& x, T& y)
{
std::cout << "swap" << std::endl;
T temp = x;
x = y;
y = temp;
}
class Foo
{
};
template <>
void swap<Foo>(Foo& x, Foo& y) // 명시적 특수화, swap 뒤의 <Foo> 를 지우면 암시적 특수화
{
std::cout << "swap<Foo>" << std::endl;
}
// Foo class가 swap을 했을 때 위에 있는 특수화된 템플릿으로 들어가게 된다.
포인터에 대한 특수화, 포인터가 가르키는 대상끼리의 값을 변경
template <typename T>
void swap(T* x, T* y) // 포인터에 대한 특수화, 오버로딩으로 봐야함 *와 &는 다르기 때문
{
std::cout << "swap pointer" << std::endl;
T temp = *x;
*x = *y;
*y = temp;
}
클래스 템플릿 특수화
// 템플릿 특수화
template<typename T, typename S>
class Test
{
public:
T num0;
S num1;
};
// 완전 특수화
template<>
class Test<int, float> // 타입이 정해져 있음
{
};
// 부분 특수화, 하나의 타입만 특수화 할 경우
template<typename T>
class Test<T, int>
{
};
int main()
{
Test<int, int> t0;
Test<int, float> t1;
Test<float, int> t2;
// bool을 그대로 사용하면 효율이 떨어지기 때문에 vector에서 특수화를 사용한다
std::vector<bool> vb;
}
템플릿 구체화
템플릿은 구체화 하기 전까지 함수도, 클래스도 아니다. 형판에 불과하다.
template
void swap<int>(int&, int&); // 명시적 구체화
swap(x, y); // 암시적 구체화, <int> 가 생략됨
템플릿의 헤더파일과 소스코드를 분리할 경우
template<typename T>
void swap(T& x, T& y);
#include "swap.h"
template<typename T>
void swap(T& x, T& y)
{
T temp = x;
x = y;
y = temp;
}
#include <iostream>
#include "swap.h"
using std::cout;
using std::endl;
int main()
{
int x = 10, y = 20;
swap(x, y); // 암시적 구체화
}
위 프로젝트는 제대로 링킹이 되지 않는다. 왜냐하면 swap.cpp파일의 템플릿이 구체화되지 않았기 때문에 기계어로 바뀌지 않았고, 헤더파일에서 구현을 링크하지 못하기 때문이다. 그래서 이렇게 분리할 경우 cpp파일에 사용할 타입을 모두 구체화해주어야 한다.
#include "swap.h"
template<typename T>
void swap(T& x, T& y)
{
T temp = x;
x = y;
y = temp;
}
template
void swap<int>(int&, int&); // 명시적 구체화 되어 기계어로 바뀜
하지만 사용할 모든타입의 명시적 구체화는 너무 비용이 크기 때문에 선언과 정의를 분리하지 않고 다음과 같이 헤더파일에 한 번에 선언과 정리를 둔다.
template<typename T>
void swap(T& x, T& y);
template<typename T>
void swap(T& x, T& y)
{
T temp = x;
x = y;
y = temp;
}
이렇게 되면 헤더파일만 include해도 정의를 찾을 수 있기 때문에 링크가 된다.
가변 인자
가변 인자는 인자의 개수가 정해지지 않은것을 말한다.
printf("%d %d %d", a, b, c); // 인자의 수가 정해지지 않음
C style 가변인자
#include <iostream>
#include <cstdarg>
using std::cout;
using std::endl;
// C 스타일 가변인자
int sum0(int count, ...) // 가변인자는 ...으로 표시한다.
{
int result = 0;
va_list args;
va_start(args, count);
for (int i = 0; i < count; ++i)
{
result += va_arg(args, int); // 스택으로부터 int의 크기만큼씩 읽는다.
}
va_end(args);
return result;
}
int main()
{
cout << sum0(4, 10, 20, 30, 40) << endl; // 100
}
템플릿 가변인자
template<typename T>
T sum1(T value) // unpack에 마지막 인자가 남았을 때
{
return value;
}
// C++ 스타일 가변인자
template<typename T, typename... Args>
T sum1(T value, Args... args) // args에 나머지 인자가 모두 들어가게 된다.
{
return value + sum1(args...); // unpack
}
int main()
{
cout << sum1(10, 20, 30, 40) << endl; // 100
}
템플릿 메타 프로그래밍
템플릿을 사용하여 컴파일 타임에 코드를 생성하고 계산을 수행하는 프로그래밍 기법이다. 런타임에 불필요한 계산을 피해서 실행속도를 향상시킨다.
Template Meta Programming (TMP)를 사용하지 않은 팩토리얼 함수
#include <iostream>
using std::cout;
using std::endl;
int factorial(int value)
{
if (value == 1)
return value;
return value * factorial(value - 1);
}
TMP를 사용한 팩토리얼 함수
template<int N>
struct Factorial
{
static const int value = N * Factorial<N - 1>::value; // 재귀
};
// 특수화를 통해 재귀 탈출
template<>
struct Factorial<1>
{
static const int value = 1;
};
TMP를 사용하면 컴파일 타임에 결정되기 때문에 변수를 넣어 줄 수 없다.
TMP를 사용하지 않은 피보나치수열
int fib(int value)
{
if (value <= 1)
return value;
return fib(value - 1) + fib(value - 2);
}
TMP 사용한 피보나치수열
template<long long N>
struct Fib
{
static const long long value = Fib<N - 1>::value + Fib<N - 2>::value;
};
template<>
struct Fib<1>
{
static const long long value = 1;
};
template<>
struct Fib<0>
{
static const long long value = 0;
};
int main()
{
cout << factorial(5) << endl;
cout << Factorial<5>::value << endl;
// 120이 컴파일 타임에 계산이 된다, 변수를 입력값으로 넣어줄 수 없다
cout << fib(6) << endl;
cout << Fib<6>::value << endl;
//cout << fib(50) << endl; // 느리다
cout << Fib<50>::value << endl;
}
Ch 07. 타입
type traits
정적타임에 타입을 핸들링하는 방법
#include <type_traits>
#include <iostream>
using namespace std;
int main()
{
cout << std::boolalpha; // bool을 true, false로 표현
cout<<std::is_pointer<int>::value<<endl; // pointer라면 true를 리턴
return 0;
}
is_pointer 구현
template<typename T>
struct is_pointer
{
static const bool value = false;
};
template<typename T>
struct is_pointer<T*>
{
static const bool value = true;
};
int main()
{
cout << is_pointer<int*>::value << endl;
cout << is_pointer<int>::value << endl; // 템플릿 특수화로 인해 true
}
is_pointer 활용 예시
template<typename T>
void foo(T t)
{
cout << is_pointer<T>::value << endl;
} // 어떤 타입인지 알 수 있다.
int main()
{
int num = 0;
int* pNum = #
foo(num);
foo(pNum);
}
// std::is_pointer<int>::value = std::is_pointer_v<int>
add_pointer : 타입에 포인터를 추가한다.
int num = 0;
std::add_pointer<int>::type pNum = # // int* pNum = &num
foo(pNum); // true
// std::add_pointer<int>::type = std::add_pointer_t<int>
add_pointer 구현
template<typename T>
struct add_pointer
{
using type = T*;
};
template<typename T>
struct add_pointer<T&>
{
using type = T*;
};
remove_pointer : 포인터를 제거한다.
std::remove_pointer<int*>::type n = *pNum; // int n
foo(n); // false
remove_pointer 구현
template<typename T>
struct remove_pointer
{
using type = T;
};
template<typename T>
struct remove_pointer<T*>
{
using type = T;
};
unscoped, scoped enum
enum Unscoped
{
A, B
};
enum class Scoped : long long
{
A, B = 10000000000
};
int main()
{
cout<<static_Cast<int>(Scoped::B); // 오류 발생
return 0;
}
타입을 알아내기 위해 underlying_type을 사용한다.
cout << static_cast<std::underlying_type<Scoped>::type>(Scoped::B) << endl;
ostream 오버로딩으로 간단하게 출력해 보기
template<typename T>
std::ostream& operator<<(std::ostream& os, const T& value)
{
// underlying_type 을 통해 타입을 알아냄
using t = std::underlying_type<T>::type;
cout << static_cast<t>(value);
return os;
}
scoped enum만 함수의 파라미터로 받기 위해 is_scoped를 구현
template<typename T>
struct is_scoped_enum
{
// enum 인데 int로 변환이 되지 않으면 Scoped
static const bool value = std::is_enum<T>::value && !std::is_convertible<T, int>::value;
// enum이면서 int로 변환이 되지 않는 enum 이면 true
};
enable_if
std::enable_if<bool, T>::type
// bool 이 true 일 때만 T가 정의 된다. 정의 되지 않으면 다른 오버로딩 함수를 찾는다.
// 이런 기법을 SFINAE(Substitution failure is not an erro) 라고 한다.
enable_if와 is_scoped를 이용하여 scoped enum만 받는 함수 오버로딩
// SFINAE(Substitution failure is not an erro) 이용
template<typename T, typename std::enable_if<is_scoped_enum<T>::value, int>::type = 0>
std::ostream& operator<<(std::ostream& os, const T& value)
{
// underlying_type 을 통해 타입을 알아냄
using t = typename std::underlying_type<T>::type;
cout << static_cast<t>(value);
return os;
}
RTTI
런 타임에 어떤 타입을 결정해 주는 메커니즘 Run Time Type Identification
#include <typeinfo>
typeid(T).name(); // 타입 이름 반환
RTTI는 기본적으로 켜져 있지만 설정에서 끄게 된다면 dynamic_cast나 typeid() 같은 RTTI를 사용하는 함수에서 오류가 날 수 있다. (런타임 중에 타입이 결정되는 경우)
RTTI의 타입정보는 가상함수 테이블 (V table)에 있기 때문에 가상함수가 없어서 V table이 만들어지지 않는다면 제대로 작동하지 않는다.
'C & C++ > C++' 카테고리의 다른 글
[C++] Modern C++ (0) | 2024.08.26 |
---|---|
[C++] STL (0) | 2024.08.20 |
[C++] 예외 처리 (0) | 2024.08.08 |
[C++] 형변환 (0) | 2024.08.07 |
[C++] 상속 (0) | 2024.08.05 |