[C++] 템플릿 (Templates)

chooha·2026년 1월 13일

C++

목록 보기
17/23

📚 C++ 템플릿 (Templates)

🔸 템플릿이란?

템플릿의 개념

  • 변수의 타입을 미리 정하지 않고, 컴파일 타임에 타입을 지정하는 기능
  • 코드 재사용성을 높이고 타입 안전성을 보장
  • 제네릭 프로그래밍(Generic Programming)의 핵심

템플릿의 동작 원리

template<typename T>
T add(T a, T b)
{
    return a + b;
}

int main()
{
    int x = add(1, 2);        // int 버전 생성
    double y = add(1.5, 2.5); // double 버전 생성
    
    return 0;
}
  • 템플릿 코드는 컴파일 전까지는 실제 코드로 존재하지 않음
  • 템플릿이 사용될 때 해당 타입에 맞는 함수가 자동 생성됨 (Instantiation)

🔸 Function Template (함수 템플릿)

Function Overloading vs Template

❌ Function Overloading (비효율적)

int add(int a, int b) { return a + b; }
double add(double a, double b) { return a + b; }
float add(float a, float b) { return a + b; }
long add(long a, long b) { return a + b; }
// 타입마다 함수를 일일이 작성해야 함!

✅ Function Template (효율적)

template<typename T>
T add(T a, T b)
{
    return a + b;
}

// 컴파일러가 필요한 버전을 자동 생성
add(1, 2);        // int 버전
add(1.5, 2.5);    // double 버전
add(1.0f, 2.0f);  // float 버전

템플릿 인스턴스화 (Template Instantiation)

template<typename T>
void print(T value)
{
    std::cout << value << std::endl;
}

int main()
{
    print(10);      // ✅ print<int>(int) 생성됨
    print(3.14);    // ✅ print<double>(double) 생성됨
    print("hello"); // ✅ print<const char*>(const char*) 생성됨
    
    return 0;
}

명시적 템플릿 인자 지정

template<typename T>
T getValue()
{
    return T{};
}

int main()
{
    auto x = getValue<int>();     // int 반환
    auto y = getValue<double>();  // double 반환
    
    return 0;
}

🔸 Template Type Deduction (템플릿 타입 추론)

자동 타입 추론

template<typename T>
void print(T value)
{
    std::cout << value << std::endl;
}

int main()
{
    int x = 10;
    print(x);  // T는 int로 추론됨
    
    print(3.14);  // T는 double로 추론됨
    
    return 0;
}

auto 키워드와의 관계

// auto와 template type deduction은 동일한 규칙 사용
template<typename T>
void func(T param) { }

auto x = 42;  // auto는 int로 추론
func(42);     // T는 int로 추론

// 둘 다 같은 타입 추론 규칙을 따름

타입 추론 규칙

template<typename T>
void func1(T param);        // 값으로 받음

template<typename T>
void func2(T& param);       // L-value reference

template<typename T>
void func3(T&& param);      // Universal reference (forwarding reference)

int x = 10;
const int cx = x;
const int& rx = x;

// func1: 값으로 받음 (const, reference 제거)
func1(x);   // T = int
func1(cx);  // T = int (const 제거)
func1(rx);  // T = int (const&에서 const와 & 제거)

// func2: L-value reference
func2(x);   // T = int, param은 int&
func2(cx);  // T = const int, param은 const int&
func2(rx);  // T = const int, param은 const int&

// func3: Universal reference (나중에 설명)

🔸 Universal Reference (Forwarding Reference)

Universal Reference란?

template<typename T>
void printVar(T&& a)  // ✅ Universal reference (T&&)
{
    std::cout << a << std::endl;
}
  • T&&R-value reference가 아니라 Universal Reference
  • L-value와 R-value를 모두 받을 수 있음
  • Template에서만 이런 특성을 가짐 (일반 함수의 &&는 R-value reference)

동작 원리: Reference Collapsing

template<typename T>
void printVar(T&& a)
{
    std::cout << a << std::endl;
}

int main()
{
    int x = 10;
    
    printVar(x);              // L-value 전달
    // T = int&
    // T&& = int& && → int& (reference collapsing)
    // 결과: printVar(int& a)
    
    printVar(std::move(x));   // R-value 전달
    // T = int
    // T&& = int&&
    // 결과: printVar(int&& a)
    
    return 0;
}

Reference Collapsing 규칙

T&  &  → T&   // L-value reference
T&  && → T&   // L-value reference
T&& &  → T&   // L-value reference
T&& && → T&&  // R-value reference

// 핵심: & 하나라도 있으면 &, 둘 다 &&일 때만 &&

🔸 std::forward vs std::move

Perfect Forwarding 문제

void process(int& x)  { std::cout << "L-value" << std::endl; }
void process(int&& x) { std::cout << "R-value" << std::endl; }

template<typename T>
void wrapper(T&& arg)
{
    // arg는 이름이 있는 변수이므로 L-value!
    process(arg);  // 항상 L-value로 전달됨
}

int main()
{
    int x = 10;
    wrapper(x);              // L-value 전달했지만
    wrapper(std::move(x));   // R-value 전달했지만
    // 둘 다 process(int&)가 호출됨! ❌
    
    return 0;
}

std::move 사용 (잘못된 방법)

template<typename T>
void wrapper(T&& arg)
{
    process(std::move(arg));  // ❌ 항상 R-value로 전달
}

int main()
{
    int x = 10;
    wrapper(x);              // L-value인데 R-value로 전달됨! ❌
    wrapper(std::move(x));   // R-value로 전달 ✅
    
    return 0;
}

std::forward 사용 (올바른 방법)

template<typename T>
void wrapper(T&& arg)
{
    process(std::forward<T>(arg));  // ✅ 원래 타입 그대로 전달
}

int main()
{
    int x = 10;
    wrapper(x);              // L-value로 전달 ✅ process(int&) 호출
    wrapper(std::move(x));   // R-value로 전달 ✅ process(int&&) 호출
    
    return 0;
}

std::move vs std::forward 비교

// std::move: 무조건 R-value로 캐스팅
template<typename T>
void func1(T&& a)
{
    std::string localVar{std::move(a)};  // 항상 move
}

// std::forward: 원래 타입 유지
template<typename T>
void func2(T&& a)
{
    std::string localVar{std::forward<T>(a)};  // L-value면 copy, R-value면 move
}

int main()
{
    std::string s = "hello";
    
    func1(s);              // move (원본 s가 비워질 수 있음)
    func1(std::move(s));   // move
    
    func2(s);              // copy (원본 s 유지)
    func2(std::move(s));   // move
    
    return 0;
}

핵심 정리

  • std::move: 항상 R-value로 변환 (이동 강제)
  • std::forward: 원래 값 카테고리 유지 (Perfect Forwarding)
  • Universal Reference + std::forward: 가장 효율적인 패턴

🔸 Template Instantiation (템플릿 인스턴스화)

Multiple Type Parameters (여러 타입 매개변수)

template<typename T, typename U>
auto add(T a, U b)  // 서로 다른 타입도 가능
{
    return a + b;
}

int main()
{
    auto result1 = add(1, 2.5);      // T=int, U=double, 반환=double
    auto result2 = add(1.5f, 2);     // T=float, U=int, 반환=float
    auto result3 = add(1L, 2.5);     // T=long, U=double, 반환=double
    
    return 0;
}

명시적 반환 타입 지정

template<typename R, typename T, typename U>
R add(T a, U b)
{
    return static_cast<R>(a + b);
}

int main()
{
    auto x = add<double>(1, 2);  // 명시적으로 double 반환
    auto y = add<int>(1.5, 2.5); // 명시적으로 int 반환 (4)
    
    return 0;
}

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

template<typename T, size_t N>  // N은 컴파일 타임 상수
class Array
{
public:
    T& operator[](size_t index) { return data[index]; }
    size_t size() const { return N; }
    
private:
    T data[N];
};

int main()
{
    Array<int, 5> arr1;     // int 배열, 크기 5
    Array<double, 10> arr2; // double 배열, 크기 10
    
    std::cout << arr1.size() << std::endl;  // 5
    std::cout << arr2.size() << std::endl;  // 10
    
    return 0;
}

비타입 매개변수 제약

  • 정수 타입 (int, size_t, long 등)
  • 포인터
  • 참조
  • enum
  • C++20부터: 부동소수점, 클래스 타입도 가능

Variadic Templates (가변 인자 템플릿)

// Base case (재귀 종료)
void print()
{
    std::cout << std::endl;
}

// Recursive case
template<typename T, typename... Args>  // Args는 parameter pack
void print(T first, Args... rest)
{
    std::cout << first << " ";
    print(rest...);  // 재귀 호출
}

int main()
{
    print(1, 2, 3, 4, 5);           // 1 2 3 4 5
    print("Hello", 42, 3.14, 'A');  // Hello 42 3.14 A
    
    return 0;
}

C++17: Fold Expression

template<typename... Args>
auto sum(Args... args)
{
    return (args + ...);  // Fold expression
}

int main()
{
    std::cout << sum(1, 2, 3, 4, 5) << std::endl;        // 15
    std::cout << sum(1.5, 2.5, 3.5) << std::endl;        // 7.5
    
    return 0;
}

🔸 Template 빌드 (Instantiation)

템플릿은 헤더 파일에 구현해야 함

❌ 잘못된 방법 (.h와 .cpp 분리)

// Math.h
template<typename T>
T add(T a, T b);

// Math.cpp
template<typename T>
T add(T a, T b)
{
    return a + b;
}

// main.cpp
#include "Math.h"
int main()
{
    add(1, 2);  // ❌ 링크 에러!
    // Math.cpp에는 add<int>가 생성되지 않음
    return 0;
}

✅ 올바른 방법 (헤더에 구현)

// Math.h
template<typename T>
T add(T a, T b)  // 헤더에 구현
{
    return a + b;
}

// main.cpp
#include "Math.h"
int main()
{
    add(1, 2);  // ✅ OK
    return 0;
}

이유:

  • 템플릿은 사용 시점에 인스턴스화됨
  • 컴파일러가 템플릿 정의를 볼 수 있어야 인스턴스화 가능
  • .cpp 파일에 숨기면 다른 파일에서 정의를 볼 수 없음

대안: Explicit Instantiation (명시적 인스턴스화)

// Math.h
template<typename T>
T add(T a, T b);

// Math.cpp
template<typename T>
T add(T a, T b)
{
    return a + b;
}

// 사용할 타입을 미리 명시
template int add<int>(int, int);
template double add<double>(double, double);

// main.cpp
#include "Math.h"
int main()
{
    add(1, 2);      // ✅ OK
    add(1.5, 2.5);  // ✅ OK
    // add(1L, 2L); // ❌ 에러! long 버전은 명시하지 않음
    return 0;
}

🔸 Class Template (클래스 템플릿)

기본 사용법

template<typename T>
class Stack
{
public:
    void push(T item)
    {
        data.emplace_back(std::move(item));
    }
    
    bool pop(T& item)
    {
        if (data.empty()) return false;
        item = std::move(data.back());
        data.pop_back();
        return true;
    }
    bool empty() const { return data.empty(); }
    
private:
    std::vector<T> data;
};

int main()
{
    Stack<int> intStack;
    intStack.push(1);
    intStack.push(2);
    
    Stack<std::string> strStack;
    strStack.push("hello");
    strStack.push("world");
    
    return 0;
}

C++17: Class Template Argument Deduction (CTAD)

template<typename T>
class Pair
{
public:
    Pair(T a, T b) : first{a}, second{b} {}
    
    T first;
    T second;
};

int main()
{
    // C++17 이전
    Pair<int> p1{1, 2};
    
    // C++17 이후: 타입 추론
    Pair p2{1, 2};        // Pair<int>로 추론
    Pair p3{1.5, 2.5};    // Pair<double>로 추론
    
    return 0;
}

🔸 Template Alias (템플릿 별칭)

using을 이용한 템플릿 별칭

// 복잡한 타입
std::map<std::string, std::vector<std::pair<int, double>>> data;

// Alias로 단순화
template<typename K, typename V>
using DataMap = std::map<K, std::vector<std::pair<int, V>>>;

DataMap<std::string, double> data;  // 훨씬 읽기 쉬움!

부분 특수화와 함께 사용

template<typename T>
using Ptr = std::unique_ptr<T>;

int main()
{
    Ptr<int> intPtr = std::make_unique<int>(42);
    Ptr<std::string> strPtr = std::make_unique<std::string>("hello");
    
    return 0;
}

🔸 Variable Template (변수 템플릿)

C++14: Variable Template

template<typename T>
constexpr T pi = T{3.1415926535897932385};

int main()
{
    std::cout << pi<float> << std::endl;       // float 정밀도
    std::cout << pi<double> << std::endl;      // double 정밀도
    std::cout << pi<long double> << std::endl; // long double 정밀도
    
    return 0;
}

타입 특성과 함께 사용

template<typename T>
constexpr bool is_pointer_v = std::is_pointer<T>::value;

int main()
{
    std::cout << is_pointer_v<int*> << std::endl;    // 1 (true)
    std::cout << is_pointer_v<int> << std::endl;     // 0 (false)
    
    return 0;
}

🔸 Concepts (C++20)

Concepts란?

  • 템플릿 매개변수에 제약 조건을 명시하는 기능
  • 컴파일 에러 메시지가 명확해짐
  • 템플릿 오버로딩이 쉬워짐

requires 키워드

// C++20 이전: 모든 타입 허용
template<typename T>
T add(T a, T b)
{
    return a + b;
}

// C++20: 제약 추가
template<typename T>
requires std::is_arithmetic_v<T>  // 산술 타입만 허용
T add(T a, T b)
{
    return a + b;
}

int main()
{
    add(1, 2);        // ✅ OK
    add(1.5, 2.5);    // ✅ OK
    // add("a", "b"); // ❌ 컴파일 에러! (명확한 에러 메시지)
    
    return 0;
}

표준 Concepts

#include <concepts>

// std::integral: 정수 타입만
template<std::integral T>
T multiply(T a, T b)
{
    return a * b;
}

// std::floating_point: 부동소수점 타입만
template<std::floating_point T>
T divide(T a, T b)
{
    return a / b;
}

int main()
{
    multiply(2, 3);      // ✅ OK (int)
    // multiply(2.5, 3); // ❌ 에러! (double은 integral이 아님)
    
    divide(5.0, 2.0);    // ✅ OK (double)
    // divide(5, 2);     // ❌ 에러! (int는 floating_point가 아님)
    
    return 0;
}

사용자 정의 Concept

// Concept 정의
template<typename T>
concept Printable = requires(T t)
{
    { std::cout << t } -> std::same_as<std::ostream&>;
};

// Concept 사용
template<Printable T>
void print(const T& value)
{
    std::cout << value << std::endl;
}

class MyClass
{
public:
    friend std::ostream& operator<<(std::ostream& os, const MyClass& obj)
    {
        os << "MyClass";
        return os;
    }
};

int main()
{
    print(42);          // ✅ OK
    print("hello");     // ✅ OK
    print(MyClass{});   // ✅ OK (operator<< 정의됨)
    
    return 0;
}

Concept 조합 (논리 연산자)

template<typename T>
concept Numeric = std::is_arithmetic_v<T>;

template<typename T>
concept Pointer = std::is_pointer_v<T>;

// AND 연산
template<typename T>
concept NumericPointer = Numeric<T> && Pointer<T>;

// OR 연산
template<typename T>
concept NumericOrPointer = Numeric<T> || Pointer<T>;

// NOT 연산
template<typename T>
concept NotPointer = !Pointer<T>;

template<NumericOrPointer T>
void process(T value)
{
    // Numeric이거나 Pointer인 타입만 허용
}

requires 표현식의 다양한 형태

// 1. Simple requirement (단순 요구사항)
template<typename T>
concept HasSize = requires(T t)
{
    t.size();  // size() 메서드가 있어야 함
};

// 2. Type requirement (타입 요구사항)
template<typename T>
concept HasValueType = requires
{
    typename T::value_type;  // value_type이라는 중첩 타입이 있어야 함
};

// 3. Compound requirement (복합 요구사항)
template<typename T>
concept Container = requires(T t)
{
    { t.begin() } -> std::same_as<typename T::iterator>;
    { t.end() } -> std::same_as<typename T::iterator>;
    { t.size() } -> std::convertible_to<size_t>;
};

// 4. Nested requirement (중첩 요구사항)
template<typename T>
concept Sortable = requires(T t)
{
    requires std::is_same_v<decltype(t < t), bool>;
};

🔸 핵심 정리

템플릿 기본

  • 컴파일 타임에 타입이 결정되는 제네릭 프로그래밍
  • 사용 시점에 인스턴스화 (Instantiation)
  • Function overloading보다 효율적

타입 추론과 Perfect Forwarding

  • Universal Reference (T&&)는 L-value와 R-value 모두 받음
  • std::forward로 원래 값 카테고리 유지 (Perfect Forwarding)
  • std::move는 항상 R-value로 변환

템플릿 종류

  • Function Template: 함수 템플릿
  • Class Template: 클래스 템플릿
  • Variable Template: 변수 템플릿 (C++14)
  • Alias Template: 템플릿 별칭

고급 기능

  • Variadic Templates: 가변 인자 템플릿
  • Non-Type Parameters: 비타입 매개변수
  • Concepts (C++20): 템플릿 제약 조건

템플릿 빌드

  • 헤더 파일에 구현 필수
  • 또는 명시적 인스턴스화 사용

<참고 자료>

코드없는 프로그래밍
C++ Templates
C++ Concepts

0개의 댓글