icon안동민 개발노트

클래스 템플릿


클래스 템플릿의 개념

 클래스 템플릿은 C++에서 제공하는 강력한 기능으로, 타입에 독립적인 클래스를 정의할 수 있게 해줍니다.

 이를 통해 다양한 데이터 타입에 대해 동작하는 일반화된 클래스를 작성할 수 있습니다.

 클래스 템플릿의 기본 구조

template <typename T>
class ClassName {
    // 클래스 멤버 선언 및 정의
};
  • template : 템플릿 선언을 시작하는 키워드입니다.
  • typename T : 템플릿 매개변수를 선언합니다. class T로 쓸 수도 있습니다.
  • T : 템플릿 매개변수로, 실제 사용 시 구체적인 타입으로 대체됩니다.

간단한 클래스 템플릿 예제 : Box 클래스

 다음은 어떤 타입의 값이든 저장할 수 있는 Box 클래스 템플릿의 예입니다.

#include <iostream>
 
template <typename T>
class Box {
private:
    T content;
 
public:
    Box(T item) : content(item) {}
    
    T getContent() const {
        return content;
    }
 
    void setContent(T item) {
        content = item;
    }
};
 
int main() {
    Box<int> intBox(5);
    std::cout << "Int Box contains: " << intBox.getContent() << std::endl;
 
    Box<std::string> stringBox("Hello, World!");
    std::cout << "String Box contains: " << stringBox.getContent() << std::endl;
 
    return 0;
}

 이 예제에서 Box 클래스 템플릿은 정수, 문자열 등 다양한 타입의 값을 저장할 수 있습니다.

템플릿 인스턴스화

 클래스 템플릿을 사용할 때, 구체적인 타입을 지정하여 템플릿을 인스턴스화합니다.

Box<int> intBox(5);           // T를 int로 인스턴스화
Box<std::string> stringBox("Hello"); // T를 std::string으로 인스턴스화

 컴파일러는 이러한 인스턴스화를 통해 실제 클래스 코드를 생성합니다.

멤버 함수 정의

 클래스 템플릿의 멤버 함수는 클래스 내부 또는 외부에서 정의할 수 있습니다.

 클래스 내부 정의

template <typename T>
class MyClass {
public:
    void myFunction() {
        // 함수 구현
    }
};

 클래스 외부 정의

template <typename T>
class MyClass {
public:
    void myFunction();
};
 
template <typename T>
void MyClass<T>::myFunction() {
    // 함수 구현
}

다중 템플릿 매개변수

 클래스 템플릿은 여러 개의 템플릿 매개변수를 가질 수 있습니다.

#include <iostream>
 
template <typename T, typename U>
class Pair {
private:
    T first;
    U second;
 
public:
    Pair(const T& f, const U& s) : first(f), second(s) {}
 
    T getFirst() const { return first; }
    U getSecond() const { return second; }
 
    void print() const {
        std::cout << "(" << first << ", " << second << ")" << std::endl;
    }
};
 
int main() {
    Pair<int, std::string> pair1(1, "One");
    pair1.print();
 
    Pair<double, char> pair2(3.14, 'π');
    pair2.print();
 
    return 0;
}

템플릿 특수화

 특정 타입에 대해 다른 구현을 제공하고 싶을 때 템플릿 특수화를 사용할 수 있습니다.

#include <iostream>
#include <cstring>
 
// 일반 템플릿
template <typename T>
class Storage {
private:
    T value;
public:
    Storage(T val) : value(val) {}
    void print() const {
        std::cout << "Value: " << value << std::endl;
    }
};
 
// char* 타입에 대한 특수화
template <>
class Storage<char*> {
private:
    char* value;
public:
    Storage(const char* val) {
        value = new char[strlen(val) + 1];
        strcpy(value, val);
    }
    ~Storage() {
        delete[] value;
    }
    void print() const {
        std::cout << "Value (char*): " << value << std::endl;
    }
};
 
int main() {
    Storage<int> intStorage(5);
    intStorage.print();
 
    Storage<char*> charStorage("Hello, World!");
    charStorage.print();
 
    return 0;
}

클래스 템플릿과 상속

 클래스 템플릿은 다른 클래스 템플릿을 상속받을 수 있습니다.

template <typename T>
class Base {
protected:
    T value;
public:
    Base(T val) : value(val) {}
    virtual void print() const {
        std::cout << "Base value: " << value << std::endl;
    }
};
 
template <typename T>
class Derived : public Base<T> {
public:
    Derived(T val) : Base<T>(val) {}
    void print() const override {
        std::cout << "Derived value: " << this->value << std::endl;
    }
};
 
int main() {
    Base<int>* basePtr = new Derived<int>(5);
    basePtr->print();  // 출력: Derived value: 5
    delete basePtr;
    return 0;
}

클래스 템플릿의 장단점

 장점

  1. 코드 재사용성 증가
  2. 타입 안전성 보장
  3. 일반화된 컨테이너 및 알고리즘 구현 가능

 단점

  1. 컴파일 시간 증가
  2. 오류 메시지가 복잡해질 수 있음
  3. 코드 크기 증가 가능성 (코드 블로트)

실습 : 제네릭 스택 구현

 다음 요구사항을 만족하는 스택 클래스 템플릿을 구현해보세요.

  1. Stack 클래스 템플릿 작성
  2. push, pop, top, empty, size 메서드 구현
  3. 스택이 비어있을 때 pop이나 top을 호출하면 예외 처리
#include <iostream>
#include <vector>
#include <stdexcept>
 
template <typename T>
class Stack {
private:
    std::vector<T> elements;
 
public:
    void push(const T& elem) {
        elements.push_back(elem);
    }
 
    T pop() {
        if (empty()) {
            throw std::out_of_range("Stack is empty");
        }
        T top = elements.back();
        elements.pop_back();
        return top;
    }
 
    T& top() {
        if (empty()) {
            throw std::out_of_range("Stack is empty");
        }
        return elements.back();
    }
 
    bool empty() const {
        return elements.empty();
    }
 
    size_t size() const {
        return elements.size();
    }
};
 
int main() {
    try {
        Stack<int> intStack;
        intStack.push(5);
        intStack.push(10);
        intStack.push(15);
 
        std::cout << "Stack size: " << intStack.size() << std::endl;
        std::cout << "Top element: " << intStack.top() << std::endl;
 
        while (!intStack.empty()) {
            std::cout << "Popped: " << intStack.pop() << std::endl;
        }
 
        // 이 줄은 예외를 발생시킬 것입니다
        intStack.pop();
    }
    catch (const std::exception& e) {
        std::cerr << "Exception caught: " << e.what() << std::endl;
    }
 
    return 0;
}

연습 문제

  1. Pair 클래스 템플릿을 확장하여 두 값을 교환하는 swap 멤버 함수를 추가하세요.
  2. Array 클래스 템플릿을 구현하세요. 이 클래스는 고정 크기의 배열을 나타내며, operator[]를 오버로딩하여 요소에 접근할 수 있어야 합니다.
  3. Queue 클래스 템플릿을 구현하세요. enqueue, dequeue, front, empty, size 메서드를 포함해야 합니다.

 참고자료

  • "C++ Templates : The Complete Guide" by David Vandevoorde and Nicolai M. Josuttis
  • "Effective C++" by Scott Meyers (항목 42 : typename의 두 가지 의미를 제대로 파악하자)
  • "Modern C++ Design" by Andrei Alexandrescu
  • C++ Reference : Class templates
  • C++ Core Guidelines : T : Templates and generic programming