Language/C++

📌 Chapter 12 - 클래스 템플릿

e-cko 2025. 10. 24. 17:16
반응형
반응형

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 멤버는 인스턴스별 저장소 규칙이 다르므로 사용 목적에 맞게 설계해야 한다.

반응형