C++ Template Basics

1. 템플릿 매개변수의 종류: 타입 vs. 비-타입

C++ 템플릿은 두 가지 주요 종류의 매개변수를 허용합니다.

종류 키워드 전달되는 것 사용 예시
타입 매개변수 (Type Parameter) typename 또는 class 임의의 데이터 타입 (int, std::string, 사용자 정의 클래스 등) template <typename T>
비-타입 매개변수 (Non-Type Parameter) 기본 타입 (int, bool, size_t 등) 컴파일 시간에 알려진 (상수) template <int N>

2. 템플릿 매개변수를 선언하는 다양한 방법

템플릿 매개변수를 선언할 때는 이름(Identifier)기본값(Default Value)의 유무에 따라 다섯 가지 주요 패턴이 나타나며, 각각의 사용 목적이 다릅니다.

2.1. 비-타입 템플릿 매개변수 (Non-Type Template Parameters)

이 매개변수들은 컴파일 시간에 알려진 값(Value)을 템플릿에 전달합니다. (예: 배열 크기, 상수 플래그 등)

template <int N>
class A { int v{N}; }; // N을 내부에서 사용

template <int N = 10>
class B { int v{N}; }; // N을 내부에서 사용, 기본값 제공

template <int = 10>
class C { int v{0}; }; // 이름 없고, 기본값만 제공 (SFINAE 용)

template <int>
class D { int v{0}; }; // 이름도 기본값도 없음 (형식 충족 용)

template <int, int>
class E { int v{0}; }; // 여러 개를 나열

int main()  
{  
    A<10> a{};  
    B<> b{};  
    C<> c{};  
    D<10> d{};  
    E<10, 10> e{};  
}
클래스 선언 (template <…>) 특징 및 역할 활용 예시
A template <int N> 이름 있음, 기본값 없음. 가장 일반적인 형태이며, 전달된 값을 내부 로직에 사용합니다. std::array<int, 5>5와 같이 크기를 전달할 때
B template <int N = 10> 이름 있음, 기본값 있음. 인수가 없을 경우 기본값을 사용하여 편의성을 높입니다. 옵션 플래그가 있는 템플릿
C template <int = 10> 이름 없음, 기본값 있음. 전달된 값을 사용하지 않으며, 인수를 생략하기 위해 기본값을 제공합니다. std::enable_if의 논리 제어 게이트 역할을 위해 사용됩니다. std::enable_if를 사용한 함수 오버로딩
D template <int> 이름 없음, 기본값 없음. 전달된 값을 사용하지 않으며, 오직 형식적으로 매개변수가 하나 필요함을 나타냅니다. 거의 사용되지 않음 (A가 더 명확함)

2.2. 타입 템플릿 매개변수 (Type Template Parameters)

이 매개변수들은 타입(Type)을 템플릿에 전달합니다. (예: int, float, 사용자 정의 구조체 등)

template <typename T>
class A { T v{0}; }; // T를 내부에서 사용

template <typename T = int>
class B { T v{0}; }; // T를 내부에서 사용, 기본값 제공

template <typename = int>
class C { int v{0}; }; // 이름 없고, 기본값만 제공 (SFINAE 용)

template <typename>
class D { int v{0}; }; // 이름도 기본값도 없음 (형식 충족 용)

template <typename, typename>
class E { int v{0}; }; // 여러 개를 나열

int main()  
{  
    A<int> a{};  
    B<> b{};  
    C<> c{};  
    D<int> d{};  
    E<int, int> e{};  
}
클래스 선언 (template <…>) 특징 및 역할 활용 예시
A template <typename T> 이름 있음, 기본값 없음. 가장 일반적인 형태이며, 전달된 타입을 내부에서 사용합니다. std::vector<int>int와 같이 컨테이너의 타입을 정의할 때
B template <typename T = int> 이름 있음, 기본값 있음. 인수가 없을 경우 기본 타입(int)을 사용하여 편의성을 높입니다. std::map의 기본 비교자(Compare = std::less<Key>)
C template <typename = int> 이름 없음, 기본값 있음. 전달된 타입을 사용하지 않으며, 인수를 생략하기 위해 기본값을 제공합니다. std::enable_if의 핵심 패턴입니다. std::enable_if를 사용한 함수 오버로딩
D template <typename> 이름 없음, 기본값 없음. 오직 형식적으로 타입 매개변수가 하나 필요함을 나타냅니다. 거의 사용되지 않음

3. 핵심 패턴: std::enable_if와 이름 없는 매개변수 (Class C)

std::enable_if는 템플릿의 추가 매개변수 위치에 삽입되어, 조건에 따라 해당 템플릿을 선택 후보에서 제외하는 방식으로 동작합니다 (SFINAE).

3.1. 패턴 1: 타입 템플릿 매개변수 사용 (Type Template Parameter)

이 방식은 추가적인 타입 매개변수를 선언하고, 그 기본값std::enable_if_t의 결과로 설정합니다.

#include <type_traits> // std::is_integral, std::enable_if_t

// T가 정수형일 때만 이 함수가 유효함
template <typename T, 
          typename = std::enable_if_t<std::is_integral<T>::value, float> // (A)
         >
void check_type_foo() 
{
    std::cout << "Pattern 1: T는 정수형입니다 (타입 매개변수 사용)" << std::endl;
}

// 사용 예시
check_type_foo<int>();   // (A)가 float으로 치환되어 성공
// check_type_foo<float>(); // (A)가 치환 실패(SFINAE)하여 컴파일 에러

동작 원리 분석:

  • 성공 시 (T = int): std::enable_if_t<true, float> $\to$ float 타입으로 치환.

    • 최종 템플릿 선언은 template <typename T, typename = float>이 되어 유효한 Class C 타입 패턴이 완성됩니다.
  • 실패 시 (T = float): std::enable_if_t<false, float> $\to$ type 멤버를 찾을 수 없어 치환 실패가 발생하며, 이 함수는 컴파일러의 후보 목록에서 제외됩니다.


3.2. 패턴 2: 비-타입 템플릿 매개변수 사용 (Non-Type Template Parameter)

이 방식은 추가적인 비-타입 매개변수를 선언하고, 그 타입std::enable_if_t의 결과로 설정하며 기본 값(= true)을 할당합니다

#include <type_traits> // std::is_integral, std::enable_if_t

// T가 정수형일 때만 이 함수가 유효함
template <typename T,
          std::enable_if_t<std::is_integral<T>::value, bool> = true // (B)
         >
void check_type_bar() 
{
    std::cout << "Pattern 2: T는 정수형입니다 (비-타입 매개변수 사용)" << std::endl;
}

// 사용 예시
check_type_bar<long>();   // (B)가 bool = true로 치환되어 성공
// check_type_bar<double>(); // (B)가 치환 실패(SFINAE)하여 컴파일 에러

동작 원리 분석:

  • 성공 시 (T = long): std::enable_if_t<true, bool> $\to$ bool 타입으로 치환.

    • 최종 템플릿 선언은 template <typename T, bool = true>이 되며, 이는 유효한 Class C 비-타입 패턴이 완성됩니다.
  • 실패 시 (T = double): std::enable_if_t<false, bool> $\to$ type 멤버를 찾을 수 없어 치환 실패가 발생하며, 이 함수는 후보 목록에서 제외됩니다.

두 패턴의 차이점 및 장점

특징 패턴 1: 타입 매개변수 (typename = …) 패턴 2: 비-타입 매개변수 (Type = Value)
선언 형태 타입 매개변수의 기본값을 조작 비-타입 매개변수의 타입을 조작
다중 오버로드 동일한 타입 시그니처로 간주되어 컴파일 오류 발생 (함수 foo를 두 번 선언하는 경우) 서로 다른 비-타입 매개변수 기본값을 가질 수 있어 템플릿 오버로딩에 적합
가독성 단순함 Type = Value 형태가 약간 복잡함

Reference

https://leimao.github.io/blog/CPP-Enable-If/