템플릿(Template)과 연산자 오버로딩
일반화 프로그래밍(Generic Programming)
핵심 개념: 다양한 타입에서 동작하는 하나의 코드를 작성하는 것
왜 일반화 프로그래밍이 필요한가?
문제 상황: 같은 로직인데 타입만 다르면 함수를 여러 번 만들어야 함
// int용 더하기
int Add(int a, int b) { return a + b; }
// float용 더하기
float Add(float a, float b) { return a + b; }
// double용 더하기
double Add(double a, double b) { return a + b; }
// ... 타입마다 계속 만들어야 함!
C++
복사
해결책: 템플릿을 사용하면 하나의 코드로 모든 타입 처리 가능!
template <typename T>
T Add(T a, T b) { return a + b; } // 이것 하나면 끝!
C++
복사
프로그래밍 패러다임 비교
•
객체 지향: 데이터 중심 ("무엇을 저장할 것인가?")
•
일반화 프로그래밍: 알고리즘 중심 ("어떻게 처리할 것인가?")
•
목표: 타입마다 별도 함수/클래스를 만들지 않고 범용적으로 사용
템플릿(Template)이란?
매개변수의 타입에 따라 함수나 클래스를 자동 생성하는 메커니즘
템플릿의 핵심 특징
•
매개변수화 타입(parameterized type): 타입을 매개변수처럼 전달할 수 있음
•
컴파일 시점 생성: 실행 전에 컴파일러가 필요한 버전을 자동으로 만들어줌
•
타입 안전성: 컴파일 시점에 타입 체크가 이루어져 안전함
비유: 템플릿은 "붕어빵 틀"과 같습니다
•
틀(템플릿)은 하나지만, 팥, 슈크림, 피자 등 다양한 재료(타입)로 붕어빵을 만들 수 있습니다
•
붕어빵을 만들 때(컴파일 시점)마다 재료에 맞는 붕어빵이 완성됩니다
변수 템플릿
언제 사용하나요?
•
수학 상수(π, e 등)를 다양한 정밀도로 사용할 때
•
타입마다 다른 기본값이나 설정값이 필요할 때
// variable template
template <typename T>
constexpr T pi = T(3.1415926535897932385L);
int main()
{
float f = pi<float>; // float 버전의 pi (정밀도 낮음)
double d = pi<double>; // double 버전의 pi (정밀도 높음)
int i = pi<int>; // int 버전의 pi (3) - 소수점 버림
// 각 타입의 정밀도에 맞게 자동으로 변환됨
return 0;
}
C++
복사
함수 템플릿
기본 문법
template <typename T>
반환타입 함수이름(T 매개변수)
{
// 함수 본체
}
C++
복사
실제 사용 예시
// ❌ 기존 방식: 타입마다 별도 함수 필요
//int Add(int a, int b) { return a + b; }
//float Add(float a, float b) { return a + b; }
// ✅ 템플릿 방식: 하나의 함수로 모든 타입 처리
template <typename T>
T Add(T a, T b)
{
return a + b;
}
int main()
{
int a = Add<int>(1, 2); // int 버전 호출
float b = Add<float>(1.0f, 2.0f); // float 버전 호출
return 0;
}
C++
복사
Add<int>(1, 2) 호출 시:
├─ 컴파일러가 int 버전의 Add 함수 자동 생성
└─ int Add(int a, int b) { return a + b; }
Plain Text
복사
명시적 특수화(Explicit Specialization)
특정 타입에 대해서만 다른 동작 정의
왜 필요한가요?
대부분의 타입은 일반적인 방식으로 처리하면 되지만, 특정 타입은 특별한 처리가 필요할 때가 있습니다.
실생활 비유:
•
일반 배송: 대부분의 물건은 택배로 배송
•
특수 배송: 꽃, 케이크는 특별 배송 (다르게 처리해야 함)
// 일반 템플릿: 대부분의 타입에 적용
template <typename T>
void Swap(T& a, T& b)
{
T temp = a;
a = b;
b = temp;
}
// double 타입에 대한 특별한 처리 (명시적 특수화)
template <> // <-- 빈 꺾쇠 괄호가 특수화를 의미
void Swap<double>(double& a, double& b)
{
// double형은 값을 서로 바꾸지 않음
std::cout << "double은 교환하지 않습니다!" << std::endl;
}
int main()
{
int x = 10, y = 20;
Swap(x, y); // 일반 템플릿 사용: 값이 교환됨
double a = 1.5, b = 2.5;
Swap(a, b); // 특수화된 버전 사용: 교환 안 됨!
return 0;
}
C++
복사
클래스 템플릿
template <typename T>
class Data
{
public:
T GetData() { return mData; }
void SetData(T data) { mData = data; }
private:
T mData; // T 타입의 데이터 저장
};
int main()
{
Data<int> intData; // int용 Data 클래스
Data<float> floatData; // float용 Data 클래스
intData.SetData(100);
floatData.SetData(3.14f);
return 0;
}
C++
복사
가변인자 템플릿 (C++11)
왜 필요한가요?
•
몇 개의 인자가 올지 모를 때 (1개, 2개, 10개...)
•
각 인자의 타입이 다를 때 (int, string, float 섞여 있을 때)
실생활 비유:
피자 주문처럼 토핑 개수와 종류가 매번 다를 수 있습니다
•
주문("페퍼로니") - 토핑 1개
•
주문("페퍼로니", "올리브", "치즈") - 토핑 3개
•
함수는 하나인데 인자는 유연하게!
기본 문법
template <typename... Args> // Args: 타입들의 묶음 (타입 팩)
void function_name(Args... args) // args: 실제 값들의 묶음 (값 팩)
{
// 함수 본체
}
C++
복사
문법 설명:
•
typename... : "여러 개의 타입"을 의미하는 ...연산자
•
Args... : 타입 팩을 풀어서 사용 (언팩)
재귀적 처리 방식
동작 원리: 하나씩 떼어내서 처리하고, 나머지는 다시 함수에 전달
#include <iostream>
#include <print>
#include <vector>
// 재귀 종료 조건 (인자가 없을 때)
void print()
{
std::cout << std::endl; // 줄바꿈하고 종료
}
// 가변인자 템플릿 함수
template <typename T, typename... Args>
void print(T first, Args... args) // first: 첫 번째, args: 나머지
{
std::cout << first << " "; // 첫 번째 출력
print(args...); // 나머지로 다시 호출 (재귀)
}
int main()
{
print(1, "Hello", 3.14, 'A');
// 실행 순서:
// 1. print(1, "Hello", 3.14, 'A')
// → 1 출력, print("Hello", 3.14, 'A') 호출
// 2. print("Hello", 3.14, 'A')
// → Hello 출력, print(3.14, 'A') 호출
// 3. print(3.14, 'A')
// → 3.14 출력, print('A') 호출
// 4. print('A')
// → A 출력, print() 호출
// 5. print()
// → 줄바꿈하고 종료
return 0;
}
C++
복사
C++17 Fold Expressions
개선점: 재귀 없이 더 간단하게! C++17부터는 접기 표현식(Fold Expression)으로 훨씬 쉽게 작성 가능
#include <iostream>
#include <print>
#include <vector>
template <typename... Args>
auto sum(Args... args)
{
return (... + args); // 왼쪽부터 차례로: ((1+2)+3)+4+5
// (... + args) 는 다음과 같이 전개됨:
// sum(1,2,3,4,5) → ((((1 + 2) + 3) + 4) + 5)
}
template <typename... Args>
void printAll(Args... args)
{
(std::print("{} ", args), ...); // 각 인자마다 print 호출
std::print("\n");
}
int main()
{
int result = sum(1, 2, 3, 4, 5); // 15 반환
printAll(1, 2, 3, 4, 5); // "1 2 3 4 5" 출력
return 0;
}
C++
복사
Fold Expression 종류:
•
(... + args) : 왼쪽 접기 - ((1+2)+3)+4
•
(args + ...) : 오른쪽 접기 - 1+(2+(3+4))
•
연산자: +, -, *, /, &&, ||, , (쉼표) 등 사용 가능
템플릿으로 연결리스트 만들기
#include <iostream>
#include <print>
#include <vector>
#include <list>
namespace ya
{
template <typename T>
class list
{
public:
struct Node
{
T data; // 제네릭 데이터
Node* back; // 다음 노드 포인터
};
list() // 생성자
{
mHead = nullptr;
mTail = nullptr;
}
~list() // 소멸자
{
// 메모리 해제 코드 필요
}
void push_back(T data)
{
if (mHead == nullptr) // 첫 번째 노드
{
mHead = new Node();
mHead->data = data;
mHead->back = nullptr;
mTail = mHead;
}
else // 기존 노드가 있는 경우
{
mTail->back = new Node();
mTail->back->data = data;
mTail->back->back = nullptr;
mTail = mTail->back;
}
}
private:
Node* mHead; // 첫 번째 노드
Node* mTail; // 마지막 노드
};
}
int main()
{
ya::list<int> intList; // int용 리스트
intList.push_back(1);
intList.push_back(2);
std::list<int> stdList; // std::list 사용 예시
stdList.push_back(1);
stdList.push_back(2);
return 0;
}
C++
복사
연산자 오버로딩(Operator Overloading)
왜 필요한가요?
문제: 사용자 정의 타입(구조체, 클래스)은 기본 연산자를 사용할 수 없음
struct Point { int x, y; };
Point p1 = {1, 2};
Point p2 = {3, 4};
Point p3 = p1 + p2; // ❌ 에러! + 연산자를 사용할 수 없음
C++
복사
해결: 연산자 오버로딩으로 직관적인 연산 가능
struct Point {
int x, y;
Point operator+(Point other) { // + 연산자 정의
return {x + other.x, y + other.y};
}
};
Point p3 = p1 + p2; // ✅ 작동! 직관적!
C++
복사
실생활 비유:
•
일반 계산기: 숫자만 더할 수 있음
•
확장된 계산기: 벡터, 행렬, 복소수도 + 버튼으로 더할 수 있게 만듦
기본 문법
타입 operator연산자기호(매개변수)
{
// 연산자 동작 정의
}
C++
복사
오버로딩 가능한 연산자들
카테고리 | 연산자들 | 사용 예시 |
산술 | +, -, \*, /, % | Point p = p1 + p2; |
대입 | =, +=, -=, \*=, /= | p1 += p2; |
비교 | ==, !=, \<, \>, \<=, \>= | if (p1 == p2) {...} |
논리 | &&, ` | if (p1 && p2) {...} |
기타 | \[\], (), -\>, ++, -- | p[0], ++p |
오버로딩 불가능한 연산자들
왜 불가능한가?: C++ 언어의 핵심 구조에 영향을 주기 때문
•
. (멤버 선택) - 객체의 멤버 접근 방식을 바꾸면 언어 전체가 망가짐
•
:: (범위 지정) - 네임스페이스/클래스 구분이 안 됨
•
?: (삼항 연산자) - 단축 평가(short-circuit) 특성을 유지해야 함
•
sizeof - 컴파일 시점에 결정되어야 함
실제 구현 예시
Point 구조체 연산자 오버로딩
struct Point
{
int x, y;
// ➕ 덧셈 연산자 오버로딩
// p1 + p2 를 호출하면 p1.operator+(p2) 가 실행됨
Point operator+(Point other)
{
Point result;
result.x = x + other.x; // 이 객체의 x + 상대방의 x
result.y = y + other.y; // 이 객체의 y + 상대방의 y
return result;
}
// ➖ 뺄셈 연산자 (Point - Point)
Point operator-(Point other)
{
Point result;
result.x = x - other.x;
result.y = y - other.y;
return result;
}
// ➖ 뺄셈 연산자 (Point - int)
Point operator-(int value)
{
Point result;
result.x = x - value;
result.y = y - value;
return result;
}
// 🔍 비교 연산자
bool operator<(Point other)
{
return (x < other.x && y < other.y);
}
};
int main()
{
Point p1 = {1, 1};
Point p2 = {3, 2};
Point p3 = p1 + p2; // p1.operator+(p2) 호출
Point p4 = p1 - p2; // p1.operator-(p2) 호출
Point p5 = p1 - 5; // p1.operator-(5) 호출
if (p1 < p2) { // p1.operator<(p2) 호출
std::cout << "p1이 p2보다 작습니다" << std::endl;
}
return 0;
}
C++
복사
Complex 수 연산자 오버로딩
실전 활용 예시: 복소수 계산을 수학 공식처럼 작성
struct Complex
{
double re, im; // 실수부(real), 허수부(imaginary)
Complex(double r, double i) : re(r), im(i) {}
void Display() {
std::cout << re << " + " << im << "i" << std::endl;
}
// 복소수 덧셈: (a + bi) + (c + di) = (a+c) + (b+d)i
Complex operator+(Complex& other)
{
return Complex(re + other.re, im + other.im);
}
};
int main()
{
Complex a(1.2, 3.4); // 1.2 + 3.4i
Complex b(5.6, 7.8); // 5.6 + 7.8i
Complex c = a + b; // ✨ 수학 공식처럼 사용!
// (1.2+3.4i) + (5.6+7.8i) = 6.8 + 11.2i
c.Display(); // "6.8 + 11.2i" 출력
return 0;
}
C++
복사
연산자 오버로딩의 장점:
•
수학 공식을 그대로 코드로 옮길 수 있음
•
c = add(a, b) 대신 c = a + b 처럼 자연스럽게 작성
•
코드가 수학 교과서처럼 읽힘
숙제 안내
ya::list 구조 분석
목표: 템플릿 기반 자료구조 완전히 이해하기
•
ya::list를 3번 따라 치며 디버깅
•
메모리 구조 그림 그리기 (각 Node가 어떻게 연결되는지)
•
팁: 디버거에서 mHead, mTail, 각 Node의 주소를 확인하며 따라가보세요
ya::list 기능 확장
yaList.push_front(10); // 앞쪽에 추가하는 함수
int len = yaList.size(); // 크기 반환하는 함수
C++
복사
Iterator 구현 (고급)
template <typename T>
class list
{
public:
struct Node
{
T data; // 제네릭 데이터
Node* back; // 다음 노드 포인터
};
class iterator
{
Node* p;
bool operator!=(const iterator& other) const
{
}
void operator++() // 전위 증가 연산자
{
}
};
......
};
// main
for (ya::list<int>::iterator iter = yaList.begin();
iter != yaList.end(); ++iter)
{
std::cout << *iter << std::endl;
}
C++
복사
Vector2 연산자 오버로딩
struct Vector2
{
float x, y;
Vector2(float x_, float y_) : x(x_), y(y_) {}
// 10가지 이상의 연산자 구현하기
// ==, !=, +, -, *, /, +=, -=, *=, /=, <, >, <= 등
};
C++
복사
핵심 정리
템플릿 (Template)
템플릿이란?
여러 타입에 대해 동작하는 함수나 클래스를 작성할 수 있게 해주는 C++의 강력한 기능입니다. 마치 "타입의 변수"처럼 작동합니다.
왜 템플릿이 필요한가요?
템플릿 없이 여러 타입을 지원하려면:
// ❌ 템플릿 없이 (코드 중복)
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; }
// ... 타입마다 함수를 만들어야 함
// ✅ 템플릿 사용 (하나의 코드)
template<typename T>
T Add(T a, T b) { return a + b; }
// 모든 타입에 대해 작동!
C++
복사
템플릿의 장점:
•
코드 재사용성: 한 번 작성으로 모든 타입 지원
•
타입 안전성: 컴파일 타임에 타입 체크
•
성능: 런타임 오버헤드 없음
•
제네릭 프로그래밍: STL(vector, map 등)의 기반
연산자 오버로딩(Operator Overloading)
•
목적: 사용자 정의 타입에 직관적인 연산 제공
•
방법: operator+, operator== 등의 함수 정의
•
장점: 수학 공식처럼 자연스러운 코드 작성 가능
•
주의: 직관에 어긋나는 오버로딩은 피할 것 (예: + 연산자로 뺄셈 구현 X)
제네릭 프로그래밍(Generic Programming)
•
철학: 알고리즘을 타입과 독립적으로 설계
•
효과: 재사용성 ↑, 유지보수성 ↑, 타입 안전성 ↑
•
C++ STL: vector, list, map 등이 모두 템플릿으로 구현됨
학습 체크리스트
•
☐ 템플릿이 컴파일 시점에 생성된다는 것을 이해했나요?
•
☐ typename T의 T가 "타입 매개변수"임을 알고 있나요?
•
☐ 연산자 오버로딩으로 +, - 등을 정의할 수 있나요?
•
☐ 가변인자 템플릿의 재귀 방식을 이해했나요?
•
☐ Fold Expression이 재귀보다 간단함을 알고 있나요?
