[2026] C++ 클래스 템플릿 | 제네릭 컨테이너와 부분 특수화

[2026] C++ 클래스 템플릿 | 제네릭 컨테이너와 부분 특수화

이 글의 핵심

C++ 클래스 템플릿: 제네릭 컨테이너와 부분 특수화. int 스택, double 스택....계속 만들어야 하나?·실무에서 겪은 문제.

들어가며: int 스택, double 스택…계속 만들어야 하나?

“타입마다 Stack 클래스를 복사하고 있어요”

간단한 스택 자료구조를 만들었습니다. 하지만 타입마다 클래스를 복사해야 했습니다. 문제의 코드: 다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

class IntStack {
    std::vector<int> data;
public:
    void push(int value) { data.push_back(value); }
    int pop() {
        int value = data.back();
        data.pop_back();
        return value;
    }
    bool empty() const { return data.empty(); }
};
class DoubleStack {
    std::vector<double> data;
public:
    void push(double value) { data.push_back(value); }
    double pop() {
        double value = data.back();
        data.pop_back();
        return value;
    }
    bool empty() const { return data.empty(); }
};
// 타입마다 클래스 추가...

위 코드 설명: IntStack과 DoubleStack은 push/pop/empty 로직이 같고 저장하는 요소 타입만 다릅니다. 새 타입마다 클래스를 복사하면 유지보수 비용이 늘고, 한쪽만 수정하면 동작이 어긋날 수 있어, “요소 타입만 다른” 구조는 클래스 템플릿 하나로 통합하는 편이 좋습니다.

추가 문제 시나리오

시나리오 1: 설정값 저장소
설정 시스템에서 int, double, std::string, bool 등 여러 타입의 값을 저장해야 합니다. 타입마다 IntConfig, StringConfig를 만들면 코드 중복이 폭발합니다. 시나리오 2: 네트워크 버퍼 풀
패킷 버퍼를 uint8_t[1024], uint8_t[4096] 등 크기별로 관리할 때, 크기마다 클래스를 복사하면 템플릿 비타입 인자(size_t N)로 한 번에 해결할 수 있습니다. 시나리오 3: 직렬화 래퍼
JSON, Protobuf 등 직렬화 대상 타입이 User, Order, Product 등 수십 개일 때, 각 타입마다 UserSerializer, OrderSerializer를 만들면 템플릿 하나로 통합할 수 있습니다. 시나리오 4: 캐시 컨테이너
키-값 캐시에서 키 타입(std::string, int64_t)과 값 타입(User, std::vector<Item>) 조합이 많을 때, template <typename K, typename V> class Cache로 제네릭하게 처리할 수 있습니다. 클래스 템플릿(타입을 인자로 받아 여러 타입에 대해 같은 구조의 클래스를 생성하는 틀)을 쓰면 Stack<int>, Stack<double>처럼 타입만 바꿔서 같은 로직을 재사용할 수 있고, 컴파일러가 타입별로 코드를 생성합니다. 함수 템플릿과 마찬가지로 “동작은 같은데 요소 타입만 다를 때” 클래스 템플릿으로 통합하면 유지보수가 쉬워집니다. 클래스 템플릿으로 해결 (아래는 복사해 붙여넣은 뒤 g++ -std=c++17 -o stack_tpl stack_tpl.cpp && ./stack_tpl 로 실행 가능): 다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 복사해 붙여넣은 뒤: g++ -std=c++17 -o stack_tpl stack_tpl.cpp && ./stack_tpl
#include <vector>
#include <iostream>
#include <string>
#include <stdexcept>
template <typename T>
class Stack {
    std::vector<T> data;
public:
    void push(const T& value) { data.push_back(value); }
    T pop() {
        if (empty()) throw std::logic_error("Stack is empty");
        T value = data.back();
        data.pop_back();
        return value;
    }
    bool empty() const { return data.empty(); }
    size_t size() const { return data.size(); }
};
int main() {
    Stack<int> intStack;
    intStack.push(10);
    intStack.push(20);
    std::cout << intStack.pop() << "\n";  // 20
    Stack<std::string> strStack;
    strStack.push("hello");
    strStack.push("world");
    std::cout << strStack.pop() << "\n";  // world
    return 0;
}

위 코드 설명: template <typename T> class Stack으로 한 번 정의하면 Stack<int>, Stack<std::string> 등 타입만 바꿔 사용할 수 있습니다. data는 std::vector<T>로 요소 타입에 맞게 저장되고, pop 시 빈 스택이면 logic_error를 던지도록 했습니다. 클래스 템플릿은 사용할 때 타입을 반드시 지정해야 합니다(Stack<int> 등). 실행 결과: 아래 명령으로 실행하면 20world 가 각각 한 줄씩 출력됩니다.

g++ -std=c++17 -o stack_tpl stack_tpl.cpp && ./stack_tpl

클래스 템플릿 인스턴스화 흐름
컴파일러는 Stack<int>, Stack<std::string>처럼 사용할 때마다 해당 타입에 맞는 클래스를 생성합니다. 아래 다이어그램은 템플릿 정의에서 구체적인 타입이 생성되는 과정을 보여 줍니다. 아래 코드는 mermaid를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 실행 예제
flowchart TB
  subgraph template[템플릿 정의]
    T[template <typename T>\nclass Stack]
  end
  subgraph instances[인스턴스화된 타입]
    I1[Stack<int>]
    I2[Stack<double>]
    I3["Stack<std string>"]
  end
  T -->|T=int| I1
  T -->|T=double| I2
  T -->|T=std::string| I3

함수 템플릿과 달리, 클래스 템플릿은 사용할 때 타입을 반드시 명시해야 합니다. Stack<int>, Stack<std::string>처럼요. C++17부터는 생성자 인자로부터 타입을 추론하는 CTAD가 도입되어 Stack s(1);처럼 쓸 수 있는 경우도 있지만, 클래스 이름만으로는 “어떤 타입의 Stack인지” 컴파일러가 알 수 없기 때문에 대부분의 코드에서는 여전히 Stack<int>처럼 적어 줍니다. 이 글을 읽으면:

  • 클래스 템플릿의 기본 문법을 이해할 수 있습니다.
  • 멤버 함수를 클래스 외부에 정의하는 방법을 알 수 있습니다.
  • 부분 특수화를 사용할 수 있습니다.
  • Stack·Array·타입 특성 등 완전한 예제를 구현할 수 있습니다.
  • 흔한 에러와 프로덕션 패턴을 파악할 수 있습니다. 실무 적용 경험: 이 글은 대규모 C++ 프로젝트에서 실제로 겪은 문제와 해결 과정을 바탕으로 작성되었습니다. 책이나 문서에서 다루지 않는 실전 함정과 디버깅 팁을 포함합니다.

목차

  1. 클래스 템플릿 기본 문법
  2. 멤버 함수 외부 정의
  3. 부분 특수화
  4. 템플릿 별칭
  5. 실전 예제: 제네릭 컨테이너
  6. 완전한 예제: Stack·Array·타입 특성
  7. 자주 발생하는 에러와 해결법
  8. 모범 사례
  9. 프로덕션 패턴

1. 클래스 템플릿 기본 문법

클래스 템플릿은 타입을 매개변수로 받는 클래스입니다. std::vector<int>, std::map<std::string, int>가 모두 클래스 템플릿의 인스턴스입니다. 한 번 정의해 두면 정수 벡터, 문자열 벡터, 사용자 정의 타입 벡터를 같은 코드로 다룰 수 있어서, STL처럼 재사용 가능한 자료 구조를 만들 때 필수입니다.

기본 선언과 사용

다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

template <typename T>
class Box {
    T value;
    
public:
    Box(const T& v) : value(v) {}
    
    T get() const { return value; }
    void set(const T& v) { value = v; }
};
int main() {
    Box<int> intBox(42);
    std::cout << intBox.get() << "\n";  // 42
    
    Box<std::string> strBox("hello");
    std::cout << strBox.get() << "\n";  // hello
    
    // ❌ 에러: 클래스 템플릿은 타입 추론 안 됨 (C++17 전)
    // Box box(42);
    
    // ✅ C++17: CTAD (Class Template Argument Deduction)
    Box box(42);  // Box<int>로 추론
}

위 코드 설명: Box<T>는 타입 T 하나를 매개변수로 받는 클래스 템플릿입니다. Box<int>, Box<std::string>처럼 사용할 때 타입을 명시해야 하고, C++17에서는 생성자 인자로부터 타입을 추론하는 CTAD로 Box box(42)처럼 쓸 수 있습니다. 함수 템플릿은 func(42)처럼 인자만으로 T를 추론할 수 있지만, 클래스는 생성자 호출 전에 “이 객체의 타입”이 정해져야 합니다. 그래서 C++17 이전에는 항상 Box<int>, Box<std::string>처럼 타입을 써 줘야 했습니다.

여러 타입 매개변수

다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

template <typename K, typename V>
class KeyValue {
    K key;
    V value;
    
public:
    KeyValue(const K& k, const V& v) : key(k), value(v) {}
    
    K getKey() const { return key; }
    V getValue() const { return value; }
};
int main() {
    KeyValue<std::string, int> kv("age", 25);
    std::cout << kv.getKey() << ": " << kv.getValue() << "\n";
    // age: 25
}

위 코드 설명: KeyValue<K, V>는 키 타입 K와 값 타입 V 두 개의 타입 매개변수를 가집니다. KeyValue<std::string, int>처럼 사용하면 “age”: 25 같은 키-값 쌍을 타입 안전하게 저장할 수 있고, map이나 pair처럼 제네릭한 자료 구조를 만들 때 쓰는 패턴입니다.

기본 템플릿 인자

다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

template <typename T, typename Container = std::vector<T>>
class Stack {
    Container data;
    
public:
    void push(const T& value) {
        data.push_back(value);
    }
    
    T pop() {
        T value = data.back();
        data.pop_back();
        return value;
    }
};
int main() {
    Stack<int> stack1;  // std::vector<int> 사용
    Stack<int, std::deque<int>> stack2;  // std::deque<int> 사용
}

위 코드 설명: 두 번째 템플릿 인자 Container에 기본값 std::vector<T>를 주면 Stack<int>만 써도 내부적으로 std::vector<int>를 사용합니다. Stack<int, std::deque<int>>처럼 두 번째 인자를 넘기면 deque 기반 스택으로 바꿀 수 있어, STL의 기본 인자 패턴과 같습니다. 기본 템플릿 인자를 두면 사용하는 쪽에서 생략할 수 있어서, Stack<int>만 써도 내부적으로 std::vector<int>를 쓰도록 할 수 있습니다. std::vector도 실제로는 할당자(allocator) 같은 두 번째 인자가 있지만 기본값이 있어서 보통은 vector<int>만 씁니다.

2. 멤버 함수 외부 정의

클래스 내부 정의 (인라인)

아래 코드는 cpp를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

template <typename T>
class Container {
    T value;
    
public:
    void set(const T& v) {
        value = v;  // 클래스 내부 정의
    }
    
    T get() const {
        return value;
    }
};

위 코드 설명: 멤버 함수를 클래스 안에 그대로 두면 자동으로 인라인으로 취급됩니다. 템플릿 클래스도 본문이 짧으면 이렇게 내부 정의로 두는 경우가 많고, 컴파일러가 인스턴스화할 때 정의를 볼 수 있어야 하므로 보통 같은 헤더에 둡니다.

클래스 외부 정의

다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

template <typename T>
class Container {
    T value;
    
public:
    void set(const T& v);
    T get() const;
};
// 외부 정의 시 template 키워드 필요
template <typename T>
void Container<T>::set(const T& v) {
    value = v;
}
template <typename T>
T Container<T>::get() const {
    return value;
}

위 코드 설명: 멤버를 클래스 밖에 정의할 때는 template <typename T>를 다시 쓰고, 함수 이름을 Container<T>::set처럼 “클래스명<T>::멤버명”으로 써야 합니다. 이 정의도 사용하는 쪽에서 인스턴스화할 수 있도록 헤더에 두지 않으면 링크 에러가 납니다. 주의: 외부 정의도 헤더 파일에 작성해야 함 (링크 에러 방지) 함수 템플릿과 마찬가지로, 클래스 템플릿의 멤버 함수도 “어떤 타입으로 인스턴스화되는지”가 사용하는 쪽(.cpp)에서 결정됩니다. 그래서 정의가 헤더에 없으면 해당 번역 단위에서 코드를 생성할 수 없어 링크 에러가 납니다. 클래스가 길어지면 선언과 정의를 나누고 싶을 수 있지만, 템플릿은 보통 한 헤더 파일에 선언과 정의를 함께 두는 방식이 일반적입니다.

정적 멤버

다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

template <typename T>
class Counter {
    static int count;  // 선언
    
public:
    Counter() { count++; }
    static int getCount() { return count; }
};
// 정의 (헤더에 작성)
template <typename T>
int Counter<T>::count = 0;
int main() {
    Counter<int> c1, c2, c3;
    std::cout << Counter<int>::getCount() << "\n";  // 3
    
    Counter<double> d1, d2;
    std::cout << Counter<double>::getCount() << "\n";  // 2
    
    // 타입마다 별도의 count 변수
}

위 코드 설명: 정적 멤버 count는 타입마다 한 번만 정의해야 하므로 template <typename T> int Counter&lt;T&gt;::count = 0; 형태로 헤더에 둡니다. Counter<int>와 Counter<double>은 서로 다른 count를 가지므로, 타입별로 객체 개수를 따로 셀 수 있습니다. 템플릿 클래스의 정적 멤버는 인스턴스화된 타입마다 따로 존재합니다. Counter<int>::countCounter<double>::count는 서로 다른 변수이므로, 정수 타입으로 만든 객체 개수와 double 타입으로 만든 객체 개수를 각각 셀 수 있습니다.

3. 부분 특수화

완전 특수화는 “이 한 타입만 다르게”일 때 쓰고, 부분 특수화는 “이런 패턴의 타입들은 다르게”일 때 씁니다. 예를 들어 “모든 포인터 타입”, “모든 배열 타입”, “두 타입이 같은 경우”처럼 조건을 걸 수 있습니다. STL의 vector<bool>이 비트 패킹으로 특수화된 것처럼, 특정 패턴에 대해 메모리나 동작을 최적화할 때 자주 쓰입니다.

포인터 타입 특수화

다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 일반 템플릿
template <typename T>
class SmartPtr {
    T* ptr;
    
public:
    SmartPtr(T* p) : ptr(p) {}
    ~SmartPtr() { delete ptr; }
    
    T& operator*() { return *ptr; }
    T* operator->() { return ptr; }
};
// 배열 타입 특수화
template <typename T>
class SmartPtr<T[]> {
    T* ptr;
    
public:
    SmartPtr(T* p) : ptr(p) {}
    ~SmartPtr() { delete[] ptr; }  // delete[] 사용
    
    T& operator { return ptr[index]; }
};
int main() {
    SmartPtr<int> p1(new int(42));
    std::cout << *p1 << "\n";
    
    SmartPtr<int[]> p2(new int[5]);
    p2[0] = 10;
    std::cout << p2[0] << "\n";
}

위 코드 설명: SmartPtr<T>는 단일 객체용(delete), SmartPtr<T[]>는 배열용(delete[])으로 부분 특수화했습니다. SmartPtr<int>는 일반 버전, SmartPtr<int[]>는 배열 버전이 선택되어, 포인터/배열에 따라 다른 소멸자와 operator[]를 사용할 수 있습니다.

두 타입이 같을 때 특수화

다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 일반 템플릿
template <typename T, typename U>
class Pair {
public:
    void print() {
        std::cout << "Different types\n";
    }
};
// 두 타입이 같을 때 특수화
template <typename T>
class Pair<T, T> {
public:
    void print() {
        std::cout << "Same type\n";
    }
};
int main() {
    Pair<int, double> p1;
    p1.print();  // Different types
    
    Pair<int, int> p2;
    p2.print();  // Same type
}

위 코드 설명: Pair<T, U>는 서로 다른 두 타입용, Pair<T, T>는 같은 타입 두 개용으로 부분 특수화했습니다. Pair<int, double>은 “Different types”, Pair<int, int>는 “Same type”을 출력합니다. 조건에 맞는 특수화가 있으면 그 버전이 선택됩니다.

4. 템플릿 별칭

타입 이름이 길어지면 std::unordered_map<std::string, std::vector<int>>처럼 중첩된 템플릿이 읽기 어려워집니다. 템플릿 별칭으로 의미 있는 이름을 붙여 두면 가독성과 유지보수성이 좋아집니다. C++11의 using으로 타입 별칭을 만드는 방식이 typedef보다 템플릿과 잘 맞아서, 새 코드에서는 using을 쓰는 것이 관례입니다.

using으로 별칭 만들기

아래 코드는 cpp를 사용한 구현 예제입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 긴 타입 이름 단축
template <typename T>
using Vec = std::vector<T>;
template <typename K, typename V>
using Map = std::unordered_map<K, V>;
int main() {
    Vec<int> numbers = {1, 2, 3};
    Map<std::string, int> ages = {{"Alice", 25}};
}

위 코드 설명: using Vec = std::vector<T>처럼 템플릿 별칭을 두면 Vec<int>가 std::vector<int>와 동일한 타입이 됩니다. Map<K, V>도 unordered_map의 별칭이라 긴 타입 이름을 짧게 쓰거나, 나중에 구현을 바꿀 때 한 곳만 수정할 수 있습니다.

부분 적용

아래 코드는 cpp를 사용한 구현 예제입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

template <typename T>
using StringMap = std::unordered_map<std::string, T>;
int main() {
    StringMap<int> ages;
    ages[Alice] = 25;
    
    StringMap<std::string> names;
    names[user1] = "Alice";
}

위 코드 설명: StringMap<T>는 키가 항상 std::string인 map의 별칭입니다. 첫 번째 타입 인자(std::string)를 고정하고 두 번째만 T로 두는 “부분 적용”처럼 쓸 수 있어, 키 타입이 같은 여러 map을 간단한 이름으로 쓸 수 있습니다.

5. 실전 예제: 제네릭 컨테이너

아래 예제들은 클래스 템플릿으로 “타입에 구애받지 않는” 자료 구조를 만드는 패턴을 보여 줍니다. STL의 queue, optional 같은 타입이 내부적으로 어떻게 설계될 수 있는지 감을 잡는 데 도움이 됩니다. 실제 프로젝트에서는 표준 라이브러리를 우선 사용하고, 표준에 없는 동작이 필요할 때만 이런 식으로 래퍼나 작은 컨테이너를 만드는 편이 안전합니다.

예제 1: 타입 안전한 큐

다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 에러 처리를 통해 안정성을 확보합니다, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

template <typename T>
class Queue {
    std::deque<T> data;
    
public:
    void enqueue(const T& value) {
        data.push_back(value);
    }
    
    T dequeue() {
        if (empty()) {
            throw std::logic_error("Queue is empty");
        }
        T value = data.front();
        data.pop_front();
        return value;
    }
    
    const T& front() const {
        if (empty()) {
            throw std::logic_error("Queue is empty");
        }
        return data.front();
    }
    
    bool empty() const {
        return data.empty();
    }
    
    size_t size() const {
        return data.size();
    }
};
int main() {
    Queue<int> q;
    q.enqueue(10);
    q.enqueue(20);
    q.enqueue(30);
    
    while (!q.empty()) {
        std::cout << q.dequeue() << " ";
    }
    // 10 20 30
}

위 코드 설명: Queue<T>는 내부에 std::deque<T>를 두고 enqueue(deque::push_back), dequeue(front + pop_front)로 FIFO를 구현합니다. 빈 큐에서 dequeue/front를 호출하면 logic_error를 던지도록 했고, 타입만 바꿔서 Queue<int>, Queue<std::string> 등으로 재사용할 수 있습니다.

예제 2: 범위 체크하는 배열

다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

template <typename T, size_t N>
class SafeArray {
    T data[N];
    
public:
    T& operator {
        if (index >= N) {
            throw std::out_of_range("Index out of range");
        }
        return data[index];
    }
    
    const T& operator const {
        if (index >= N) {
            throw std::out_of_range("Index out of range");
        }
        return data[index];
    }
    
    constexpr size_t size() const { return N; }
    
    T* begin() { return data; }
    T* end() { return data + N; }
    const T* begin() const { return data; }
    const T* end() const { return data + N; }
};
int main() {
    SafeArray<int, 5> arr;
    arr[0] = 10;
    arr[1] = 20;
    
    try {
        arr[10] = 30;  // 예외 발생
    } catch (const std::out_of_range& e) {
        std::cerr << e.what() << "\n";
    }
    
    // 범위 기반 for 지원
    for (int& value : arr) {
        value *= 2;
    }
}

위 코드 설명: SafeArray<T, N>은 크기 N이 컴파일 시점에 고정된 배열로, operator[]에서 인덱스가 N 이상이면 out_of_range 예외를 던집니다. begin/end를 제공해 범위 기반 for에 쓸 수 있고, 타입과 크기를 템플릿 인자로 받아 타입 안전한 고정 크기 배열을 만드는 패턴입니다.

예제 3: 옵셔널 값 컨테이너

다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

template <typename T>
class Optional {
    bool hasValue;
    alignas(T) unsigned char storage[sizeof(T)];
    
    T* ptr() { return reinterpret_cast<T*>(storage); }
    const T* ptr() const { return reinterpret_cast<const T*>(storage); }
    
public:
    Optional() : hasValue(false) {}
    
    Optional(const T& value) : hasValue(true) {
        new (storage) T(value);  // placement new
    }
    
    ~Optional() {
        if (hasValue) {
            ptr()->~T();  // 명시적 소멸자 호출
        }
    }
    
    bool has_value() const { return hasValue; }
    
    T& value() {
        if (!hasValue) {
            throw std::logic_error("No value");
        }
        return *ptr();
    }
    
    const T& value() const {
        if (!hasValue) {
            throw std::logic_error("No value");
        }
        return *ptr();
    }
    
    T value_or(const T& defaultValue) const {
        return hasValue ? *ptr() : defaultValue;
    }
};
int main() {
    Optional<int> opt1(42);
    std::cout << opt1.value() << "\n";  // 42
    
    Optional<int> opt2;
    std::cout << opt2.value_or(0) << "\n";  // 0
    
    try {
        opt2.value();  // 예외
    } catch (const std::logic_error& e) {
        std::cerr << e.what() << "\n";
    }
}

위 코드 설명: Optional<T>는 값이 있을 수도 없을 수도 있는 타입으로, storage에 placement new로 T를 만들고 소멸자에서 명시적으로 ~T()를 호출합니다. has_value(), value(), value_or(기본값)로 std::optional과 비슷한 인터페이스를 제공하며, 값이 없을 때 value()를 호출하면 logic_error를 던지도록 했습니다.

6. 완전한 예제: Stack·Array·타입 특성

완전한 Stack 템플릿

컨테이너 선택 가능, 이동 생성자·대입, 예외 안전성을 갖춘 완전한 Stack 예제입니다. 다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#include <vector>
#include <deque>
#include <stdexcept>
#include <utility>
template <typename T, typename Container = std::deque<T>>
class Stack {
    Container data;
public:
    using value_type = T;
    using container_type = Container;
    using size_type = typename Container::size_type;
    Stack() = default;
    explicit Stack(const Container& cont) : data(cont) {}
    explicit Stack(Container&& cont) : data(std::move(cont)) {}
    Stack(const Stack&) = default;
    Stack(Stack&&) noexcept = default;
    Stack& operator=(const Stack&) = default;
    Stack& operator=(Stack&&) noexcept = default;
    void push(const T& value) { data.push_back(value); }
    void push(T&& value) { data.push_back(std::move(value)); }
    template <typename....Args>
    void emplace(Args&&....args) {
        data.emplace_back(std::forward<Args>(args)...);
    }
    T pop() {
        if (empty()) throw std::logic_error("Stack is empty");
        T value = std::move(data.back());
        data.pop_back();
        return value;
    }
    T& top() {
        if (empty()) throw std::logic_error("Stack is empty");
        return data.back();
    }
    const T& top() const {
        if (empty()) throw std::logic_error("Stack is empty");
        return data.back();
    }
    bool empty() const { return data.empty(); }
    size_type size() const { return data.size(); }
};
int main() {
    Stack<int> s1;
    Stack<int, std::vector<int>> s2;  // vector 기반
    s1.push(42);
    s1.emplace(100);
}

위 코드 설명: Container 기본값으로 std::deque<T>를 쓰고, emplace로 불필요한 복사/이동을 줄입니다.

완전한 Array 템플릿 (고정 크기)

std::array와 유사한 고정 크기 배열 템플릿입니다. 컴파일 시점 크기, 반복자, at() 범위 검사, std::tuple_size 호환을 지원합니다. 다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#include <stdexcept>
#include <iterator>
#include <algorithm>
template <typename T, size_t N>
class Array {
    T data[N];
public:
    using value_type = T;
    using size_type = size_t;
    using difference_type = ptrdiff_t;
    using reference = T&;
    using const_reference = const T&;
    using pointer = T*;
    using const_pointer = const T*;
    using iterator = T*;
    using const_iterator = const T*;
    T& operator { return data[i]; }
    const T& operator const { return data[i]; }
    T& at(size_type i) {
        if (i >= N) throw std::out_of_range("Array index out of range");
        return data[i];
    }
    const T& at(size_type i) const {
        if (i >= N) throw std::out_of_range("Array index out of range");
        return data[i];
    }
    T& front() { return data[0]; }
    const T& front() const { return data[0]; }
    T& back() { return data[N - 1]; }
    const T& back() const { return data[N - 1]; }
    T* data_ptr() { return data; }
    const T* data_ptr() const { return data; }
    iterator begin() { return data; }
    iterator end() { return data + N; }
    const_iterator begin() const { return data; }
    const_iterator end() const { return data + N; }
    const_iterator cbegin() const { return data; }
    const_iterator cend() const { return data + N; }
    constexpr bool empty() const { return N == 0; }
    constexpr size_type size() const { return N; }
    constexpr size_type max_size() const { return N; }
    void fill(const T& value) { std::fill_n(data, N, value); }
    void swap(Array& other) noexcept { std::swap(data, other.data); }
};
// std::tuple_size 지원 (구조화 바인딩)
namespace std {
template <typename T, size_t N>
struct tuple_size<Array<T, N>> : integral_constant<size_t, N> {};
}

위 코드 설명: at()에서 범위 검사, begin()/end()로 STL 호환, std::tuple_size로 구조화 바인딩 지원.

타입 특성(Type Traits) 클래스 템플릿

컴파일 시점에 타입의 특성을 조회하는 메타 프로그래밍 예제입니다. std::is_integral, std::remove_const 같은 패턴을 이해하는 데 도움이 됩니다. 다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#include <type_traits>
// 기본: false
template <typename T>
struct is_pointer_v : std::false_type {};
// 포인터 특수화: true
template <typename T>
struct is_pointer_v<T*> : std::true_type {};
template <typename T>
inline constexpr bool is_pointer = is_pointer_v<T>::value;
// 사용
static_assert(!is_pointer<int>);
static_assert(is_pointer<int*>);
static_assert(is_pointer<double*>);

위 코드 설명: 기본 템플릿은 std::false_type을 상속해 false를 반환하고, T* 패턴에 대한 부분 특수화는 std::true_type을 상속해 true를 반환합니다. static_assert로 컴파일 시점에 검증합니다. 다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// remove_const 구현
template <typename T>
struct remove_const {
    using type = T;
};
template <typename T>
struct remove_const<const T> {
    using type = T;
};
template <typename T>
using remove_const_t = typename remove_const<T>::type;
// 사용
static_assert(std::is_same_v<remove_const_t<const int>, int>);
static_assert(std::is_same_v<remove_const_t<int>, int>);

위 코드 설명: remove_constconst T일 때 T를 추출하고, 그렇지 않으면 T 그대로 둡니다. remove_const_ttypename remove_const<T>::type의 별칭으로, C++14에서 도입된 _t 접미사 패턴입니다.

7. 자주 발생하는 에러와 해결법

문제 1: “undefined reference” 링크 에러

원인: 템플릿 멤버 함수를 .cpp 파일에 정의하고, 헤더에는 선언만 둔 경우. 컴파일러가 사용하는 쪽에서 인스턴스화할 수 없어서 링크 에러가 납니다. 해결법: 다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// ❌ 잘못된 방식: Stack.cpp에 정의
// Stack.h
template <typename T>
class Stack {
    std::vector<T> data;
public:
    void push(const T& value);  // 선언만
};
// Stack.cpp - 컴파일러가 main.cpp에서 Stack<int>를 인스턴스화할 때 이 정의를 못 봄
template <typename T>
void Stack<T>::push(const T& value) { data.push_back(value); }
// ✅ 올바른 방식: 정의를 헤더에 모두 포함
// Stack.h
template <typename T>
class Stack {
    std::vector<T> data;
public:
    void push(const T& value) { data.push_back(value); }  // 인라인 정의
};

문제 2: “dependent name” 관련 에러

원인: 템플릿 안에서 T::value 같은 의존 타입을 쓸 때, 컴파일러가 value가 타입인지 값인지 구분하지 못합니다. 해결법: 아래 코드는 cpp를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

template <typename T>
class Wrapper {
    // ❌ 에러: 'value'가 타입인지 확인 불가
    // T::value_type x;
    // ✅ typename 키워드로 "타입"임을 명시
    typename T::value_type x;
    // ✅ template 키워드로 "멤버 템플릿"임을 명시
    T obj;
    obj.template foo<int>();
};

문제 3: >> 파싱 오류 (C++11 이전)

원인: Stack<std::vector<int>>처럼 중첩된 >>를 C++03에서는 >>(시프트 연산자)로 파싱할 수 있어 에러가 납니다. 해결법: 아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// ❌ C++03: ">>"가 shift로 해석됨
Stack<std::vector<int>> s;
// ✅ C++03: 공백으로 구분
Stack<std::vector<int> > s;
// ✅ C++11 이후: >> 자동 해석
Stack<std::vector<int>> s;

문제 4: CTAD로 타입 추론 실패

원인: 생성자 인자만으로는 템플릿 타입을 추론할 수 없는 경우(예: 기본 생성자). 해결법: 다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

template <typename T>
class Box {
    T value;
public:
    Box() : value{} {}
    Box(const T& v) : value(v) {}
};
int main() {
    // ❌ 에러: T를 추론할 수 없음
    // Box b;
    // ✅ 타입 명시
    Box<int> b;
}

문제 5: 특수화 순서/선택 오류

원인: 부분 특수화가 여러 개일 때, 더 구체적인 버전이 선택되어야 합니다. 컴파일러는 “가장 특수화된” 버전을 선택합니다. 해결법: Traits<T* const>Traits<T*>보다 더 구체적이므로 int* const에 대해 선택됩니다.

8. 모범 사례

1. 템플릿 매개변수는 typename 또는 class 사용

typenameclass는 타입 매개변수에 동일하게 사용됩니다. typename이 “타입”임을 더 명확히 하므로 최신 코드에서는 typename을 선호합니다.

template <typename T> class Stack {};   // 권장
template <class T> class Stack {};      // 동일

2. 기본 템플릿 인자로 사용 편의성 높이기

아래 코드는 cpp를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 코드를 직접 실행해보면서 동작을 확인해보세요.

template <typename T, typename Alloc = std::allocator<T>>
class MyVector {};
// 사용
MyVector<int> v;  // Alloc 생략 가능

3. using으로 타입 별칭 제공

아래 코드는 cpp를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 코드를 직접 실행해보면서 동작을 확인해보세요.

template <typename T>
class Stack {
public:
    using value_type = T;
    using size_type = size_t;
    using reference = T&;
    using const_reference = const T&;
};

4. noexcept로 이동/스왑 명시

아래 코드는 cpp를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 에러 처리를 통해 안정성을 확보합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

template <typename T>
class Stack {
    Stack(Stack&& other) noexcept : data(std::move(other.data)) {}
    void swap(Stack& other) noexcept { data.swap(other.data); }
};

5. if constexpr로 타입별 분기

아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

template <typename T>
void process(T& value) {
    if constexpr (std::is_arithmetic_v<T>) value *= 2;
    else value.append(" processed");
}

6. static_assert로 컴파일 시점 제약

아래 코드는 cpp를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 코드를 직접 실행해보면서 동작을 확인해보세요.

template <typename T>
class NumericStack {
    static_assert(std::is_arithmetic_v<T>, "T must be arithmetic");
    // ...
};

9. 프로덕션 패턴

패턴 1: CRTP (Curiously Recurring Template Pattern)

다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

template <typename Derived>
class Comparable {
public:
    bool operator!=(const Derived& other) const {
        return !(static_cast<const Derived*>(this)->operator==(other));
    }
};
class Point : public Comparable<Point> {
    int x, y;
public:
    bool operator==(const Point& other) const {
        return x == other.x && y == other.y;
    }
};

위 코드 설명: 파생 클래스가 자신을 템플릿 인자로 전달해, ComparableDerived 타입을 알고 operator==를 호출할 수 있게 합니다. operator!=를 한 번만 정의해 재사용합니다.

패턴 2: Policy 기반 설계

다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

template <typename T, typename Container = std::vector<T>>
class Stack {
    Container data;
    
public:
    void push(const T& v) { data.push_back(v); }
    T pop() {
        T v = data.back();
        data.pop_back();
        return v;
    }
};
// 사용: 컨테이너 정책 변경
Stack<int> s1;                           // vector
Stack<int, std::deque<int>> s2;          // deque
Stack<int, std::list<int>> s3;           // list

패턴 3: 타입 안전한 단위

아래 코드는 cpp를 사용한 구현 예제입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며. 코드를 직접 실행해보면서 동작을 확인해보세요.

template <typename T, typename Tag>
struct StrongType { T value; explicit StrongType(const T& v) : value(v) {} };
using Meter = StrongType<double, struct MeterTag>;
using Second = StrongType<double, struct SecondTag>;
// Meter(5.0) + Second(10.0);  // ❌ 컴파일 에러

패턴 4: 외부 인스턴스화 (빌드 시간 단축)

아래 코드는 cpp를 사용한 구현 예제입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// Stack.h - 선언
template <typename T>
class Stack { /* ....*/ };
// Stack.cpp - explicit instantiation
#include "Stack.h"
template class Stack<int>;
template class Stack<std::string>;
// main.cpp - Stack<int>, Stack<std::string>만 사용 시
// 링크 시 Stack.cpp의 정의 사용

위 코드 설명: 자주 쓰는 타입만 .cpp에서 template class Stack<int>로 명시적 인스턴스화하면, 해당 타입에 맞는 코드가 한 번만 생성되고, 다른 번역 단위에서는 링크만 하면 됩니다. 빌드 시간을 줄일 수 있습니다.

구현 체크리스트

  • 템플릿 정의는 헤더에 모두 포함
  • typename/template로 의존 이름 명시
  • static_assert로 타입 제약 검사
  • 이동 생성자·대입에 noexcept 적용
  • using으로 타입 별칭 제공
  • 자주 쓰는 타입은 explicit instantiation 고려

같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.


이 글에서 다루는 키워드 (관련 검색어)

C++ 클래스 템플릿, template class, 부분 특수화, 제네릭 컨테이너, 템플릿 별칭, 타입 특성, CRTP, Policy 기반 설계, Strong Type 등으로 검색하시면 이 글이 도움이 됩니다.

정리

항목내용
기본 문법template <typename T> class C { };
사용C<int> obj; (타입 명시 필수, C++17 CTAD 예외)
멤버 정의외부 정의도 헤더에 작성 (링크 에러 방지)
부분 특수화포인터, 배열, 동일 타입 등 특정 패턴 특수화
별칭using Alias = Template<T>;
타입 특성std::true_type/false_type 상속, 부분 특수화
실전 용도컨테이너, 래퍼, 타입 안전성, CRTP, Policy 설계

자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. C++ 클래스 템플릿 완벽 가이드. template<typename T> class 문법, 멤버 함수 정의, 부분 특수화(partial specialization), 템플릿 별칭(using), Stack·Array·타입 특성 구현, 흔한 에러와 프로덕션 패턴(CRTP, Policy 설계)까지 실무에 바로 적용할 수 있습니다.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다. 한 줄 요약: 클래스 템플릿으로 타입에 무관한 Stack·Queue를 만들고 부분 특수화로 예외를 다룰 수 있습니다. 다음으로 가변 인자 템플릿(#9-3)를 읽어보면 좋습니다. 다음 글: C++ 실전 가이드 #9-3: 가변 인자 템플릿

관련 글

... 996 lines not shown ... Token usage: 63706/1000000; 936294 remaining Start-Sleep -Seconds 3