[C++] 13. 템플릿 #1

kkado·2023년 10월 18일
0

열혈 C++

목록 보기
13/16
post-thumbnail

💬 윤성우 님의 <열혈 C++ 프로그래밍> 책을 혼자 공부하며 배운 내용을 정리합니다. 글의 모든 내용은 책에서 발췌하였습니다.


템플릿 이해하기

템플릿(template)이라 함은 틀, 양식, 뭐 그런 뜻으로 사용된다. C++의 템플릿도 이와 성격이 유사하다.

함수 템플릿은 함수를 만드는 도구이다. 함수의 기능은 결정되어 있지만 자료형은 결정되어 있지 않다.

모형자와 다양한 색깔의 펜을 이용해서 다양한 색깔의 똑같은 도형을 그릴 수 있는 것처럼, 함수 템플릿도 다양한 자료형의 함수를 만들어낼 수 있다.

int Add(int n1, int n2)
{
	return n1 + n2;
}

가령, 위 함수의 기능은 '덧셈' 이고, int형 데이터를 대상으로 한다.
이러한 함수를 만들어낼 수 있는 템플릿은 다음과 같이 정의한다.

T Add(T num1, T num2)
{
	return num1 + num2;
}

직전에 보았던 함수와 비교해보면 int형 선언을 T로 했음을 알 수 있다. 이는 '자료형을 결정하지 않았다' 는 뜻이며, 나중에 T 대신에 실제로 사용할 자료형을 결정하겠다는 뜻이다.

따라서 위의 함수의 기능은 '덧셈' 이고, 대상 자료형은 '아직 결정되지 않음' 이라고 정의할 수 있다.

그리고 이 T라는 것이 '자료형을 결정하지 않았다' 라는 뜻을 나타내기 위해, 아래와 같이 template <typename T>을 붙여 완성한다.

template <typename T>
T Add(T num1, T num2)
{
	return num1 + num2;
}

template <typename T> 대신에 template <class T> 를 사용해도 된다. 그리고 꼭 T 문자를 안 써도 된다.

이제 함수 템플릿을 실제로 사용해보자.

template <typename T>
T Add(T num1, T num2)
{
	return num1 + num2;
}

int main()
{
    cout << Add<int>(15, 20) << "\n";
    cout << Add<double>(1.5, 5.4) << "\n";
    cout << Add<int>(3.2, 3.2) << "\n";
    cout << Add<double>(1.5, 5.41) << "\n";
}
35
6.9
6
6.91

함수명<사용할 자료형>() 의 형태로 사용한다.

컴파일러는 새로운 자료형을 만나면 컴파일 시 그 자료형을 이용해서 함수를 뚝딱 만든다. 같은 자료형을 여러 번 사용해도 여러 번 만드는 건 아니고 자료형마다 한 번씩 만든다.

컴파일을 할 때 함수가 만들어지면, 그만큼 속도가 느린 건 아닐까? 물론 컴파일 속도가 감소하긴 하지만, 실행할 때는 이미 만들어진 함수를 호출하는 것과 다를 바가 없다.

호출할 때 Add<int>(3, 4)가 아니라 Add(3, 4) 로 호출해도 된다. 컴파일러가 전달되는 인자를 참조하여 호출될 함수의 자료형을 결정하기 때문이다.

Add(1.2, 3.4); 를 호출하게 되면, 컴파일러는 '전달되는 인자의 자료형이 double 형이니, 함수 템플릿 T가 double형이 되어야 값의 손실이 없을 것' 이라 판단한다.


방금 살펴본 함수 템플릿을 기반으로 컴파일러가 만들어내는 함수들을 가리켜 '템플릿 함수' 라고 한다.

int Add<int> (int num1, int num2)
{
    return num1 + num2;
}

double Add<double> (double num1, double num2)
{
    return num1 + num2;
}

위 템플릿 함수의 표시에서 <int><double>은, 이 함수가 일반 함수가 아닌, 컴파일러가 만들어낸 템플릿 함수임을 표시하는 것이다.

템플릿 함수와 일반 함수는 구분이 되어서, 두 가지 모두 함께 존재할 수 있다.

template <typename T>
T Add(T num1, T num2)
{
    cout << "T Add(T n1, n2)\n";
	return num1 + num2;
}

int Add(int num1, int num2)
{
    cout << "int Add(T n1, n2)\n";
    return num1 + num2;
}

double Add(double num1, double num2)
{
    cout << "double Add(T n1, n2)\n";
    return num1 + num2;
}

int main()
{
    cout << Add(5, 7) << "\n";
    cout << Add(3.7, 7.5) << "\n";
    cout << Add<int>(5, 7) << "\n";
    cout << Add<double>(3.7, 7.5) << "\n";
}
int Add(T n1, n2)
12
double Add(T n1, n2)
11.2
T Add(T n1, n2)
12
T Add(T n1, n2)
11.2

<int> 처럼 자료형을 명시해 주면 템플릿 함수가 호출되고, 명시해주지 않으면 일반 함수가 호출된다.

그러나 이처럼 함수 템플릿을 정의한 상황에서 일반 함수까지 정의하는 것은 바람직하지 못하다.


둘 이상의 타입에 대한 템플릿 선언

함수 템플릿을 정의할 때는 다양한 자료형의 선언이 가능할 뿐만 아니라 둘 이상의 형에 대해서 템플릿을 선언할 수도 있다.

template <class T1, class T2>
void showData(double num)
{
    cout << (T1)num << ", " << (T2)num << "\n";
}

int main()
{
    showData<char, int>(65);
    showData<char, int>(67);
    showData<char, double>(68.9);
    showData<short, double>(69.2);
    showData<short, double>(70.4);
}
A, 65
C, 67
D, 68.9
69, 69.2
70, 70.4

위의 함수 템플릿은 매개변수형이 double 로 선언되었기 때문에 전달되는 인자를 가지고는 T1, T2의 자료형을 결정짓지 못하기 때문에 이러한 경우에는 템플릿의 함수를 호출할 때 호출 형식을 완전히 갖춰서 <int> 와 같이 그 자료형을 명시해 주어야 한다.


함수 템플릿의 특수화

다음 코드와 실행 결과를 관찰해보자.

template<typename T>
T Max(T a, T b)
{
    return a > b ? a : b;
}

int main()
{
    cout << Max(11, 15) << "\n";
    cout << Max('T', 'Q') << "\n";
    cout << Max(3.5, 7.5) << "\n";
    cout << Max("Simple", "Best") << "\n";
}
15
T
7.5
Best

전달받은 두 인자 중 큰 값을 리턴하는 Max 함수를 템플릿 함수로 정의했다.

그런데 문자열을 대상으로 호출할 경우 아무 의미가 없어지게 된다.
만약 문자열의 길이를 비교할 것이었다면 다음과 같이 구성됐어야 한다.

const char* Max(const char* a, const char* b)
{
	return strlen(a) > strlen(b)? a : b;
}

또는, 사전순 비교가 목적이었다면 다음과 같이 구성됐어야 한다.

const char* Max(const char* a, const char* b)
{
	return strcmp(a, b) > 0 ? a : b;
}

이렇듯, 상황에 따라서 템플릿 함수의 구성방법에 예외를 둘 필요가 있는데, 이 때 사용되는 것이 '함수 템플릿의 특수화(specialization of function template' 이다.

다음 예제를 살펴보면서 알아보자

template<typename T>
T Max(T a, T b)
{
    return a > b ? a : b;
}

template <>
char* Max(char* a, char* b)
{
    cout << "char* Max<char*> (char* a, char* b)\n";
    return strlen(a) > strlen(b) ? a : b; 
}

template <>
const char* Max(const char* a, const char* b)
{
    cout << "const char* Max<const char*> (const char* a, const char* b)\n";
    return strcmp(a, b) > 0 ? a : b;
}

int main()
{
    cout << Max(11, 15) << "\n";
    cout << Max('T', 'Q') << "\n";
    cout << Max(3.5, 7.5) << "\n";
    cout << Max("Simple", "Best") << "\n";

    char str1[] = "Simple";
    char str2[] = "Best";
    cout << Max(str1, str2) << "\n";
}
15
T
7.5
const char* Max<const char*> (const char* a, const char* b)
Simple
char* Max<char*> (char* a, char* b)
Simple

함수 템플릿 Max를 char*형, const char*형에 대해 특수화하였다.

문자열의 선언으로 인해 반환되는 주소 값의 포인터형은 const char* 이다. 따라서 이 문장에 의해 호출되는 함수는 const char* Max(const char* a, const char* b) 이다.
str1와 str2의 포인터형은 char* 이다. 따라서 이 문장에 의해 호출되는 함수는 char* Max(char* a, char* b) 이다.

함수 템플릿을 특수화한다는 것은 컴파일러에게 'char*형 함수는 내가 이렇게 제시를 하니, char* 형 템플릿 함수가 필요한 경우에는 별도로 만들지 말고 이것을 써라.' 고 알려주는 것이다.

특수화된 두 함수는 자료형 타입이 생략된 것으로, 생략하지 않고 작성하면 char* Max<char*>(char* a, char* b) 와 같이 작성할 수 있다. 자료형을 생략하든 말든 그 의미는 차이가 없으나 가급적이면 뜻을 명확히 하기 위해 생략하지 않는 것이 좋다.


클래스 템플릿

함수를 템플릿으로 정의했듯이 클래스도 템플릿으로 정의할 수 있다. 그리고 클래스 템플릿을 기반으로 컴파일러가 만들어내는 클래스를 템플릿 클래스라고 한다.

먼저 예시가 될 간단한 Point 클래스이다. 여러 번 다룬 적이 있다.

class Point
{
private:
    int xpos, ypos;
public:
    Point(int x=0, int y=0) : xpos(x), ypos(y)
    {}

    void showPoisition() const
    {
        cout << "[" << xpos << ", " << ypos << "]\n";
    }
};

이 클래스를 템플릿을 이용해서 만들어보면 다음과 같다.

template <typename T>
class Point
{
private:
    T xpos, ypos;
public:
    Point(T x=0, T y=0) : xpos(x), ypos(y)
    {}

    void showPoisition() const
    {
        cout << "[" << xpos << ", " << ypos << "]\n";
    }
};

사용되는 자료형을 T형으로 바꿔주는 것이 전부다. 전반적으로 함수 템플릿과 만드는 방벙비 비슷하다.

이제 위의 클래스 템플릿을 사용해서 객체를 생성해보자!

int main()
{
    Point<int> pos1(3, 4);
    Point<double> pos2(1.2, 3.4);

    pos1.showPoisition();
    pos2.showPoisition();
}
[3, 4]
[1.2, 3.4]

사용법 역시 함수 템플릿을 사용할 때와 유사하며 직관적이라 이해하기 쉬운 것 같다.

그러나 아쉽게도 템플릿 클래스는 생성할 때 자료형 정보를 생략할 수 없다.


클래스 템플릿의 선언과 정의의 분리

클래스 템플릿 역시 일반 클래스와 마찬가지로 멤버를 클래스 외부에 정의하는 것이 가능하다.

template <typename T>
class SimpleTemplate
{
public:
    T simpleFunc(const T& ref);
};

template <typename T>
T SimpleTemplate<T>::simpleFunc(const T& ref) {
    ...
}

SimpleTempalte<T>는 'T에 대해 템플릿화된 SimpleTempalte 클래스 템플릿' 이라는 뜻이다. 그리고 외부 함수의 경우에도 template <typename T> 를 명시해주지 않으면 컴파일러는 대관절 T가 무엇인지 알 수 없으니 에러를 발생시킨다.

그리고 기존의 클래스에서도 그랬듯이, 파일을 나누어 보자.
먼저 PointTemplate.h 헤더 파일이다.

#ifndef __POINT_TEMPLATE_H_
#define __POINT_TEMPLATE_H_

template<typename T>
class Point
{
private:
    T xpos, ypos;
public:
    Point(T x=0, T y=0);
    void showPosition() const;
};

#endif

그리고 멤버함수가 정의되어 있는 PointTemplate.cpp 파일이다.

#include <iostream>
#include "PointTemplate.h"
using namespace std;

template <typename T>
Point<T>::Point(T x, T y) : xpos(x), ypos(y)
{}

template <typename T>
void Point<T>::Point::showPosition() const
{
    cout << "[" << xpos << ", " << ypos << "]\n";
}

그리고 이렇게 정의된 Point 클래스를 사용하는 PointMain.cpp 파일이다.

#include <iostream>
#include "PointTemplate.h"
using namespace std;

int main()
{
    Point<int> pos1(3, 4);
    Point<double> pos2(1.2, 3.4);
    Point<char> pos3('P', 'F');

    pos1.showPosition();
    pos2.showPosition();
    pos3.showPosition();
}

아무 문제 없을 것 같지만! 컴파일 해보면 문제가 발생한다.


파일을 나눌 시 고려할 사항

컴파일은 파일 단위로 이뤄진다는 사실을 기억할 것이다. 그리고 다시 PointMain 파일을 살펴보자.

이 파일이 컴파일될 때 컴파일러는 3개의 템플릿 클래스를 생성해야 한다. 그리고 이를 위해서는 Point 클래스를 모두 알아야 한다. 즉 헤더 파일뿐만 아니라 PointTemplate.cpp 에 있는 정보 역시 필요하다는 것.

그러나 main 함수가 정의된 부분에는 PointTemplate.cpp 부분을 참조할 수 있는 선언이 되어 있지 않다.

그런데 컴파일러가 PointMain.cpp를 컴파일 할 때 PointTemplate.cpp도 함께 컴파일을 하니까, 컴파일러는 PointTemplate.cpp의 내용도 알고 있는 것이 아닌가? 라는 궁금증이 생길 수 있다.

둘이 동시에 컴파일 되는 것은 맞지만 이 둘은 서로 다른 소스 파일이기 때문에 파일 단위 컴파일 원칙에 의해 컴파일 시에 서로의 코드를 참조하지 않는다. 따라서 에러가 발생한다.

다행히 간단하게 두 가지 해결책을 제시할 수 있다.
1. 헤더 파일에 템플릿 Point의 생성자와 멤버 함수를 모두 넣는 것 (클래스의 선언부와 정의부를 나누지 않는 것)
2. main.cpp에다 #include "PointTemplate.cpp" 을 추가하는 것

소스 파일을 추가한다니 조금 이상하겠지만 템플릿의 경우에는 이렇게 해야한다.


배열 클래스의 템플릿화

이전에 배열 인덱스 연산자의 오버로딩에 대해서 배울 때 배열에 담길 자료형에 따라 클래스를 일일이 따로 만들어 주었었는데, 이제 클래스 템플릿을 배웠으니 여러 자료형을 포괄할 수 있는 템플릿을 만들 수 있을 것이다.

ArrayTemplate.h

#ifndef __POINT_TEMPLATE_H_
#define __POINT_TEMPLATE_H_

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

template<typename T>
class BoundCheckArray
{
private:
    T* arr;
    int arrlen;
public:
    BoundCheckArray(int n): arrlen(n)
    {
        arr = new T[n];
    }
    ~BoundCheckArray()
    {
        delete []arr;
    }

    T& operator[] (int idx) 
    {
        if(idx < 0 || idx >= arrlen)
        {
            cout << "Index out of range exception\n";
            exit(1);
        }
        return arr[idx];
    }

    T& operator[] (int idx) const
    {
        if(idx < 0 || idx >= arrlen)
        {
            cout << "Index out of range exception\n";
            exit(1);
        }
        return arr[idx];
    }

    int getArrlen()
    {
        return arrlen;
    }
};

#endif

멤버 함수의 정의부를 따로 분리하지 않고 클래스 안에서 정의하였다.
따라서 이 헤더 파일 하나만 include 하면 템플릿 기반 객체를 생성할 수 있다. 이제 이어서 컴파일 할 파일들을 보자

Point.h

#ifndef __POINT_TEMPLATE_H_
#define __POINT_TEMPLATE_H_

#include <iostream>
using namespace std;

class Point
{
private:
    int xpos, ypos;
public:
    Point(int x=0, int y=0);
    friend ostream& operator<< (ostream& os, const Point& pos)
};

#endif

Point.cpp

#include <iostream>
#include "Point.h"
using namespace std;

Point::Point(int x, int y) : xpos(x), ypos(y) {}

ostream& operator<<(ostream& os, const Point& pos)
{
    os << "[" << pos.xpos << ", " << pos.ypos << "]\n";
}

BoundArrayMain.cpp

#include <iostream>
#include "ArrayTemplate.h"
#include "Point.h"
using namespace std;

int main()
{
    BoundCheckArray<int> iarr(5);
    for(int i=0;i<5;i++)
    {
        iarr[i] = (i+1) * 11;
    }

    for(int i=0;i<5;i++)
    {
        cout << iarr[i] << "\n";
    }

    BoundCheckArray<Point> oarr(3);
    oarr[0] = Point(3, 4);
    oarr[1] = Point(5, 6);
    oarr[2] = Point(7, 8);
    for(int i=0;i<oarr.getArrlen();i++)
    {
        cout << oarr[i];
    }
    
    typedef Point* POINT_PTR;
    BoundCheckArray<POINT_PTR> parr(3);
    parr[0] = new Point(3, 4);
    parr[1] = new Point(5, 6);
    parr[2] = new Point(7, 8);
    
    delete parr[0];
    delete parr[1];
    delete parr[2];
}
11
22
33
44
55
[3, 4]
[5, 6]
[7, 8]
[3, 4]
[5, 6]
[7, 8]

profile
울면안돼 쫄면안돼 냉면됩니다

0개의 댓글