클래스 템플릿
독자 여러분, 지난 장에서 함수 템플릿을 통해 데이터 타입에 독립적인 함수를 정의하는 방법을 학습했습니다.
이제 템플릿의 또 다른 강력한 형태인 클래스 템플릿(Class Templates) 에 대해 알아보겠습니다.
클래스 템플릿은 함수 템플릿과 마찬가지로 타입 매개변수를 사용하여, 다양한 데이터 타입을 저장하고 처리할 수 있는 제네릭(Generic) 클래스를 만들 수 있게 해줍니다.
표준 C++ 라이브러리(STL)의 std::vector
, std::list
, std::map
등 대부분의 컨테이너들은 바로 이 클래스 템플릿으로 구현되어 있습니다.
왜 클래스 템플릿을 사용하는가?
함수 템플릿과 유사하게, 클래스 또한 특정 데이터 타입에 종속될 때 코드 중복 문제가 발생할 수 있습니다.
예를 들어 정수형 데이터를 저장하는 스택, 문자열 데이터를 저장하는 스택, 실수형 데이터를 저장하는 스택을 만들고 싶다면 어떻게 해야 할까요?
// 정수형 스택
class IntStack {
private:
int* arr;
int top;
int capacity;
public:
// ... 생성자, push, pop 등 구현 ...
};
// 문자열 스택
class StringStack {
private:
std::string* arr;
int top;
int capacity;
public:
// ... 생성자, push, pop 등 구현 ...
};
// Double형 스택도 만들려면 또 복사/붙여넣기 해야 할까?
보시다시피, 스택의 push
, pop
등의 로직은 어떤 데이터 타입을 저장하든 동일합니다.
하지만 저장하는 데이터 타입(int
, std::string
, double
등)이 달라지면 해당 클래스 전체를 복사하여 타입만 바꿔야 하는 비효율적인 상황이 발생합니다.
클래스 템플릿은 이러한 문제를 해결하기 위해 등장했습니다.
클래스 템플릿을 사용하면 저장할 데이터 타입을 T
와 같은 타입 매개변수로 지정하여, 하나의 클래스 정의로 다양한 타입의 데이터를 다룰 수 있는 제네릭 클래스를 만들 수 있습니다.
클래스 템플릿의 정의와 사용
클래스 템플릿은 template
키워드와 함께 타입 매개변수를 사용하여 정의합니다.
클래스 이름 뒤에 <T>
와 같이 타입 매개변수를 명시하며, 클래스 내부에서는 이 타입 매개변수를 마치 실제 타입처럼 사용합니다.
template <typename T> // 또는 template <class T>
class 클래스이름 {
public:
// T 타입을 사용하는 멤버 변수 및 멤버 함수
T data;
T getValue();
void setValue(T value);
};
// 여러 개의 타입 매개변수 사용 가능
template <typename KeyType, typename ValueType>
class MyMap {
// KeyType과 ValueType을 사용하는 멤버
};
두 개의 서로 다른 타입의 값을 저장하는 Pair
클래스를 만들어 봅시다.
#include <iostream>
#include <string>
// 클래스 템플릿 정의
template <typename T1, typename T2>
class Pair {
private:
T1 first;
T2 second;
public:
// 생성자
Pair(T1 f, T2 s) : first(f), second(s) {
std::cout << "Pair 객체 생성.\n";
}
// Getter 함수 (const 멤버 함수)
T1 getFirst() const { return first; }
T2 getSecond() const { return second; }
// Setter 함수
void setFirst(T1 f) { first = f; }
void setSecond(T2 s) { second = s; }
void display() const {
std::cout << "First: " << first << ", Second: " << second << std::endl;
}
};
int main() {
// 1. int와 double 타입을 사용하는 Pair 객체 생성
// 컴파일러가 Pair<int, double> 클래스를 인스턴스화
Pair<int, double> p1(10, 20.5);
p1.display(); // 출력: First: 10, Second: 20.5
// 2. std::string과 char 타입을 사용하는 Pair 객체 생성
Pair<std::string, char> p2("Hello", 'W');
p2.display(); // 출력: First: Hello, Second: W
// 3. Pair 템플릿을 중첩하여 사용
Pair<int, Pair<std::string, double>> p3(1, Pair<std::string, double>("Nested", 3.14));
p3.display(); // 출력: First: 1, Second: First: Nested, Second: 3.14
std::cout << "p3.getFirst(): " << p3.getFirst() << std::endl;
std::cout << "p3.getSecond().getFirst(): " << p3.getSecond().getFirst() << std::endl;
return 0;
}
Pair
클래스 템플릿은 T1
과 T2
라는 두 개의 타입 매개변수를 가집니다.
main
함수에서 Pair<int, double> p1
과 같이 클래스 이름을 사용할 때 <int, double>
과 같이 실제 타입을 명시해야 합니다.
컴파일러는 이 정보를 바탕으로 Pair
클래스의 특정 버전(예: Pair<int, double>
)을 생성(인스턴스화)합니다.
클래스 템플릿의 선언과 정의 분리
함수 템플릿과 마찬가지로 클래스 템플릿도 일반적으로 선언과 정의를 분리하여 헤더 파일(.h
)과 소스 파일(.cpp
)에 작성할 수 있습니다.
하지만 클래스 템플릿은 함수 템플릿보다 조금 더 복잡한 규칙을 가집니다.
일반적인 권장 사항
- 클래스 템플릿의 선언과 정의를 모두 헤더 파일에 포함하는 것이 가장 일반적이고 권장되는 방법입니다.
.cpp
파일에 정의를 분리할 경우, 컴파일러가 템플릿을 인스턴스화할 때 필요한 모든 정보를.cpp
파일에서 찾을 수 없어 링커 오류가 발생할 수 있습니다. 이를 해결하려면.cpp
파일에서 사용할 모든 가능한 템플릿 인스턴스를 명시적으로 인스턴스화하거나, 템플릿 정의를 헤더 파일에 두어야 합니다.
예시 (모범 사례): 모든 정의를 헤더 파일에 포함
#pragma once
#include <iostream>
#include <vector> // std::vector 사용 예시
// 클래스 템플릿 선언 및 정의
template <typename T>
class MyContainer {
private:
std::vector<T> elements; // T 타입의 요소를 저장하는 vector
int capacity;
public:
MyContainer(int cap) : capacity(cap) {
elements.reserve(capacity); // 미리 메모리 할당
std::cout << "MyContainer<" << typeid(T).name() << "> 생성, 용량: " << capacity << std::endl;
}
void add(const T& item) {
if (elements.size() < capacity) {
elements.push_back(item);
std::cout << "항목 추가: " << item << std::endl;
} else {
std::cerr << "컨테이너가 가득 찼습니다!\n";
}
}
T get(int index) const {
if (index >= 0 && index < elements.size()) {
return elements[index];
} else {
std::cerr << "오류: 인덱스 범위를 벗어났습니다.\n";
// 실제 코드에서는 예외 처리 또는 다른 오류 반환 방식 사용
return T(); // T의 기본 생성자로 생성된 값 반환
}
}
void displayAll() const {
std::cout << "컨테이너 내용: [";
for (size_t i = 0; i < elements.size(); ++i) {
std::cout << elements[i] << (i == elements.size() - 1 ? "" : ", ");
}
std::cout << "]\n";
}
// 소멸자도 템플릿 클래스 내에 정의
~MyContainer() {
std::cout << "MyContainer<" << typeid(T).name() << "> 소멸.\n";
}
};
#include "MyContainer.h" // 모든 클래스 템플릿 정의가 포함됨
int main() {
MyContainer<int> intContainer(5); // int 타입 컨테이너
intContainer.add(10);
intContainer.add(20);
intContainer.add(30);
intContainer.displayAll(); // 출력: 컨테이너 내용: [10, 20, 30]
std::cout << "인덱스 1의 값: " << intContainer.get(1) << std::endl; // 출력: 인덱스 1의 값: 20
std::cout << "--------------------------------\n";
MyContainer<std::string> stringContainer(3); // std::string 타입 컨테이너
stringContainer.add("Apple");
stringContainer.add("Banana");
stringContainer.add("Cherry");
stringContainer.add("Durian"); // 컨테이너가 가득 찼습니다!
stringContainer.displayAll(); // 출력: 컨테이너 내용: [Apple, Banana, Cherry]
std::cout << "--------------------------------\n";
// 동적 할당
MyContainer<double>* doubleContainer = new MyContainer<double>(2);
doubleContainer->add(1.1);
doubleContainer->add(2.2);
doubleContainer->displayAll();
delete doubleContainer; // 소멸자 호출
return 0;
}
비타입 템플릿 매개변수
템플릿 매개변수는 타입(typename T
) 외에 정수, 열거형, 포인터, 참조 등의 비타입 값도 가질 수 있습니다.
이는 주로 배열의 크기나 다른 상수 값을 컴파일 시점에 지정할 때 유용합니다.
template <typename T, int N> // N은 정수형 비타입 매개변수
class MyStaticArray {
private:
T arr[N]; // 크기가 N인 T 타입 배열
public:
// ...
};
#include <iostream>
#include <string>
template <typename T, int MAX_SIZE> // T: 타입, MAX_SIZE: 정수형 비타입 매개변수
class FixedSizeStack {
private:
T elements[MAX_SIZE]; // 컴파일 시점에 크기가 결정되는 배열
int top;
public:
FixedSizeStack() : top(-1) {
std::cout << "FixedSizeStack<" << typeid(T).name() << ", " << MAX_SIZE << "> 생성.\n";
}
void push(const T& item) {
if (top < MAX_SIZE - 1) {
elements[++top] = item;
std::cout << "Push: " << item << std::endl;
} else {
std::cerr << "스택이 가득 찼습니다!\n";
}
}
T pop() {
if (top >= 0) {
T item = elements[top--];
std::cout << "Pop: " << item << std::endl;
return item;
} else {
std::cerr << "스택이 비어 있습니다!\n";
return T(); // T의 기본 생성자로 생성된 값 반환
}
}
bool isEmpty() const { return top == -1; }
bool isFull() const { return top == MAX_SIZE - 1; }
};
int main() {
FixedSizeStack<int, 5> intStack; // int형, 최대 5개 저장 가능한 스택
intStack.push(10);
intStack.push(20);
intStack.pop(); // 출력: Pop: 20
intStack.push(30);
intStack.push(40);
intStack.push(50);
intStack.push(60); // 출력: 스택이 가득 찼습니다!
std::cout << "--------------------------------\n";
FixedSizeStack<std::string, 3> stringStack; // string형, 최대 3개 저장 가능한 스택
stringStack.push("A");
stringStack.push("B");
stringStack.push("C");
stringStack.pop(); // 출력: Pop: C
stringStack.pop(); // 출력: Pop: B
stringStack.pop(); // 출력: Pop: A
stringStack.pop(); // 출력: 스택이 비어 있습니다!
return 0;
}
클래스 템플릿의 특수화 (심화)
때로는 특정 타입에 대해 템플릿 클래스가 다른 동작을 하도록 만들고 싶을 때가 있습니다.
이를 템플릿 특수화(Template Specialization) 라고 합니다.
// 일반 템플릿 클래스
template <typename T>
class MyClass { /* ... */ };
// 특정 타입 (예: int)에 대한 부분 특수화 또는 완전 특수화
template <> // 완전 특수화
class MyClass<int> { /* int 타입에 대한 특별한 구현 */ };
템플릿 특수화는 복잡하고 고급 개념이므로, 현재 장에서는 개념만 소개하고 자세한 내용은 이후 고급 템플릿에서 다루겠습니다.