템플릿의 개념
템플릿의 동작 원리
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;
}
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<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란?
template<typename T>
void printVar(T&& a) // ✅ Universal reference (T&&)
{
std::cout << a << std::endl;
}
T&&는 R-value reference가 아니라 Universal Reference&&는 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
// 핵심: & 하나라도 있으면 &, 둘 다 &&일 때만 &&
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;
}
핵심 정리
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 등)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;
}
템플릿은 헤더 파일에 구현해야 함
❌ 잘못된 방법 (.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;
}
기본 사용법
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;
}
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;
}
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란?
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>;
};
템플릿 기본
타입 추론과 Perfect Forwarding
T&&)는 L-value와 R-value 모두 받음std::forward로 원래 값 카테고리 유지 (Perfect Forwarding)std::move는 항상 R-value로 변환템플릿 종류
고급 기능
템플릿 빌드
코드없는 프로그래밍
C++ Templates
C++ Concepts