콘텐츠로 이동

C++ 템플릿 프로그래밍

템플릿(Template)은 타입을 파라미터로 받는 코드 생성 도구입니다. 동일한 로직을 int, float, std::string 등 다양한 타입에 대해 중복 없이 작성할 수 있습니다. 컴파일러가 사용 시점에 실제 타입을 대입해 코드를 생성하므로 런타임 오버헤드가 없습니다.


// 타입 파라미터 T를 받는 함수 템플릿
template<typename T>
T Max(T A, T B)
{
return (A > B) ? A : B;
}
// 사용 — 컴파일러가 T를 추론
int MaxInt = Max(3, 7); // T = int
double MaxDouble = Max(3.14, 2.71); // T = double
// Max(3, 3.14); // 오류 — T 추론 불가 (int vs double)
// 명시적 타입 지정
double Result = Max<double>(3, 3.14); // T = double 강제
template<typename T, typename U>
auto Add(T A, U B) -> decltype(A + B)
{
return A + B;
}
auto Result = Add(3, 3.14); // T=int, U=double, 반환 double
// C++14: auto 반환 타입 추론
template<typename T, typename U>
auto Multiply(T A, U B)
{
return A * B;
}
// 값을 파라미터로 받는 템플릿
template<typename T, int N>
void PrintArray(const T (&Arr)[N])
{
for (int i = 0; i < N; ++i)
{
std::cout << Arr[i] << " ";
}
std::cout << "\n";
}
int Numbers[] = {1, 2, 3, 4, 5};
PrintArray(Numbers); // T=int, N=5 (자동 추론)

// 고정 크기 스택 자료구조
template<typename T, int Capacity = 64>
class TStack
{
public:
void Push(const T& Value)
{
if (Size < Capacity)
{
Data[Size++] = Value;
}
}
T Pop()
{
return Data[--Size];
}
const T& Top() const { return Data[Size - 1]; }
bool IsEmpty() const { return Size == 0; }
int Num() const { return Size; }
private:
T Data[Capacity];
int Size = 0;
};
// 사용
TStack<int> IntStack; // TStack<int, 64>
TStack<float, 16> FloatStack; // 용량 16
TStack<std::string> StringStack;
template<typename T>
class TBox
{
public:
explicit TBox(T Value);
T Get() const;
void Set(T Value);
private:
T Value;
};
// 외부 정의 시 template<> 반복 필요
template<typename T>
TBox<T>::TBox(T InValue) : Value(InValue) {}
template<typename T>
T TBox<T>::Get() const { return Value; }
template<typename T>
void TBox<T>::Set(T InValue) { Value = InValue; }

특정 타입에 대해 완전히 다른 구현을 제공합니다.

// 일반 템플릿
template<typename T>
struct TypeName
{
static const char* Get() { return "unknown"; }
};
// int에 대한 명시적 특수화
template<>
struct TypeName<int>
{
static const char* Get() { return "int"; }
};
// float에 대한 명시적 특수화
template<>
struct TypeName<float>
{
static const char* Get() { return "float"; }
};
std::cout << TypeName<int>::Get(); // "int"
std::cout << TypeName<double>::Get(); // "unknown"

3.2 부분 특수화 (클래스 템플릿만 가능)

섹션 제목: “3.2 부분 특수화 (클래스 템플릿만 가능)”
// 일반 템플릿
template<typename T, typename U>
struct Pair
{
T First;
U Second;
void Print() { std::cout << First << ", " << Second; }
};
// 두 타입이 같을 때 부분 특수화
template<typename T>
struct Pair<T, T>
{
T First;
T Second;
void Print() { std::cout << "(same type) " << First << ", " << Second; }
T Sum() const { return First + Second; } // 추가 기능
};
// 포인터 타입에 대한 부분 특수화
template<typename T>
struct Pair<T*, T*>
{
T* First;
T* Second;
void Print() { std::cout << "pointer pair"; }
};
Pair<int, double> P1; // 일반
Pair<int, int> P2; // 부분 특수화 — Sum() 사용 가능
Pair<int*, int*> P3; // 포인터 특수화

4. SFINAE — Substitution Failure Is Not An Error

섹션 제목: “4. SFINAE — Substitution Failure Is Not An Error”

타입 대입이 실패해도 오류가 아니라 해당 오버로드를 후보에서 제거합니다. 타입 조건에 따라 함수 오버로드를 선택하는 데 활용합니다.

#include <type_traits>
// 정수 타입일 때만 활성화
template<typename T>
typename std::enable_if<std::is_integral<T>::value, T>::type
Double(T Value)
{
return Value * 2;
}
// 부동소수점 타입일 때만 활성화
template<typename T>
typename std::enable_if<std::is_floating_point<T>::value, T>::type
Double(T Value)
{
return Value * 2.0;
}
auto A = Double(5); // 정수 버전
auto B = Double(3.14); // 부동소수점 버전
// Double("hello"); // 오류 — 어떤 버전도 매칭 안 됨
// 더 간결한 C++17 문법
template<typename T, std::enable_if_t<std::is_integral_v<T>, int> = 0>
void Process(T Value)
{
std::cout << "정수 처리: " << Value;
}
template<typename T, std::enable_if_t<std::is_floating_point_v<T>, int> = 0>
void Process(T Value)
{
std::cout << "실수 처리: " << Value;
}

5. if constexpr — 컴파일 타임 조건 분기 (C++17)

섹션 제목: “5. if constexpr — 컴파일 타임 조건 분기 (C++17)”

SFINAE보다 훨씬 읽기 쉬운 컴파일 타임 분기입니다.

#include <type_traits>
template<typename T>
void Describe(T Value)
{
if constexpr (std::is_integral_v<T>)
{
std::cout << "정수: " << Value << ", 비트 수: " << sizeof(T) * 8;
}
else if constexpr (std::is_floating_point_v<T>)
{
std::cout << "실수: " << std::fixed << Value;
}
else if constexpr (std::is_pointer_v<T>)
{
std::cout << "포인터: " << static_cast<void*>(Value);
}
else
{
std::cout << "기타 타입";
}
}
Describe(42); // 정수: 42, 비트 수: 32
Describe(3.14); // 실수: 3.140000
// if constexpr로 타입별 직렬화 구현
template<typename T>
std::string Serialize(const T& Value)
{
if constexpr (std::is_arithmetic_v<T>)
{
return std::to_string(Value);
}
else if constexpr (std::is_same_v<T, std::string>)
{
return "\"" + Value + "\"";
}
else
{
return Value.ToString(); // 커스텀 타입의 ToString() 호출
}
}

6. 가변 인자 템플릿 (Variadic Templates, C++11)

섹션 제목: “6. 가변 인자 템플릿 (Variadic Templates, C++11)”

임의 개수의 타입 파라미터를 받습니다.

// 재귀 종료 함수
void Print() {} // 파라미터 없을 때 종료
// 가변 인자 템플릿 함수
template<typename T, typename... Args>
void Print(T First, Args... Rest)
{
std::cout << First;
if constexpr (sizeof...(Rest) > 0)
{
std::cout << ", ";
Print(Rest...); // 재귀 펼치기
}
}
Print(1, 3.14, "hello", true); // 출력: 1, 3.14, hello, 1
// 폴드 표현식 (C++17) — 재귀 없이 더 간결하게
template<typename... Args>
auto Sum(Args... Values)
{
return (... + Values); // 단항 폴드: ((V1 + V2) + V3) + ...
}
auto Total = Sum(1, 2, 3, 4, 5); // 15
// 출력 폴드
template<typename... Args>
void PrintAll(Args... Values)
{
((std::cout << Values << " "), ...); // 쉼표 연산자 폴드
}
PrintAll(1, "hello", 3.14); // 1 hello 3.14
// 완벽 전달 + 가변 인자 — std::make_unique 스타일
template<typename T, typename... Args>
std::unique_ptr<T> Make(Args&&... args)
{
return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}

#include <type_traits>
// 자주 쓰는 type traits
static_assert(std::is_integral_v<int>); // true
static_assert(std::is_floating_point_v<float>); // true
static_assert(std::is_pointer_v<int*>); // true
static_assert(std::is_same_v<int, int>); // true
static_assert(std::is_base_of_v<Base, Derived>); // true
static_assert(std::is_constructible_v<MyClass, int, float>); // 생성자 존재 확인
// 타입 변환
using NoRef = std::remove_reference_t<int&>; // int
using NoCV = std::remove_cv_t<const int>; // int
using Decay = std::decay_t<int[5]>; // int*
using Common = std::common_type_t<int, double>; // double

8. 실전 패턴 — 타입 안전 싱글턴

섹션 제목: “8. 실전 패턴 — 타입 안전 싱글턴”
template<typename T>
class TSingleton
{
public:
TSingleton(const TSingleton&) = delete;
TSingleton& operator=(const TSingleton&) = delete;
static T& Get()
{
static T Instance; // C++11: 스레드 안전 정적 초기화
return Instance;
}
protected:
TSingleton() = default;
virtual ~TSingleton() = default;
};
// 사용
class AudioManager : public TSingleton<AudioManager>
{
friend class TSingleton<AudioManager>;
AudioManager() = default;
public:
void PlaySound(const std::string& SoundID) { /* ... */ }
};
AudioManager::Get().PlaySound("explosion");

기능사용 시점
함수 템플릿타입만 다른 동일 로직
클래스 템플릿타입 파라미터화된 자료구조·컨테이너
명시적 특수화특정 타입에 완전히 다른 구현
부분 특수화포인터·동일 타입 등 패턴 기반 다른 구현
if constexpr컴파일 타임 타입 분기 (C++17 권장)
std::enable_ifSFINAE 기반 오버로드 선택 (C++11/14)
Variadic Templates임의 개수 타입 파라미터

핵심 규칙:

  • 템플릿 정의는 헤더 파일에 위치해야 합니다 (링커가 정의를 찾을 수 없음)
  • C++17 이상에서는 SFINAE 대신 if constexpr을 우선 사용
  • sizeof...(Args)로 파라미터 팩의 원소 수를 컴파일 타임에 확인

10. Class Template Argument Deduction (CTAD, C++17)

섹션 제목: “10. Class Template Argument Deduction (CTAD, C++17)”

C++17 이전에는 클래스 템플릿 인스턴스화 시 타입 인자를 명시해야 했습니다. C++17부터 생성자 인자로 타입을 추론합니다.

#include <vector>
#include <pair>
#include <tuple>
// C++14 이전
std::pair<int, double> p1(42, 3.14);
std::vector<int> v1 = {1, 2, 3};
// C++17 CTAD — 타입 생략 가능
std::pair p2(42, 3.14); // pair<int, double>
std::tuple t(1, 2.0, "hi"); // tuple<int, double, const char*>
std::vector v2 = {1, 2, 3}; // vector<int>
// 커스텀 클래스에 CTAD 추론 가이드 추가
template<typename T>
class Buffer
{
public:
T* data;
std::size_t size;
Buffer(T* d, std::size_t s) : data(d), size(s) {}
};
// 추론 가이드 — T[]를 T*로 변환하는 가이드
template<typename T, std::size_t N>
Buffer(T (&)[N], std::size_t) -> Buffer<T>;
int arr[] = {1, 2, 3};
Buffer buf(arr, 3); // Buffer<int> — 추론 가이드 없으면 실패

// 컨테이너 타입 자체를 파라미터로 받기
template<typename T, template<typename, typename> class Container = std::vector>
class Stack
{
Container<T, std::allocator<T>> data_;
public:
void push(const T& v) { data_.push_back(v); }
void pop() { data_.pop_back(); }
T top() const { return data_.back(); }
bool empty() const { return data_.empty(); }
};
Stack<int> vec_stack; // vector 기반 (기본값)
Stack<int, std::deque> deq_stack; // deque 기반
Stack<std::string, std::list> lst_stack; // list 기반
// C++17: 추론 개선 — 탄력적인 컨테이너 래퍼
template<template<typename...> class Cont, typename... Args>
auto make_sorted(Args&&... args)
{
Cont<std::common_type_t<Args...>> c = {std::forward<Args>(args)...};
std::sort(c.begin(), c.end());
return c;
}
auto sv = make_sorted<std::vector>(3, 1, 4, 1, 5, 9, 2);
// vector<int>{1, 1, 2, 3, 4, 5, 9}

12. 비타입 템플릿 파라미터 확장 (C++20)

섹션 제목: “12. 비타입 템플릿 파라미터 확장 (C++20)”
// C++20: 부동소수점, 클래스 타입도 비타입 파라미터로 사용 가능
template<double Scale>
double scaled(double x) { return x * Scale; }
auto triple = scaled<3.0>(5.0); // 15.0
// 문자열 리터럴을 비타입 파라미터로 (C++20)
template<std::size_t N>
struct StringLiteral
{
constexpr StringLiteral(const char (&str)[N])
{
std::copy_n(str, N, value);
}
char value[N];
};
template<StringLiteral Name>
struct Tag
{
static constexpr auto name = Name.value;
};
Tag<"Player"> player_tag;
// player_tag.name == "Player" (컴파일 타임)
// 구조체를 비타입 파라미터로 (C++20)
struct Config {
int max_size;
bool debug;
};
template<Config C>
class Container
{
std::array<int, C.max_size> data_;
public:
static constexpr bool is_debug = C.debug;
};
Container<Config{64, true}> debug_container;
Container<Config{256, false}} release_container;

기능사용 시점
함수 템플릿타입만 다른 동일 로직
클래스 템플릿타입 파라미터화된 자료구조·컨테이너
명시적 특수화특정 타입에 완전히 다른 구현
부분 특수화포인터·동일 타입 등 패턴 기반 다른 구현
if constexpr컴파일 타임 타입 분기 (C++17 권장)
std::enable_ifSFINAE 기반 오버로드 선택 (C++11/14)
Variadic Templates임의 개수 타입 파라미터
CTAD클래스 템플릿 타입 인자 자동 추론 (C++17)
템플릿 템플릿 파라미터컨테이너 타입을 파라미터로 받을 때

핵심 규칙:

  • 템플릿 정의는 헤더 파일에 위치해야 합니다 (링커가 정의를 찾을 수 없음)
  • C++17 이상에서는 SFINAE 대신 if constexpr을 우선 사용
  • C++20 이상에서는 Concepts로 타입 제약을 명시적으로 표현
  • sizeof...(Args)로 파라미터 팩의 원소 수를 컴파일 타임에 확인