템플릿의 두드등장 템플릿은 컴파일 타임에 작동하지만, 매크로와 달리 오류를 확인해줄 수 있다. (왜냐면 컴파일러가 함수를 만들어주니까, 매크로와 달리 컴파일러의 검수가 진행된다)
그렇기에 템플릿은 속도가 빠르지만, 실행할 때마다 생성해주기 때문에 실행 파일이 커질 수 있다. (만약 여러 자료형을 여러개 만들면 다 각자의 함수를 만들어주니까.)
⭐템플릿은 형틀과 같은 기능을 지원한다. (클래스 / 함수를 찍어내는 틀) 즉, 기능은 결정되어 있지만 자료형은 결정되어 있지 않은 상태, 모든 자료형에 해당하는 함수나 클래스를 생성하기 위한 도구를 만드는 문법.
템플릿의 종류
- 함수 템플릿 : 함수를 생성하기 위한 틀
- 템플릿 함수 : “함수 템플릿”으로 생성한 함수.
- 클래스 템플릿 : 클래스를 생성하기 위한 틀
함수 템플릿의 예제 :: 단항 템플릿
template<typename T>
T Add(T Dst, T Src){
return Dst + Src;
}
Add<int>(101.3,20.2); //꺽쇠 안에 명시적인 자료형 선언
함수를 호출한다고는 하긴 하지만, 메타 프로그래밍을 통해서 컴파일러가 함수를 생성하고 메모리에 저장하고 사용하게 된다.
템플릿의 특수화 : 특정 자료형에 한하여 기능을 개별적으로 구현해주는 문법
template<>
const char* Add(const char* Dst, const char* Src){
return Dst + Src; // 문자열용 덧셈으로 가정
}
템플릿의 특수화 조건
- 원본 템플릿이 존재해야 한다.
- 원본 템플릿보다 코드 상 위에 존재하면 안된다 (원본이 먼저 선언되어야 함)
특수화가 진행된 템플릿 함수에 한하여 const 오버로딩이 지원한다.
함수 템플릿의 예제 :: 다항 템플릿
template<typename T1, typename T2, typename T3>
T3 Add(T1 Dst, T2 Src){
return Dst + Src;
}
static 키워드와 함수 템플릿
template<typename T>
T Fuc(T num){
static T sum = 0;
sum += 0;
return sum;
}
이런 경우에는 static 변수 안에 매개변수의 자료형에 따라 따로 메모리를 할당할 수 있다. 그러니까 int sum과 float sum이 구분되어서 메모리에 할당된다는 이야기.
클래스 템플릿
template<typename T>
class CObj{
public :
CObj();
~CObj();
public :
T Add(T Dst, T Src);
private:
T m_X, m_Y;
}
클래스 템플릿의 특수화
template<>
class CObj{
public :
CObj();
~CObj();
public :
const char* Add(const char* Dst, const char* Src);
private:
const char* m_X, m_Y;
}
클래스 템플릿의 부분 특수화
//원본 템플릿
template <typename A, typename B>
class InstanceTemplate
{
public:
void Add(A a, B b) {
std::cout << a + b<<std::endl;
}
};
부분 특수화 템플릿 1
template <>
class InstanceTemplate<int, char *>{};
부분 특수화 템플릿 2
template <typename T>
class InstanceTemplate<T, char *>{};
주의 선언을 하고 몸체를 밖에 구현하게 된다면( 혹은 C++ 파일에 ) 문제가 생기게 된다. 왜냐면 클래스 템플릿이기 때문에 아직 클래스가 아닌데 클래스 쓰듯이 사용하면 안된다.
그래서 밖에 구현하고 싶다면 아래와 같이 사용해야 한다.
template<typename T>
CObj<T>::CObj();
꽤나 번거로운 작업이기 때문에 보통은 헤더 파일 안에 템플릿을 구현하는 경우가 많다. 헤더 파일에 넣게 된다는 것은 공개된다는 뜻과 동일하기 때문에 이 부분도 주의하며 사용해야 한다.
static 키워드와 클래스 템플릿 static 키워드와 함수 템플릿 관계와 동일하게 작동함. 템플릿 클래스 안에 있는 클래스 변수(static 변수)는 다른 자료형 별로 생성되고, 같은 자료형 끼리는 메모리 공간을 공유한다.
클래스 템플릿의 상속
template<typename T>
class CPlayer : CObj<T>{}
위와 같이 진행해야 함.
STL (Standard Template Library)
C++이 표준으로 사용하는 STL을 사용하는 것이지만, 나중에 회사가면 회사 내의 STL이 따로 있을 수도 있다.
그러니, 문법이라고 보는 것은 오해의 소지가 많고, 프로그래머 간의 약속을 템플릿으로 만든 집합체라고 보는 것이 맞다.
템플릿의 일반 사용법
template <typename A, typename B>
class InstanceTemplate
{
public:
void Add(A a, B b) {
std::cout << a + b<<std::endl;
}
};템플릿 특수화 만약 내가 위처럼 A,B를 받는다고는 했지만 그중에서도 double이 하나 들어오면 특별하게 취급하고 싶을 때.
template <typename B>
class InstanceTemplate<double, B> {
public:
void Double(double a, B b) {
std::cout << a * b<<std::endl;
}
};마찬가지로 내가 정한 것만 들어왔으면 할 때
template <>
class InstanceTemplate<char, char> {
public:
void Double(char a, char b) {
std::cout << a <<b << std::endl;
}
};그래도 template<>을 써주긴 해야함.
함수 템플릿
template<typename B>
void coutAll(B _b) {
std::cout << _b << std::endl;
}static 키워드와 함수 템플릿
template<typename T>
T Fuc(T num){
static T sum = 0;
sum += 0;
return sum;
}이런 경우에는 static 변수 안에 매개변수의 자료형에 따라 따로 메모리를 할당할 수 있다. 그러니까 int sum과 float sum이 구분되어서 메모리에 할당된다는 이야기.
템플릿 비대화 해결법
템플릿 비대화 해결법의 핵심은 “타입에 따라 달라지는 부분만 템플릿으로 남기고, 타입과 무관한 로직은 템플릿 밖으로 빼는 것”이다.
1. 타입과 무관한 로직을 일반 함수로 분리
void PrintProcessStart(std::size_t count)
{
std::cout << "Process Start\n";
std::cout << "Count: " << count << "\n";
}
void PrintProcessEnd()
{
std::cout << "Process End\n";
}
template <typename ElementType>
void Process(const std::vector<ElementType>& elements)
{
PrintProcessStart(elements.size());
for (const ElementType& element : elements)
{
element.Update();
}
PrintProcessEnd();
}이렇게 하면 템플릿으로 중복 생성되는 영역이 줄어든다.
2. 비템플릿 베이스 클래스로 공통 부분 분리
class ResourceManagerBase
{
protected:
void LogLoadStart()
{
std::cout << "Load Start\n";
}
void LogLoadEnd()
{
std::cout << "Load End\n";
}
};
template <typename ResourceType>
class ResourceManager : public ResourceManagerBase
{
public:
void Add(const std::string& key, ResourceType resource)
{
LogLoadStart();
resources[key] = std::move(resource);
LogLoadEnd();
}
private:
std::unordered_map<std::string, ResourceType> resources;
};공통 기능은 ResourceManagerBase에 있고, 타입별 저장 구조만 템플릿에 남는다.
3. 명시적 인스턴스화 사용
사용 타입이 정해져 있으면 템플릿 인스턴스화를 한 cpp에서만 만들 수 있다.
헤더:
// ResourceStorage.h
#pragma once
template <typename ResourceType>
class ResourceStorage
{
public:
void Add(ResourceType resource);
};
extern template class ResourceStorage<int>;
extern template class ResourceStorage<float>;cpp:
/// ResourceStorage.cpp
#include "ResourceStorage.h"
template <typename ResourceType>
void ResourceStorage<ResourceType>::Add(ResourceType resource)
{
// 구현
}
template class ResourceStorage<int>;
template class ResourceStorage<float>;extern template의 의미
Storage<int>, Storage<float>는 여기서 인스턴스화하지 마라.다른 cpp에 이미 명시적으로 생성된 버전이 있다.
이 방식은 여러 번역 단위에서 같은 템플릿이 중복 인스턴스화되는 것을 줄이는 데 도움이 된다.
단점:
- 사용할 타입을 미리 알아야 함
- 새 타입을 추가할 때 cpp에도 명시적 인스턴스화를 추가해야 함
- 템플릿의 자유도가 줄어듦
4. 타입 소거 사용
컴파일 타임 타입별 최적화가 꼭 필요 없다면 타입 소거를 사용할 수 있다. (서로 다른 구체 타입을 공통 인터페이스로 감싸서, 사용하는 쪽에서 실제 타입을 몰라도 다룰 수 있게 하는 방식)
타입 소거 방식:
void AddCallback(std::function<void()> callback)
{
callbacks.push_back(std::move(callback));
}std::function을 쓰면 함수 자체는 비템플릿이 된다. 대신 런타임 간접 호출 비용이나 작은 할당 비용이 생길 수 있다.
즉:
템플릿
- 컴파일 타임 최적화 유리
- 타입별 코드 생성 가능
- 코드 비대화 가능
타입 소거
- 코드 중복 감소 가능
- 런타임 간접 호출 비용 가능
5. 가상 함수 기반 인터페이스 사용
타입마다 컴파일 타임에 분기할 필요가 없다면 런타임 다형성을 쓸 수 있다.
class IUpdatable
{
public:
virtual ~IUpdatable() = default;
virtual void Update(float deltaTime) = 0;
};void UpdateAll(std::vector<IUpdatable*>& objects, float deltaTime)
{
for (IUpdatable* object : objects)
{
object->Update(deltaTime);
}
}템플릿으로 UpdateAll<Player>, UpdateAll<Enemy>, UpdateAll<NPC>를 각각 만들지 않고, 하나의 비템플릿 함수로 처리할 수 있다.
단점은 가상 함수 호출 비용, 인라이닝 어려움, 캐시 효율 저하 가능성이다.
6. 템플릿 파라미터 수 줄이기
정말 컴파일 타임에 달라져야 하는 것만 템플릿 파라미터로 두는 게 좋다.
예를 들어 LoggerType이 성능상 컴파일 타임 결정이 필요 없다면 인터페이스 포인터나 일반 객체 참조로 받을 수 있다.