1. 함수 템플릿
템플릿을 사용하면 함수가 변수를 매개변수로 전달받아 사용하듯이, 템플릿이 타입을 받아들여서 사용할 수 있게된다.
- 범용성 (Generic Programming): 하나의 코드로 다양한 데이터 타입을 처리하여 코드 중복을 줄임. 여러 데이타 타입을 처리하기 위해서 함수를 여러개 만들지 않아도 됨을 말함.
- 효율성과 안정성 (Efficiency & Safety): 컴파일 타임에 타입을 검사하여 오류를 줄이고, 런타임 오버헤드 없는 고성능 코드를 생성함. 템플릿이 여러 타입을 받아들이게 되어서 동일한 함수명을 이용해서 서로 다른 타입을 가진 매개변수를 처리할 수 있다. 컴파일 타임에 여러 함수를 만들지 않게 되어서 오버헤드를 줄일 수 있다는 장점이 생긴다는 것이다.
문법 설명:
- 함수는 함수명과 함께 “괄호(())”를 명시적으로 붙여야한다. 다시말하면 ” 괄호()”가 없으면 함수가 아니다.
- 이와 마찬가지로 템플릿은, “꺽쇠 괄호(<>)”를 명시적으로 동반하여야 한다.
- 그러므로 타입을 함수의 매개 변수 “꺽쇠 괄호(<>)” 안에 넣어서 사용하면 된다.
- 왜 꺽쇠? 괄호(())를 사용했나? 대괄호(“[]”), 중괄호(“{}”) 모두 사용 중이라 생각하고 넘어가면 될 것 같다.
- ‘T’는 Type(자료형)의 약자이다.
#include <iostream>
template <typename T> //typename T -> 타입이 T란 의미이다.
T add(T a, T b) {
return a+b;
}
int main() {
std::cout << std::showpoint; // 이후 출력되는 모든 실수에 소수점을 표시함
std::cout << add(10,20) << std::endl;
std::cout << add(10.0f,20.0f) << std::endl;
std::cout << add(10.0,20.0) << std::endl;
std::cout << "\n--- Type Check ---" << std::endl;
std::cout << "Float result type: " << typeid(add(10.0f, 20.0f)).name() << std::endl;
std::cout << "Double result type: " << typeid(add(10.0, 20.0)).name() << std::endl;
return 0;
}
// std::showpoint에 대한 설명은 본 포스팅의 주제와 다르지만
// 중요한 내용이라 다른 포스팅에서 다루도록 할 예정이다.
2. 클래스 템플릿
클래스 템플릿은 객체를 생성할 때 여러 타입을 처리할 수 있는 객체를 찍어 내듯이 생성할 수 있도록 해준다. std::vector를 예로 들면 std::vector는 int, float, double.. 등등의 여러 자료형을 하나의 클래스(std::vector)를 통해서 처리할 수 있다는 의미이다. 이 클래스 템플릿의 장점은 아래와 같다.
- 자료구조의 범용성 확보 (Generic Data Structures)
- 명시적 타입 안정성 (Type Safety)
- 유지보수의 효율성 (Maintainability)
클래스가 객체를 찍어 내는 설계도와 같이 템플릿을 이용해서 만들어진 템플릿 클래스도 객체를 찍어 낸다. 단, 클래스 내부의 타입을 변경해서 찍어 낸다는 것이 일반 클래스와 가장 큰 다른 점이라고 이해하면 될 것이다.
아래는 클래스 템플릿의 일반적인 형태이다.
template <typename T>
class Box {
private:
T data;
public:
Box(T d) : data(d) {}
};
아래는 템플릿 함수(Add())를 클래스 템플릿(template <typename T> class Calculator {})으로 변경한 코드다.
#include <iostream>
#include <typeinfo>
// 클래스 템플릿 정의
template <typename T>
class Calculator {
private:
T val1;
T val2;
public:
// 생성자: 멤버 변수 초기화
Calculator(T v1, T v2) : val1(v1), val2(v2) {}
// 덧셈 수행 함수
T add() {
return val1 + val2;
}
// 현재 T의 타입을 확인하기 위한 헬퍼 함수
const std::string getTypeName() {
return typeid(T).name();
}
};
int main() {
std::cout << std::showpoint; // 실수 소수점 표시 설정
// 1. int 타입 인스턴스화
Calculator<int> intCalc(10, 20);
std::cout << "Int Add: " << intCalc.add() << std::endl;
// 2. float 타입 인스턴스화
Calculator<float> floatCalc(10.0f, 20.0f);
std::cout << "Float Add: " << floatCalc.add() << std::endl;
// 3. double 타입 인스턴스화 (C++17부터는 <double> 생략 가능 - CTAD)
Calculator<double> doubleCalc(10.0, 20.0);
std::cout << "Double Add: " << doubleCalc.add() << std::endl;
std::cout << "\n--- Type Check ---" << std::endl;
// 클래스 내부에서 T가 무엇으로 결정되었는지 확인
std::cout << "FloatCalc T is: " << floatCalc.getTypeName() << std::endl;
std::cout << "DoubleCalc T is: " << doubleCalc.getTypeName() << std::endl;
return 0;
}
---------------실행결과--------------
Int Add: 30
Float Add: 30.0000
Double Add: 30.0000
--- Type Check ---
FloatCalc T is: f
DoubleCalc T is: d
위 코드를 읽어보면, 클래스 내부에 타입에 따른 함수를 여러개 만들지 않고, 타입에 따른 객체를 만들어서 객체명으로 매개변수의 타입을 유추할 수 있도록 만들어짐을 볼 수 있다.
3. 템플릿 특수화
특수 타입에 대해서만 템플릿을 다르게 처리하려고 할 때 사용한다. 먼저 아랫 경우에는 템플릿을 그대로 적용할 수 없게된다. 이유는 s1과 s2가 번지이기 때문이다.
template <typename T>
bool isEqual(T a, T b) {
return a == b;
}
int main() {
char s1[] = "hello";
char s2[] = "hello";
isEqual(s1, s2); // 결과는 항상 0
}
// isEqual(char* s1, char* s2)로 받아들인 후
// return s1 == s1 하기 때문이다.
// 즉, 주소값이 같은지를 비교하기 때문이다.
그러므로 isEqual 대신 문자열을 비교하려면, strcmp를 사용해서 아래와 같이 작성해야 한다.
// 예외 템플릿, 특수 템플릿
#include <iostream>
#include <cstring>
template <typename T>
bool isEqual(T a, T b) {
return a == b;
}
// char* 전용 특수화(꼭 추가가 되어야 됨)
template <>
bool isEqual<char*>(char* str1, char* str2) {
// strcmp를 사용하여 두 문자열이 같으면 true를 반환해야 함
return strcmp(str1, str2)==0; //strcmp는 같을 때 0을 반환하기 때문
}
int main() {
char s1[] = "hello";
char s2[] = "hello";
std::cout << isEqual(s1, s2) << std::endl; // 결과는 true(1)
char s3[] = "gdbye";
std::cout << isEqual(s1, s3) << std::endl; // 결과는 false(0)
return 0;
}
4. 요약
함수 템플릿과 클래스 템플릿의 공통점은 함수 및 메소드 함수는 자료형에 따른 여러 함수를 하나의 템플릿으로 만들도록 한 것이다. 또한 여기에 추가하여, 클래스 템플릿은 내부 자료(property)의 타입도 객체 생성 시, 자유롭게 변경할 수 있도록 한 점이 큰 차잇점이 된다.
그러므로 클래스 템플릿으로 설계를 해 놓으면, 다양한 타입을 이용해서 객체를 생성할 수 있게된다. 이 클래스 템플릿 중, 자주 사용할만한 것이 std:: 라이브러리, 즉 STL(Standard Template Library) 라이브러리가 되었다.
위에서도 언급했지만, 이 중 std::vector가 대표적인데 이는 동적 배열(힙에 존재)을 타입에 구애받지 않고 객체로 바로 바로 만들어 낼 수 있는 훌륭한 도구임을 알고 있을 것이다. 아래에서 std::vector 사용법을 확인 할 수 있다.
#include <iostream>
#include <vector>
int main() {
// 1. 선언 및 초기화 (C++11 초기화 리스트 사용)
std::vector<int> v = {10, 20, 30};
// 2. 요소 추가 (push_back)
// 메모리가 부족하면 자동으로 재할당(Reallocation)이 일어남. 메모리 블록을 옮긴다는 의미.
v.push_back(40);
v.push_back(50);
// 3. 요소 접근 및 수정 (배열과 동일)
// 시간 복잡도: O(1)
v[0] = 999;
// 4. 크기 확인
std::cout << "Size: " << v.size() << "\n"; // 출력: 5
// 5. 순회 (Range-based for loop)
std::cout << "Elements: ";
for (int num : v) {
std::cout << num << " ";
}
std::cout << "\n";
return 0;
}