C++/C++ STL

C++ STL (뇌를 자극하는) - 4장. 함수 템플릿 (Section 2 ~ 3)

둠치킨 2025. 7. 28. 14:56

※ 제가 개인적으로 공부하는 것이라 요약하거나 책에서 빠진 내용이 있을 수 있습니다 ※

Section 2. 함수 템플릿

클래스 템플릿은 클래스를 만들어내는 틀(메타 코드)다. 함수 템플릿과 별반 다를게 없다. 단지 함수에서 클래스 바뀐 것 뿐.

 

클래스 템플릿을 설명하기 위한 정수형 배열을 추상화한 클래스 Array를 최소의 기능만을 갖게 만들어봅니다.

정수형 Array 클래스

#include <iostream>
using namespace std;

class Array
{
    int* buf;
    int size;     //원소의 개수
    int capacity; //저장 가능한 메모리 크기
    
public:
    explicit Array(int cap = 100) : buf(0), size(0), capacity(cap)
    {
        buf = new int[capacity];
    }
    
    ~Array() { delete[] buf; }
    
    void Add(int data)
    {
        buf[size++] = data;
    }
    
    int operator[] (int idx) const
    {
        return buf[idx];
    }
    
    int GetSize() const
    {
        return size;
    }
    // 나머지 생략
}

int main()
{
    Array arr;
    arr.Add(10); arr.Add(20); arr.Add(30);
    
    for(int i=0; i<arr.GetSize(); i++)
    {
    	cout << arr[i] << '\n';
    }
    
    return 0;
}

이제 정수뿐 아니라 실수를 저장하는 Array가 필요한데, IntArray, DoubleArray, StringArray 같이 직접 정의하는 것은 코드가 중복되면서 불필요하게 작성해야하고, Array 클래스의 원소 타입을 클라이언트가 결정할 수 없다는 단점이 있어서 template<typename T> class Array로 정의하는게 더 좋다.

template<typename T=int, int capT=100> //int, 100 디폴트 매개변수 값 지정
class Array
{
    T* buf;
    ...
}

 

클래스 템플릿 특수화도 가능하다. 클래스 템플릿 특수화(class Template Specialization)는 함수 템플릿 특수화처럼 일반 버전의 템플릿을 사용할 수 없는 경우나 성능 개선이나 특수한 기능 등을 위해 특수한 버전을 별도로 제공하고자 할 때 사용.

클래스 템플릿 특수화를 설명하기 위해 객체 정보를 출력하는 ObjectInfo라는 클래스 템플릿을 생성. ObjectInfo 클래스는 객체의 타입, 크기, 값을 출력하는 Print() 인터페이스를 갖습니다.

template<typename T>
class ObjectInfo
{
    T data;
public:
    ObjectInfo(const T& d) : data(d) { }
    
    void Print()
    {
    	cout << "타입: " << typeid(data).name() << '\n';
        cout << "크기: " << sizeof(data) << '\n';
        cout << "값: " << data << '\n';
    }
};

int main()
{
    ObjectInfo<int> d1(10);
    d1.Print();
    
    ObjectInfo<double> d2(5.5);
    d2.Print();
    
    ObjectInfo<string> d3("Hello");
    d3.Print();
    
    return 0;
}

/*
타입 : int
크기 : 4
값 : 10

타입 : double
크기 : 8
값 : 5.5

타입 : class std::basic_string<char, struct std::char_traits<char>, class std::allocator<char>>
크기 : 32
값 : Hello!
*/

다른 타입과 다르게 string의 타입 정보가 조금 길게 출력된다. 사실 string도 템플릿 클래스로 typedef로 되어 있기 때문입니다. 또한, string 타입의 크기는 실제 객체의 크기(32)로 별 의미가 없다. 관심 가져야 할 것은 문자열 Hello!의 길이가 6인 것이다.

이제 ObjectInfo를 특수화해 string 정보만 출력하는 template<> class ObjectInfo<string> 클래스를 추가한다.

template<> // T를 string으로 특수화 (클래스 템플릿 특수화)
class ObjectInfo<string>
{
    string data;
public:
    ObjectInfo(const string& d) : data(d) { }
    
    void Print()
    {
        cout << "타입 : " << "string" << '\n';
        cout << "문자열 길이 : " << data.size() << '\n';
        cout << "값 : " << data << '\n';
        cout << '\n';
    }
};

int main()
{
    ObjectInfo<string> d3("Hello!");
    d3.Print();
}

/*
타입 : string
문자열 길이 : 6
값 : Hello!
*/

이제 string 객체인 d3는 특수화 버전의 ObjectInfo 객체로 문자열의 타입과 길이를 보기 좋게 출력한다.

Section 3. STL을 위한 템플릿 예제

For_each 예제

template<typename IterT, typename Func>
void For_each(IterT begin, IterT end, Func pf)
{
    while (begin != end)
    {
        pf(*begin++);
    }
}

template<typename T>
void Print(T data)
{
    cout << data << " ";
}

int main()
{
    int arr[5] = {10, 20, 30, 40, 50};
    For_each(arr, arr+5, Print<int>);
    cout << '\n';
   	
    string sarr[3] = {"abc", "ABCDE", "Hello!"};
    For_each(sarr, sarr+3, Print<string>);
    cout << '\n';
}

위 예제를 보면 주의할 점은 출력 함수의 템플릿 매개변수를 컴파일러가 유추할 수 없으므로 명시적으로 매개변수 인자(Print<int>, Print<string>)를 지정해야 한다.

마지막으로 출력 함수 Print()를 함수 객체로 바꾼다. 함수 객체를 사용하면 부가적인 서비스를 함수 객체가 제공가능하다.

template<typename T>
struct PrintFunctor
{
    string sep; // 출력 구분자 정보
    
    explicit PrintFunctor(const string& s=" "):sep(s) { }
    
    void operator() (T data) const
    {
        cout << data << sep;
    }
};

int main()
{
    int arr[5] = {10, 20, 30, 40, 50};
    For_each(arr, arr+5, PrintFunctor<int>());
}

함수 객체는 부가정보를 가질 수 있으므로 sep이라는 출력 패턴 구분자를 가진다. 정수는 디폴트 출력 구분자로 " " (공백 문자열을) 사용하며 문자열은 출력 구분자"*\n"을 사용.

이제 템플릿을 쉽게 사용할 수 있으므로 STL의 대표 조건자인 less와 greater를 작성해보자.

 

템플릿 Less, Greater

#include <iostream>
#include <functional>
using namespace std;

template<typename T>
struct Less
{
    bool operator() (T a, T b)
    {
    	return a < b;
    }
};

template<typename T>
struct Greater
{
    bool operator() (T a, T b)
    {
    	return a > b;
    }
};

이렇게만 짜도 STL의 less, greater와 동일하게 동작한다. 하지만, 다른 점은 STL의 less, greater가 표준 어댑터와 동작하도록 단순한 규칙을 가져야한다는 것이다.

템플릿의 매개변수와 함수 객체를 결합하면 반환 타입과 함수 매개변수 타입을 클라가 결정하는 유연한 함수 객체를 만들 수 있다.

 

반환 타입과 매개변수 타입을 인자로 갖는 함수 객체

template<typename RetType, typename ArgType>
class Functor
{
public:
    RetType operator() (Argtype data)
    {
    	cout << data << '\n';
        return RetType();
    }
};

int main()
{
    Functor<void, int> functor1;
    functor1(10); // 10출력
}

functor1은 void 반환 타입과 int 매개변수 타입을 갖는 함수 객체.

 

이제 pair 클래스를 템플릿으로 구현해 보겠습니다. pair 클래스는 두 객체를 하나의 객체로 취급할 수 있게 두 객체를 묶어줍니다. STL의 모든 쌍을 이루는 객체는 pair 객체를 사용. 대표적으로 map 컨테이너의 key, value의 쌍을 표현할 때나 구간의 시작과 끝을 표현할 때 사용.

template<typename T1, typename T2>
struct Pair
{
    T1 first;
    T2 second;
    Pair(const T1& ft, const T2& sd) : first(ft), second(sd) { }
};

int main()
{
    Pair<int, int> p1(10, 20);
    cout << p1.first << ',' << p1.second << '\n';
    
    pair<int, int> p2(10, 20);
    cout << p2.first << ',' << p2.second << '\n';
}

// 출력
// 10,20
// 10,20

 

이것만은 알고 갑시다.

1. 다음은 배열의 원소를 복사하는 함수 템플릿 Copy()의 호출 코드. 함수 템플릿 Copy()를 작성해보세요

int main()
{
    int arr1[5] = {10, 20, 30, 40, 50};
    int arr2[5];
    // Copy(t,s,n) t : 목적지 주소, s : 소스 주소, n : 원소 개수
    Copy(arr2, arr1, 5);
    
    MyType myArr1[5] = {...}
    MyType myArr2[5];
    Copy(myArr2, myArr1, 5);
    
    return 0;
}

// 답
template<typename T>
void Copy(T t[], const T s[], int size)
{
    for (int i=0; i<size; i++)
    	t[i] = s[i];
}

 

2. Push, Pop, Empty 인터페이스를 갖는 Stack을 만들어라

// 에러처리 생략
template<typename T>
class Stack
{
    T buf[100];
    int top;
public:
    Stack() : top(0) { }
    void Push(const T& data)
    {
    	buf[top++] = data;
    }
    const T& Pop ()
    {
    	return buf[--top];
    }
    bool Empty() const
    {
    	return top <= 0;
    }
};

 

3. Push, Pop, Empty 인터페이스를 갖는 Queue를 만들어라

template<typename T>
class Queue
{
    enum {CAP = 100};
    T buf[CAP];
    int front;
    int rear;
public:
    Queue() : front(0), rear(0) { }
    
    void Push(const T& data)
    {
    	buf[rear = (rear+1)%CAP] = data;
    }
    const T& Pop()
    {
    	return buf[front=(front+1)%CAP];
    }
    bool Empty() const
    {
        return rear == front;
    }
};