
C++ 클래스 템플릿이란?
템플릿은 타입에 구애받지 않고 재사용 가능한 코드를 작성하게 한다.
특히 클래스 템플릿은 C++ 프로그래밍에서 코드 재사용성과 타입 안전성을 높이는 도구이다.
클래스 템플릿은 다양한 타입에 대해서 동일한 구조와 기능을 제공하는 클래스를 정의하는 방법.
클래스 템플릿의 매개변수는 클래스의 멤버 변수 타입, 메서드의 매개 변수나 반환 타입을 결정한다.
게임 개발에서는 기획서에 정의된 데이터 처리나 유사한 기능을 가진 게임 객체 관리를 위해서 자주 사용된다.
클래스 템플릿 선언
클래스 템플릿은 일반 함수 템플릿과 유사한 방식으로 선언한다.
// C++14
template<typename T>
class ObjectMgr { ... }
// C++17
template<class T>
class ObjectMgr { ... }
template<typename T> 또는 template<class T> 를 사용하는데,
일단 이게 템플릿임을 선언하는 키워드 이다.
T는 템플릿 매개변수로, 실제 클래스 사용 시에 구체적인 타입으로 대체된다.
C++14와 C++17에서는 typename과 class 키워드가 이 문맥에서 완전히 동일한 의미를 가진다.
템플릿 선언부에서 T는 "타입 매개변수(type parameter)"라고 하고,
클래스 내부에서 실제 타입처럼 사용한다.
컴파일러는 해당 선언을 보고서 템플릿을 사용하는 각 타입마다
별도의 클래스 코드를 생성할 준비를 한다.
#include <vector>
#include <iostream>
using namespace std;
template<typename T>
class ObjectMgr
{
private:
vector<T> objects;
public:
void addObjects(T const & entity);
T const& getGameObject(size_t idx);
};
vector<T> objects 는 템플릿 파라미터 T타입의 객체들을 저장하는 벡터이다.
이는 ObjectMgr 클래스가 여러개의 T 타입 객체를 관리할 수 있게 한다.
void addObjects(T const & entity) 는 T 타입의 상수 참조를 매개변수로 받아
벡터에 추가하는 메서드이다. const &를 사용해서 객체의 복사본을 만들지 않고
효율적으로 참조만 하면서도 원본 데이터 변경을 방지한다.
T const& getGameObject(size_t idx)는 특정 인덱스의 T타입 객체를 상수 참조로
반환해주는 메서드이다. 반환타입 T const &는 반환 객체를 수정할 수 없도록 하면서
불필요한 복사를 방지하도록 유지하는 것.
즉, 정리하자면 해당 클래스 템플릿은 어떤 타입이든 저장하고
검색할 수 있는 범용 컨테이너 역할을 한다.
현재 클래스 내부에서는 선언만 있고 정의는 없는 상태로 실제 구현은 클래스 외부에서 이뤄진다.
클래스 템플릿 형식 한정자(Qualifier)
클래스 템플릿의 멤버 함수를 클래스 외부에 정의할 때는 클래스 템플릿 형식 한정자를 사용한다.
template<typename T>
void ObjectMgr<T>::addObjects(T const& entity)
{
objects.push_back(entity);
std::cout << "createObjects datatype " << std::endl;
}
template<typename T>
T const& ObjectMgr<T>::getGameObject(size_t idx)
{
std::cout << "getGameObject datatype " << std::endl;
return objects.at(idx);
}
여기서 벡터를 통해 멤버 변수에 새 객체를 추가하는 기능도 있지만
다른 걸 눈여겨 볼것은 getGameObject 메서드이다.
getGameObject 메서드는 return objects.at(idx)를 통해
벡터의 특정 인덱스에 있는 객체를 반환한다.
.at(idx)는 범위 검사를 수행하므로 유효하지 않은 인덱스에 접근할 경우
std::out_of_range 예외가 발생한다.
해당 부분은 objects[idx] 보다 안전하지만 약간의 성능 오버헤드가 있다.
두 메서드 모두 템플릿 인자 T에 따라 다른 타입을 처리할 수 있고,
이 형식 한정자가 없으면 컴파일러는 이 정의가 어떤 클래스의 멤버함수인지 구분할 수 없다.
템플릿 인스턴스화 과정
템플릿 인스턴스화는 컴파일러가 템플릿에 구체적인 타입을 적용하여 실제 클래스 코드를 생성하는 과정이다.
int main()
{
ObjectMgr<int> intObjectMgr;
ObjectMgr<std::string> stringObjectMgr;
intObjectMgr.addObjects(10);
stringObjectMgr.addObjects("가나다");
auto intVal = intObjectMgr.getGameObject(0);
auto stringVal = stringObjectMgr.getGameObject(0);
std::cout << intVal << std::endl;
std::cout << stringVal << std::endl;
}
해당 코드에서 중요한 점은 동일한 템플릿 코드가 서로 다은 타입(int 와 string)에 대해
완전히 다른 클래스를 생성하고, 이 두 클래스는 컴파일 시점에 독립적으로 생성되며,
타입 안전성이 보장된다는 것이다.
템플릿 인스턴스화의 특징
1. 지연 인스턴스화 :
템플릿 클래스는 실제로 사용될 때만 인스턴스화를 하는데,
특히 클래스 템플릿의 멤버 함수들은 실제로 호출될 때만 인스턴스화 된다.
2. 코드 생성 : 컴파일러는 각 타입별로 별도의 클래스 코드를 생성한다.
따라서 ObjectMgr<int>와 ObjectMgr<std::string>은 서로 다른 클래스이다.
3. 타입 안전성 : 각 인스턴스화된 클래스는 지정된 타입에 대해서 타입 안전성을 보장한다.
4. 컴파일 시간 최적화 : 템플릿 인스턴스화는 컴파일 시간에 이루어져 런타임 오버헤드가 없다.
클래스 템플릿의 고급 활용
중첩 템플릿 사용
클래스 템플릿은 다양한 방식으로 활용할 수 있다.
void exampleObjectMgr(ObjectMgr<int> const& mgr)
{
using objController = ObjectMgr<int>; // 타입 별칭 정의
ObjectMgr<int> arrayObjectMgr[3]; // 템플릿 클래스 배열
objController arrayObjectController[3]; // 별칭을 사용한 동일 타입 배열
ObjectMgr<int*> ptrArrayObjectMgr[10]; // 포인터 타입을 템플릿 인자로 사용
// 중첩 템플릿: ObjectMgr<int>를 관리하는 ObjectMgr
ObjectMgr<ObjectMgr<int>> dObjectMgr;
// 객체 추가 및 계층 구조 형성
dObjectMgr.addObjects(arrayObjectMgr[0]);
dObjectMgr.addObjects(arrayObjectMgr[1]);
dObjectMgr.addObjects(arrayObjectMgr[2]);
auto entityObjectmgr = dObjectMgr.getGameObject(0);
entityObjectmgr.addObjects(100);
auto entity = entityObjectmgr.getGameObject(0);
std::cout << "entity value : " << entity << std::endl;
}
클래스 템플릿의 특수화
특정 타입에 대해 다른 구현을 제공하고 싶을 때 템플릿 특수화를 사용할 수 있다.
// int 타입에 대한 특수화
void ObjectMgr<int>::addObjects(int const& entity) {
objects.push_back(entity);
std::cout << "createObjects int datatype " << std::endl;
}
해당 코드는 ObjectMgr 템플릿의 addObjects 메서드에 대한 int 타입 특수화를 제공한다.
템플릿 선언부가 int 타입으로 구체화 되었기 때문인데,
일단 ObjectMgr<int> 한정자는 정확히 int 타입으로 인스턴스화된 ObjectMgr에서만
해당 메서드가 적용된다.
이런 특수화는 특정 타입에 대해서 최적화된 구현이나 특수 처리가 필요할때 좋다.
예를들어 int 타입에 대해서는 메모리 최적화나 특별한 연산을 적용할 수 있다.
특수화를 통해 다형성 없이도 타입별 다른 동작을 구현할 수 있기 때문.
템플릿 파라미터에 기본값 설정
템플릿 파라미터에 기본값을 설정할 수 있다.
template<typename T, typename Objlist = std::vector<T>>
class ObjectMgr {
private:
Objlist objects;
public:
void addObjects(T const& entity);
T const& getGameObject(size_t idx);
};
이에 맞게 메서드 정의도 수정해야 한다.
template<typename T, typename Objlist>
void ObjectMgr<T, Objlist>::addObjects(T const& entity) {
objects.push_back(entity);
std::cout << "createObjects datatype " << std::endl;
}
template<typename T, typename Objlist>
T const& ObjectMgr<T, Objlist>::getGameObject(size_t idx) {
std::cout << "getGameObject datatype " << std::endl;
return objects.at(idx);
}
auto와 decltype을 이용한 템플릿 파라미터 처리(C++17)
C++17부터는 템플릿 파라미터로 auto를 사용할 수 있다.
template<typename T, auto chunkNum>
class ObjectMgrExt
{
public:
using chunkNumType = decltype(chunkNum);
private:
std::array<T, chunkNum> objects;
chunkNumType objectNum;
public:
void addObjects(T const& entity);
T const& getGameObject(size_t idx);
auto getObjectNum() {
return objectNum;
}
};
int main()
{
ObjectMgrExt<std::string, 30> objectMgrExt;
auto numValue = objectMgrExt.getObjectNum();
std::cout << numValue << std::endl;
}
해당 템플릿은 두가지 템플릿 매개변수를 사용하고, auto chunkNum은
비타입 템플릿 매개변수(non-type template parameter)로, 컴파일 시점에 결정되는 값.
using chunkNumType = decltype(chunkNum)은
decltype 키워드를 사용해서 chunkNum의 타입을 추출하고 이를
chunkNumType이라는 별칭으로 정의한다.
decltype은 참고로 주어진 표현식의 타입을 결정하는 연산자이다.
즉, chunkNum의 정확한 타입(Int, size_t)에 관계 없이 동작한다.
자세한 내용은 https://likeitit.tistory.com/152 글을 참고하길 바란다.
auto getObjectNum(): 참고로 함수 반환타입으로 auto를 사용하는건 C++ 14부터 가능해졌다.
함수가 objectNum을 반환하므로 반환타입은 chunkNumType으로 자동 추론된다.
auto numValue = objectmgrExt.getObjectNum()은 반환값의 타입을
자동으로 추론해서 numValue 변수에 저장한다.
클래스 템플릿 상속
클래스 템플릿은 상속을 통해 확장할 수도 있다.
// 기본 템플릿 클래스
template<typename T, auto chunkNum>
class ObjectMgrExt { ... };
// 템플릿 클래스를 상속받는 템플릿 클래스
template<typename T, auto chunkNum>
class BulletObjMgr : public ObjectMgrExt<T, chunkNum> { ... };
// 인스턴스화된 템플릿 클래스를 상속
class BulletObjectMgrExt : public ObjectMgrExt<int, 20> { ... };
첫번째 클래스인 ObjectMgrExt는 기본 템플릿 클래스로
두개의 템플릿 매개변수(T와 chunkNum)를 가지고 있다.
두번쨰 클래스 BulletObjMgr은 템플릿 클래스를 상속받는 또다른 템플릿 클래스이다.
template<typename T, auto chunkNum> 부모 클래스와 동일한 템플릿 매개변수를 사용.
public ObjectMgrExt<T, chunkNum>은 부모클래스와 동일한 매개변수를 사용하겠다는 것.
즉, 해당 패턴은 부모클래스의 기능을 확장하면서도 유연성까지 가지고 가겠다는 뜻이다.
세번째 클래스 BulletObjectMgrExt는 인스턴스화된 템플릿 클래스를 상속한다.
public ObjectMgrExt<int, 20> 부모 클래스를 int 타입과 20이라는 구체적인 값으로 인스턴스화 시킨다.
이는 더이상 템플릿 클래스가 아니라 구체적인 클래스이다.
해당 접근 방식은 특정 타입과 값에 특화된 클래스를 만들때 유용.
템플릿 상속 관련 특징
1. 템플릿 클래스를 상속하는 파생클래스도 템플릿일 수 있다.
2. 인스턴스화된 템플릿 클래스를 상속하는 파생 클래스는 더이상 탬플릿이 아니다.
3. 일반적인 상속 규칙(가시성, 가상함수 등)이 모두 적용된다.
4. 부모 클래스의 템플릿 기능을 물려받으며, 추가 멤버 변수나 메서드로 기능을 확장할 수도 있다.
5. 템플릿 클래스 상속은 코드 재사용성과 특수화의 균형을 맞추는데 유용하다.
'Game DevTip > C++' 카테고리의 다른 글
| 17. C++ 람다(Lamda) 표현식 (3) | 2025.07.27 |
|---|---|
| 16. C++ 순수 가상함수(추상 클래스 & 인터페이스)에 대해서 (5) | 2025.07.26 |
| 15. C++ 예외 처리 Try Catch문 (0) | 2025.07.25 |
| 14. C++ 이동의미론과 rvalue & lvalue (0) | 2025.07.22 |
| 13. C++ 스마트 포인터에 대해서 (1) | 2025.07.19 |
댓글