Company
교육 철학

LV11 템플릿(template), 리스트(list), 연산자 오버로딩

템플릿(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이 재귀보다 간단함을 알고 있나요?