1. 클래스 템플릿 (Class Template)
1) 클래스 템플릿
· 클래스 템플릿은 클래스를 일반화한 정의로서, 클래스의 동작은 고정하고 자료형을 나중에 결정하도록 하는 기법이다.
· 클래스 템플릿을 기반으로 컴파일러가 생성하는 실제 클래스를 템플릿 클래스(template class)라고 한다.
· 서로 다른 자료형으로 같은 구조의 클래스를 여러 번 정의할 필요를 줄이기 위해 클래스 템플릿을 사용한다.
· 문법은 template <typename T> 또는 template <class T> 형식을 사용한다.
· 템플릿 인자 이름(T)은 원하는 이름으로 변경해서 사용한다.
⭕ 추가 설명
1️⃣ 템플릿 정의는 컴파일 시점에 타입별로 인스턴스화(실제 클래스 생성) 된다.
2️⃣ 템플릿으로 한 번 정의하면 다양한 타입에 대해 같은 구현을 재사용할 수 있다.
3️⃣ 템플릿 구현은 소스 분리 시 주의가 필요하다(구현은 헤더에 노출해야 한다).
2) 간단한 예시와 멤버 함수 외부 정의
· 아래 코드는 기본 클래스 템플릿의 예시이다.
#include <iostream> // 표준 입출력 헤더이다.
using namespace std; // std:: 접두사를 생략하기 위해 사용한다.
// 클래스 템플릿 정의이다.
template <typename T> // 템플릿 매개변수 T를 선언한다.
class Ex { // Ex 라는 이름의 클래스 템플릿이다.
public: // public 접근 지정자이다.
T a; // 멤버 변수 a 이다.
T b; // 멤버 변수 b 이다.
Ex(T a, T b) : a(a), b(b) {} // 생성자 정의이다.
T sum() const { return a + b; } // 합을 반환하는 멤버 함수이다.
};
// 멤버 함수 외부 정의 예시이다.
template <typename T> // 템플릿 매개변수 재선언이다.
Ex<T> :: Ex(T a, T b) : a(a), b(b) {} // 외부에서 생성자 정의를 할 때는 Ex<T> 형식으로 표기한다.
int main() { // 프로그램 진입점이다.
Ex<int> ex1(1, 2); // int 타입으로 인스턴스화 한다.
cout << ex1.sum() << endl; // 결과 3 출력이다.
Ex<double> ex2(1.1, 2.2); // double 타입으로 인스턴스화 한다.
cout << ex2.sum() << endl; // 결과 3.3 출력이다.
return 0; // 정상 종료이다.
}
· 예제 결과 요약은 다음과 같다.
· 첫 줄: 3 출력이다.
· 두 번째 줄: 3.3 출력이다.
✳️ 파일 분할 팁
· 템플릿 구현은 인스턴스화 시점에 필요하므로 헤더(.h/.hpp)에 구현을 포함하거나 구현 파일(.tpp/.ipp)을 헤더에서 include 방식으로 포함해야 한다.
· .cpp에만 구현한 뒤 별도 빌드로 해결하려 하면 링크 오류나 미인스턴스화 문제가 발생할 수 있다. ⚠️
2) 배열 클래스 템플릿 (ExArray)
· 배열을 다루는 클래스 템플릿은 내부 데이터 관리, 인덱스 연산자, 복사/이동 동작을 명확히 정의해야 한다.
· 인덱스 연산자 operator[]는 비-const 버전은 T& 반환, const 버전은 const T& 반환 형태가 일반적이다(복사 비용과 수정 가능성 제어를 위해).
#include <iostream> // 표준 입출력 헤더이다.
using namespace std; // std:: 접두사를 생략하기 위해 사용한다.
// 배열 클래스 템플릿 정의이다.
template <typename T> // 템플릿 매개변수 T 선언이다.
class ExArray { // ExArray 클래스 템플릿 정의이다.
private: // 비공개 멤버이다.
T* data; // 동적 배열을 가리키는 포인터이다.
int size; // 배열 길이를 저장하는 변수이다.
public: // 공개 멤버이다.
explicit ExArray(int n) : data(new T[n]), size(n) {} // 생성자 정의이다.
~ExArray() { delete[] data; } // 소멸자에서 동적 메모리 해제한다.
T& operator[](int idx) { return data[idx]; } // 비-const 인덱스 연산자이다.
const T& operator[](int idx) const { return data[idx]; } // const 인덱스 연산자이다.
int length() const { return size; } // 길이 반환 멤버 함수이다.
};
// 사용 예시이다.
int main() {
ExArray<int> a1(3); // int 타입 길이 3 배열 생성이다.
a1[0] = 10; // 인덱스 접근 및 값 대입이다.
a1[1] = 20; // 값 대입이다.
a1[2] = 30; // 값 대입이다.
for (int i = 0; i < a1.length(); ++i) // 배열 출력 루프이다.
cout << a1[i] << (i+1==a1.length()? "\n" : ", "); // 값 출력이다.
return 0; // 정상 종료이다.
}
· 예제 결과: 10, 20, 30 출력이다.
⚠️ 초보자 실수 포인트
· 인덱스 범위 검사 미실시로 인한 UB(정의되지 않은 동작)가 빈번하다. 가능한 경우 bounds-check를 추가하거나 std::vector 사용을 권장한다.
· const 버전에서 값 복사를 반환하면 불필요한 복사가 발생하므로 const T& 반환을 권장한다.
3) 일반함수와 friend 선언
· 클래스 템플릿에서 friend 함수를 선언하는 방법은 여러 가지가 있으며, 접근성과 인스턴스화 시 동작이 달라진다.
· 주요 패턴은 (A) 클래스 내부에서 inline friend 정의 또는 (B) friend 템플릿 선언 후 외부 정의 방식이다.
⭕ 예시 A: 내부에 inline friend로 정의하기
#include <iostream> // 표준 입출력 헤더이다.
using namespace std; // std:: 접두사 생략이다.
template <typename T>
class ExInline {
T value; // 멤버 변수이다.
public:
ExInline(T v) : value(v) {} // 생성자 정의이다.
// 내부에서 friend 연산자를 정의하면 각 인스턴스마다 별도의 함수가 정의된다.
friend ExInline operator+(const ExInline& lhs, const ExInline& rhs) {
return ExInline(lhs.value + rhs.value); // 내부에서 접근 가능한 멤버를 사용한다.
}
friend ostream& operator<<(ostream& os, const ExInline& e) {
os << e.value; // 출력 연산자 구현이다.
return os; // 스트림 반환이다.
}
};
int main() {
ExInline<int> a(1); // 인스턴스 생성이다.
ExInline<int> b(2); // 인스턴스 생성이다.
auto c = a + b; // operator+ 호출이다.
cout << c << endl; // 결과 출력이다.
return 0; // 정상 종료이다.
}
⭕ 예시 B: 외부 템플릿 연산자와 friend 선언
#include <iostream> // 표준 입출력 헤더이다.
using namespace std; // std:: 접두사 생략이다.
template <typename T>
class ExFriend {
T value; // 멤버 변수이다.
public:
ExFriend(T v) : value(v) {} // 생성자 정의이다.
// 외부 템플릿 연산자를 friend로 선언한다.
template <typename U>
friend ExFriend<U> operator+(const ExFriend<U>&, const ExFriend<U>&);
template <typename U>
friend ostream& operator<<(ostream& os, const ExFriend<U>& e);
};
// 외부에서 템플릿 연산자 정의이다.
template <typename U>
ExFriend<U> operator+(const ExFriend<U>& a, const ExFriend<U>& b) {
return ExFriend<U>(a.value + b.value); // private 멤버에 접근 가능하다(친구 선언 덕분).
}
template <typename U>
ostream& operator<<(ostream& os, const ExFriend<U>& e) {
os << e.value; // 출력 구현이다.
return os; // 스트림 반환이다.
}
int main() {
ExFriend<int> x(3); // 인스턴스 생성이다.
ExFriend<int> y(4); // 인스턴스 생성이다.
cout << (x + y) << endl; // 연산 결과 출력이다.
return 0; // 정상 종료이다.
}
⚠️ 주의 사항
· inline friend로 정의하면 함수가 클래스 정의 안에서 각 인스턴스별로 중복 생성될 수 있다.
· 외부 템플릿 연산자를 사용하면 코드 재사용성과 가독성이 좋아진다.
· friend 선언 시 템플릿 파라미터가 다르면 서로 다른 함수가 생성되는 점을 인지해야 한다.
2. 특수화 (Specialization)
1) 클래스 템플릿 특수화 (Full Specialization)
· 완전 특수화(Full Specialization)는 특정 타입에 대해 기본 템플릿과 다른 구현을 제공하는 방법이다.
· 문법은 template<> class Ex<int> { ... } 형식을 사용한다. 이때 template<>는 새 템플릿 매개변수를 도입하지 않는다는 의미이다.
· template <typename T> 는 일반 템플릿(Primary Template)을 정의할 때 사용한다.
· template <> 는 특수화(특정 타입에 대한 구현) 를 정의할 때 사용한다.
이때 괄호 안에 새로운 타입 매개변수를 적지 않는다.
#include <iostream> // 표준 입출력 헤더이다.
using namespace std; // std:: 접두사 생략이다.
template <typename T>
class Holder { // 일반 템플릿 정의이다.
public:
void info() const { cout << "Primary template" << endl; } // 일반 구현이다.
};
// int에 대한 완전 특수화 정의이다.
template <>
class Holder<int> { // int 전용 특수화이다.
public:
void info() const { cout << "Specialized for int" << endl; } // 특수화 구현이다.
};
int main() {
Holder<double> h1; // 일반 템플릿 인스턴스이다.
Holder<int> h2; // int 특수화 인스턴스이다.
h1.info(); // "Primary template" 출력이다.
h2.info(); // "Specialized for int" 출력이다.
return 0; // 정상 종료이다.
}
⚠️ 실무 주의
· 특수화는 정확히 일치하는 타입에 대해서만 적용된다.
· 라이브러리에 따라 특정 형태의 특수화를 지원하지 않는 경우가 있으므로 표준 문법을 사용하는 것이 안전하다.
2) 부분 특수화 (Partial Specialization)
· 부분 특수화(Partial Specialization)는 클래스 템플릿의 일부 타입만 고정하는 특수화 방식이다.
· 부분 특수화는 클래스 템플릿에서만 사용 가능하며 함수 템플릿에서는 불가능하다.
· 전체 특수화가 부분 특수화보다 우선 적용된다.
#include <iostream> // 표준 입출력 헤더이다.
using namespace std; // std:: 접두사 생략이다.
template <typename T1, typename T2>
class Pair { // 기본 템플릿 정의이다.
public:
void info() const { cout << "Primary Pair" << endl; } // 기본 구현이다.
};
// 두 번째 템플릿 매개변수가 int로 고정된 부분 특수화이다.
template <typename T1>
class Pair<T1, int> { // 부분 특수화 정의이다.
public:
void info() const { cout << "Partial specialized: second=int" << endl; } // 부분 특수화 구현이다.
};
int main() {
Pair<double, char> p1; // 기본 템플릿 인스턴스이다.
Pair<float, int> p2; // 부분 특수화 인스턴스이다.
p1.info(); // Primary Pair 출력이다.
p2.info(); // Partial specialized: second=int 출력이다.
return 0; // 정상 종료이다.
}
3. 템플릿 인자 (Template Arguments)
· 템플릿 매개변수(parameter)는 템플릿을 정의할 때 사용하는 변수 이름(T, T1 등)이다.
· 템플릿 인자(argument)는 템플릿을 인스턴스화할 때 전달되는 실제 타입 또는 값이다.
· 템플릿 매개변수는 타입 매개변수와 비-타입(non-type) 매개변수를 모두 허용한다.
⭕ 비-타입 템플릿 매개변수 예시
#include <iostream> // 표준 입출력 헤더이다.
using namespace std; // std:: 접두사 생략이다.
template <typename T, int N> // 타입과 정수 비-타입 매개변수이다.
class StaticArray { // 정적 배열을 갖는 템플릿이다.
T data[N]; // 컴파일 타임 크기의 배열이다.
public:
int size() const { return N; } // 크기 반환 함수이다.
};
int main() {
StaticArray<int, 3> a1; // int 배열 3칸 타입이다.
StaticArray<int, 5> a2; // int 배열 5칸 타입이다.
// a1 = a2; // 에러이다. 서로 다른 타입이므로 대입 불가이다.
cout << a1.size() << ", " << a2.size() << endl; // "3, 5" 출력이다.
return 0; // 정상 종료이다.
}
· 템플릿 인자에 기본값을 지정할 수 있다. 예: template <typename T = int, int N = 10> 형태로 사용한다.
· Ex<> ex;와 같이 빈 꺽쇠를 사용하면 기본 인자값으로 인스턴스화가 이루어진다.
4. 템플릿과 static
1) static 지역변수 (함수 템플릿 내부)
· 템플릿 함수 내부의 static 지역변수는 템플릿 인스턴스별로 각각 존재한다.
· 즉 show<int>()와 show<double>()는 각각 별도의 static 변수를 가진다.
#include <iostream> // 표준 입출력 헤더이다.
#include <typeinfo> // typeid 사용 헤더이다.
using namespace std; // std:: 접두사 생략이다.
template <typename T>
void showCount() { // 템플릿 함수 정의이다.
static int cnt = 0; // 각 인스턴스별로 별도의 static 변수이다.
++cnt; // 카운터 증가이다.
cout << "count for " << typeid(T).name() << " = " << cnt << endl; // 상태 출력이다.
}
int main() {
showCount<int>(); // int 인스턴스 첫 호출이다.
showCount<int>(); // int 인스턴스 두 번째 호출이다.
showCount<double>(); // double 인스턴스 첫 호출이다.
return 0; // 정상 종료이다.
}
· 출력 예시이다.
· count for i = 1 (컴파일러에 따라 타입명은 다를 수 있다) 출력이다.
· count for i = 2 출력이다.
· count for d = 1 출력이다.
2) static 멤버 변수 (클래스 템플릿의 static)
· 클래스 템플릿의 static 멤버 변수는 해당 템플릿 인스턴스 전체에서 공유된다.
· 각 타입 인스턴스별로 독립적인 static 변수가 존재한다.
#include <iostream> // 표준 입출력 헤더이다.
using namespace std; // std:: 접두사 생략이다.
template <typename T>
class ExStatic { // 클래스 템플릿 정의이다.
public:
static T a; // 타입 T를 사용하는 static 멤버이다.
void add(T v) { a += v; } // 값을 더하는 멤버 함수이다.
T get() const { return a; } // 현재 값 반환 멤버 함수이다.
};
// static 멤버 변수의 정의(초기화)이다.
template <typename T>
T ExStatic<T>::a = T(); // 기본 생성값으로 초기화한다.
int main() {
ExStatic<int> i1; // int 인스턴스 생성이다.
ExStatic<int> i2; // 같은 int 인스턴스 생성이다.
i1.add(5); // i1을 통해 값 증가이다.
cout << i2.get() << endl; // i2에서도 변경된 값이 보인다 => 5 출력이다.
ExStatic<double> d1; // double 인스턴스 생성이다.
cout << d1.get() << endl; // double 인스턴스는 별개이므로 0 출력이다.
return 0; // 정상 종료이다.
}
3) static 멤버 특수화
· 특정 타입에 대해 static 멤버의 초기값이나 동작을 다르게 지정할 수 있다(명시적 특수화).
· 특수화 시 타입 일치를 정확히 맞춰야 한다. 예를 들어 T가 double이면 정의도 double 타입으로 해야 한다.
// 앞서 ExStatic<T> 정의가 있다고 가정한다.
// double에 대해 static 멤버의 초기값을 특수화하는 예시이다.
template <>
double ExStatic<double>::a = 3.14; // double 특수화 초기화이다.
⚠️ 주의 사항
· static 멤버를 정의하지 않으면 링크 에러가 발생할 수 있다(정의 필요).
· static 멤버를 특수화할 때는 타입 일치를 반드시 확인한다.
✅마무리 정리
1️⃣ 클래스 템플릿은 타입에 독립적인 클래스 설계를 가능하게 한다.
2️⃣ 멤버 함수의 외부 정의 시 Ex<T>:: 형식으로 표기해야 한다.
3️⃣ 부분 특수화는 클래스 템플릿에서만 가능하고 함수 템플릿에서는 불가능하다.
4️⃣ 템플릿의 비-타입 매개변수는 컴파일타임 상수로 동작하므로 서로 다른 값은 다른 타입이다.
5️⃣ 템플릿 함수의 static 지역변수와 클래스 템플릿의 static 멤버는 인스턴스별 저장소 규칙이 다르므로 사용 목적에 맞게 설계해야 한다.
'Language > C++' 카테고리의 다른 글
| 📌 Chapter 14 - 형변환 연산자 (C++ Casting Operators) (0) | 2025.10.30 |
|---|---|
| 📌 Chapter 13 - 예외처리 (Exception Handling) (0) | 2025.10.28 |
| 📌 Chapter 12 - 템플릿 (0) | 2025.10.24 |
| 📌 Chapter 11 - string (0) | 2025.10.11 |
| 📌 Chapter 10 - 나머지 연산자 오버로딩 (0) | 2025.09.16 |