⊙ 템플릿의 용도에 대해 이해한다.
⊙ 함수 템플릿과 클래스 템플릿의 사용 방법을 안다.
템플릿(Template)
템플릿은 일반화된 프로그래밍(generic programming)을 가능하게 하는 기능이다. 간단히 말해 타입에 구애 받지 않는 일반화된 코드 작성 기법이라고 할 수 있다. 말보다는 예시를 보면 더 빠르게 이해할 수 있다.
#include <iostream>
using namespace std;
int addInt(int x, int y)
{
return x+y;
}
double addDouble(double x, double y)
{
return x+y;
}
int main()
{
cout<<addInt(3, 4)<<endl;
cout<<addDouble(3.3, 4.2)<<endl;
return 0;
}
매개변수로 들어오는 두 수를 더하는 함수 add를 만들기 위해서는 두 가지 함수를 구현해야 한다. int 타입의 두 수를 더하는 함수와 double타입의 두수를 더하는 함수. 구현하고 보니 둘은 완전히 같은 기능을 하고 있는데 데이터 타입만 다른 것을 볼 수 있다. 이런 경우 해결할 수 있는 방법으로 일단 함수 오버로딩을 떠올릴 것이다.
#include <iostream>
using namespace std;
int add(int x, int y)
{
return x+y;
}
double add(double x, double y)
{
return x+y;
}
int main()
{
cout<<add(3, 4)<<endl;
cout<<add(3.3, 4.2)<<endl;
return 0;
}
이렇게 함수 오버로딩을 해주면 같은 기능을 하는 함수를 같은 이름으로 사용할 수 있다. 이게 최선이라고 생각했지만 더 간단히 만들 수 있는 방법이 있다면 믿을 수 있는가?
#include <iostream>
using namespace std;
template <typename T>
T add(T x, T y)
{
return x+y;
}
int main()
{
cout<<add(3, 4)<<endl;
cout<<add(3.3, 4.2)<<endl;
return 0;
}
훨씬 간단해진 모습을 볼 수 있다. 여러 종류의 데이터형들은 보이질 않고 T 하나로 대체되었다. 그리고 add 함수 위에 못 보던 문장이 하나 붙은 것을 볼 수 있다. 이건 아래서 나중에 살펴보고 코드가 어떻게 깔끔해졌는지 살펴보자. main을 살펴보면 데이터 타입에 상관없이 원하는 매개변수를 넣어서 사용하고 있다. 그런데 심지어 함수는 단 하나 밖에 작성되지 않은 상태이다. add는 이제 타입에 상관없이 매개변수를 받고, 계산을 하여 리턴 값을 반환하는 것이다.
바로 타입에 구애 받지 않는 일반화된 코드 작성 기법 템플릿을 사용한 것이다.
사용 방법
템플릿을 사용하기 위해서는 함수나 클래스 앞에 템플릿 접두어를 붙여야 한다.
template <typename T> // T는 원하는 이름 아무거나
template <class T>
둘 중에 아무거나 사용해도 상관없다. 단지 class는 옛날부터 쓰던 표현이고 typename 그거보단 나중에 나온 표현 방법일 뿐이다. T에는 원하는 이름을 아무거나 적어도 상관없다. 다만 가독성과 명확성을 고려해 template <typename T>를 더 권장하긴 한다.
여러개를 정의하고 싶다면 아래와 같이 ','를 이용해 구분하여 정의하면 된다.
template <typename T1, typename T2>
이렇게 접두어를 붙이면 이제 T가 데이터 타입을 대체할 수 있게 된다. 즉 이 T는 원래 데이터 타입이 쓰이는 곳에서 사용이 가능하다. 예를 들면 반환값, 매개변수, 함수내에서 변수 지정 등이 있다.
함수 템플릿
함수 템플릿은 함수의 동작을 일반화하여, 다양한 데이터 타입에서 동일한 로직을 사용할 수 있도록 하는 기능을 만들 수 있다. 하나의 함수 템플릿으로 여러 타입의 입력 데이터를 처리 가능하도록 만든다. 컴파일러가 함수 출 시 제공된 인자 타입에 맞게 템플릿을 인스턴스화 한다.
사실 우리는 함수 템플릿을 이미 봤다. 저 위에 처음 보여준 저게 함수 템플릿이다. 함수 앞에 템플릿 접두어를 붙이고 데이터 타입 대신 설정한 타입 이름(typename)을 붙인다.
#include <iostream>
using namespace std;
template <typename T>
void swapValues(T &a, T &b) {
T temp = a;
a = b;
b = temp;
}
int main() {
int x = 10, y = 20;
swapValues(x, y);
cout << "Swapped int values: x = " << x << ", y = " << y << endl;
double p = 3.14, q = 2.71;
swapValues(p, q);
cout << "Swapped double values: p = " << p << ", q = " << q << endl;
return 0;
}
이왕이면 다른 예시가 좋을 것 같아 함수 템플릿의 다른 예시를 가져왔다. 값을 서로 교환하는 함수를 템플릿으로 구현한 것이다. 매개변수와 함수 내 변수 모두 데이터타입이 T로 선언된 것을 볼 수 있다.
클래스 템플릿
함수 뿐만 아니라 클래스도 템플릿화 할 수 있다. 클래스 템플릿이라고 하여 함수 템플릿과 크게 다른 것이 아니다. 함수 대신 클래스의 설계를 일반화하여, 다양한 데이터 타입에 대해 동일한 구조와 로직을 적용할 수 있도록 만드는 것이다.
마찬가지로 앞에 템플릿 접두어를 붙어야 하는데 이번에는 함수 앞이 아니라 클래스 앞에 붙이는 것 뿐이다. 클래스 선언 앞에 template <typename T>를 선언한다. 그리고 클래스 안에서 자유롭게 마찬가지로 변수, 반환값, 매개변수 등에서 자유롭게 사용하면 된다.
이 예제는 Stack 클래스를 템플릿을 이용해 일반화한 코드이다.
#include <iostream>
#include <vector>
template <typename T>
class Stack {
private:
std::vector<T> elements; // 내부 컨테이너
public:
void Push(T element) {
elements.push_back(element);
}
void Pop() {
if (!elements.empty()) {
elements.pop_back();
}
}
T Top() const {
if (!elements.empty()) {
return elements.back();
}
throw std::out_of_range("Stack is empty");
}
bool IsEmpty() const {
return elements.empty();
}
};
int main() {
Stack<int> intStack; // int 타입 스택
intStack.Push(10);
intStack.Push(20);
std::cout << "Top: " << intStack.Top() << std::endl;
intStack.Pop();
std::cout << "Top after pop: " << intStack.Top() << std::endl;
Stack<std::string> stringStack; // string 타입 스택
stringStack.Push("Hello");
stringStack.Push("World");
std::cout << "Top: " << stringStack.Top() << std::endl;
return 0;
}
Stack 클래스 앞에 템플릿 접두어를 적었고 타입 이름은 T로 하였다.
T가 어디 사용되었는지 확인해보도록 하자. 멤버 변수인 elements의 벡터 타입에 T가 적힌 것을 확인할 수 있다. 이제 데이터 타입에 상관없이 elements 벡터를 만들 수 있다. 원소를 집어넣는 Push는 매개변수로 데이터 타입에 상관없는 원소를 받아온다. 스택의 가장 위에 있는 원소를 반환하는 Top 함수는 원소를 반환해야 하기 때문에 반환 타입이 T이다.
main을 살펴보면 Stack을 선언할 때 옆에 <int>나 <std::string>을 적은 것을 확인할 수 있다. 클래스 템플릿으로 구현했다면 그 클래스로 객체를 생성할 때 "그래서 T를 무슨 타입으로 쓸건지"를 알려줘야 한다. 그래서 클래스 명 옆에 <>기호 안에 데이터 타입을 표시한다. 그렇게 생성한 객체를 보면 타입에 상관없이 int형 스택을 사용했다가 string형 스택을 사용했다가 하는 것을 확인할 수 있다.
클래스 템플릿의 헤더파일과 구현파일 나누기
위 예제는 한 스크립트 안에 구조와 구현까지 전부다 다 들어가 있다. 하지만 C++에서는 헤더파일과 구현파일을 나누어 구현하기도 한다. 그럴 경우 템플릿을 어떻게 사용해야하는지 알아보자. 나는 별 생각없이 파일을 나누었다가 좀 헤맸다.
결론적을 얘기하자면 분리가 불가능하다. 가능하게 만들 수는 있지만 원래하던 대로 평화롭게 헤더 파일과 구현 파일을 나눌 수는 없다. 그래서 보통은 같이 한 파일에 적는다. 이는 템플릿은 컴파일 타임에 모든 것이 준비되어야 하기 때문으로 헤더 안에 구현까지 들어 있어야 한다.
대신 선언부와 구현부를 나누어 적을 수는 있다. 아래는 위에 있는 Stack 클래스를 선언부와 구현부로 나누어 적은 것이다.
#ifndef STACK_H_
#define STACK_H_
#include <vector>
#include <iostream>
template <typename T>
class Stack {
private:
std::vector<T> elements; // 내부 컨테이너
public:
void Push(T element);
void Pop();
T Top() const;
bool IsEmpty() const;
};
// 구현부
template <typename T>
void Stack<T>::Push(T element) {
elements.push_back(element);
}
template <typename T>
void Stack<T>::Pop() {
if (!elements.empty()) {
elements.pop_back();
}
}
template <typename T>
T Stack<T>::Top() const {
if (!elements.empty()) {
return elements.back();
}
throw std::out_of_range("Stack is empty");
}
template <typename T>
bool Stack<T>::IsEmpty() const {
return elements.empty();
}
#endif
선언 부는 별로 다를게 없지만 구현부에서는 주의해야할 사항들이 있다. T를 사용하는 각 함수에 모두 접두사를 붙여야 한다는 것이다. 위에 하나만 적으면 되는 것이 아니라 함수마다 template <typename T> 접두사를 붙여주어야 한다.
이 외 접두사 활용법
템플릿 접두사로 템플릿 선언 말고도 다른 기능을 추가할 수 있다.
기본유형 할당
typename T 옆에 대입 연산자와 데이터 타입을 적어주면 기본유형을 할당한다.
template <typename T = int>
class Stack
{
...
};
만약 내가 객체를 선언할 때 클래스명 옆에 <>안에 아무런 데이터 타입을 적어주지 않는다면 기본적으로 기본유형이라고 생각해라 라는 뜻이다. 아래의 예를 보자.
int main()
{
Stack<> myStack; // int형 스택
}
이렇게 <>안에 아무것도 넣지 않으면 위에서 선언한대로 기본유형인 int인 것으로 친다.
비형식 매개변수
놀랍게도 typename T 자리에 그냥 변수가 들어갈 수도 있다. 이것을 비형식(타입) 매개변수라고 한다.
사용하는 방법은 기존과 동일하다. 만약 Stack이 벡터로 구현되지 않고 배열로 구현되어 초기 크기값을 받아야 하는 상황이라면 Stack을 생성할 때 배열의 크기를 받아올 수 있을 것이다.
template <typename T, int capcity>
class Stack
{
T element[capacity];
...
};
그리고 실제 사용은 이렇게 하면 된다.
int main()
{
Stack<int, 10> intStack;
}
데이터 타입을 넣는 것처럼 int형 값을 넣어주면 스택의 크기 초기화가 가능하다.
배운 내용 정리
- 템플릿은 일반화된 프로그래밍(generic programming)을 가능하게 하는 기능이다. 간단히 말해 타입에 구애 받지 않는 일반화된 코드 작성 기법이라고 할 수 있다.
- 함수 템플릿은 함수의 동작을 일반화하여, 다양한 데이터 타입에서 동일한 로직을 사용할 수 있도록 하는 기능을 만들 수 있다.
- 클래스 템플릿은 클래스의 설계를 일반화하여, 다양한 데이터 타입에 대해 동일한 구조와 로직을 적용할 수 있도록 만드는 것이다.
- 템플릿은 컴파일 타임에 모든 것이 준비되어야 하기 때문에 헤더 안에 구현까지 들어 있어야 한다.
- typename T 옆에 대입 연산자와 데이터 타입을 적어주면 기본유형을 할당한다.
- typename T 자리에 그냥 변수가 들어갈 수도 있다. 이것을 비형식(타입) 매개변수라고 한다.
'C++ > 게임 개발자를 위한 C++ 문법' 카테고리의 다른 글
범위 기반 for 문(ranged-based for statement) (0) | 2025.01.07 |
---|---|
디자인 패턴(Design Pattern) : 생성(Creational) 패턴 (0) | 2025.01.07 |
스마트 포인터(Smart Pointer) (0) | 2025.01.03 |
선형 탐색과 이진 탐색 (0) | 2025.01.02 |
얕은 복사(Shallow Copy) VS 깊은 복사(Deep Copy) (0) | 2024.12.30 |