Notice
Recent Posts
Recent Comments
Link
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
Archives
Today
Total
05-16 04:51
관리 메뉴

nomad-programmer

[Programming/C++] 템플릿 (Template) 본문

Programming/C++

[Programming/C++] 템플릿 (Template)

scii 2023. 1. 26. 00:49

템플릿은 C++에서 없어서는 안될 아주 중요한 기능인데 템플릿을 사용하면 컴파일러가 개발자를 대신해서 클래스나 함수를 만들어내게 할 수 있다. 그리고 C++에는 이러한 템플릿을 사용해서 만들어진 다양한 클래스와 함수들을 STL이라는 이름으로 제공하고 있다.

왜 템플릿을 사용해야 할까?

템플릿은 C++에서 가장 눈에 띄는 기능이라고 할 수 있다. C++의 다른 기능들은 Java와 C# 같은 객체지향 언어에서도 대부분 지원하는 것들이지만 템플릿 만큼은 C++이 아니면 찾아볼 수 없는 독특한 기능이기 때문이다.

템플릿은 함수를 자동으로 생성하는 것에 더해서 클래스까지 자동으로 생성해줄 수 있다. 그리고 이 두 가지를 구분하기 위해서 템플릿 클래스, 템플릿 함수 등으로 나누어 부르게 된다.

템플릿 클래스

#include <iostream>

using namespace std;

class AutoArray {
public:
    AutoArray(int* ptr) : _ptr(ptr) {}
    ~AutoArray() {
        delete[] _ptr;
    }

    // []연산자를 오버로딩했다. 반환 값이 단순히 int가 아니라 int& 라는 점을 주의하자.
    // int&로 반환 값을 정의해야 arr[0] = 100; 처럼 대입이 가능하다.
    int& operator[](const int index) const {
        return _ptr[index];
    }

private:
    int* _ptr;
};


int main() {
    AutoArray arr(new int[100]);

    arr[0] = 100;

    return 0;
}

위의 소스 코드를 보면 알겠지만 AutoArray 클래스는 int 타입의 배열만을 관리할 수 있다. 만약 double 타입의 배열이나 구조체 혹은 클래스 타입의 배열을 관리할 수 있는 스마트 포인터 클래스가 필요해진다면 AutoArray 클래스와 비슷한 클래스를 여러 개 만들어야 할 것이다.
하지만 그 방법은 매우 많은 문제점을 가지고 있다. 또 다른 클래스를 만들어야 하는 작업 시간도 문제지만 가장 큰 문제는 같은 내용의 소스 코드가 중복된다는 점이다. 다루는 배열의 타입이 다를 뿐 그 클래스들이 하는 일은 모두 동일할 것이기 때문이다. 이런 경우에 가장 좋은 해결책은 AutoArray를 템플릿 클래스로 만드는 일이다. 
AutoArray 클래스에 약간의 수정을 가하면 모든 타입의 배열을 관리할 수 있는 스마트 포인터로 거듭날 수 있다. 다음의 예제는 AutoArray를 템플릿 클래스로 변환한 모습이다.

#include <iostream>

using namespace std;

template <typename T>
class AutoArray {
public:
    AutoArray(T* ptr) : _ptr(ptr) {}
    ~AutoArray() {
        delete[] _ptr;
    }

    T& operator[](const int index) const {
        return _ptr[index];
    }

private:
    T* _ptr;
};


int main() {
    AutoArray<int> arr_int(new int[100]);
    arr_int[0] = 100;

    AutoArray<double> arr_double(new double[55]);
    arr_double[0] = 3.14;

    return 0;
}

템플릿 클래스를 만들기 위해서는 template 키워드를 사용한다. 'typename T' 부분은 컴파일러에게 T 대신에 int나 float 등 여러 가지 종류의 타입 이름이 올 수 있다는 것을 알려주는 것이다.
AutoArray를 만들기 위해서 AutoArray<double>과 같은 표현을 사용했다. 이것은 T 대신에 double을 대입한 것이라고 생각하면 된다.

템플릿의 기본 개념은 아주 간단하다. 무엇일 될지 모르는 타입을 T와 같은 변수로 두고 T를 기준으로 템플릿 클래스를 작성하는 것이다. 그리고 템플릿 클래스를 사용할 때 T 대신에 구체적인 타입을 명시해준다. 위의 코드처럼 AutoArray<int> 와 같이 사용하게되면 그제서야 컴파일러는 T 대신에 int를 넣어 새로운 클래스를 만들게 된다. 물론 그렇게 만들어진 소스 코드가 눈에 보이는 것은 아니지만 내부적으로는 그 클래스가 존재하고 있다.

위의 코드에서 나오는 T를 템플릿 매개 변수(Template Parameter)라고 부르는데 템플릿 매개 변수는 얼마든지 많이 올 수 있다. 예를 들어 서로 다른 타입의 배열 두 개를 보관할 수 있는 템플릿 클래스는 다음과 같이 만들 수 있다.

template<typename A, typename B, int MAX>
class TwoArray {
    ...
    A arr1[MAX];
    B arr2[MAX];
};

TwoArray<char, float, 55> arr;

'TwoArray<char, float, 55> arr' 라는 부분이 나오는데 이 코드를 보고 내부적으로 다음과 같은 클래스를 하나 만들게 된다.

class TwoArray_char_float_55 {
    ...
    char arr1[55];
    float arr2[55];
};

클래스의 이름은 임의로 만든 것이고, 중요한 것은 A, B, MAX라고 적혀있던 부분이 char, float, 55로 바뀌었다는 점이다. 템플릿 매개 변수의 개수가 많아졌지만 기본적인 개념은 같다.

한 가지 특이한 점은 typename 대신 class라고 적어주어도 무방하다. 어느 것을 사용하건 아무런 차이가 없다.


템플릿 함수

다음의 예제는 모든 타입에 대해서 사용할 수 있는 max() 함수이다.

#include <iostream>

using namespace std;

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

int main() {
    int i1 = 5, i2 = 3;
    int i3 = Max(i1, i2);

    double d1 = 0.9, d2 = 3.14;
    double d3 = Max<double>(d1, d2);

    cout << "i3: " << i3 << endl;
    cout << "d3: " << d3 << endl;

    return 0;
}


// 결과
i3: 5
d3: 3.14

임의의 타입 T를 사용해서 Max() 함수를 작성했고, main() 함수에서는 int타입과 double 타입의 인자를 사용해서 Max() 함수를 호출했다. 템플릿 클래스와 크게 다른 점은 없지만 한 가지 눈에 띄는 부분이 있다. 바로 Max() 함수를 호출하는 부분인데 클래스 템플릿처럼 Max<int>(i1, i2) 처럼 사용하는 것이 맞다. T 대신에 대입할 타입을 적어주어야 하기 때문이다. 하지만 템플릿 함수의 경우에는 위의 소스 코드처럼 타입을 명시하지 않고 그냥 사용하는 것이 가능하다.
왜냐하면 입력하는 인자의 타입을 통해 T 대신 어떤 타입을 대입해야 하는지 유추할 수 있기 때문이다. 예를 들어 int 타입의 인자를 사용했기 때문에 T 대신에 int를 사용해야 한다는 사실을 알 수 있고, 컴파일러는 T 대신에 int를 대입해서 새로운 Max() 함수를 만들어 낸다.


템플릿 사용 시 유의할 점

템플릿은 컴파일 시간에 코드를 만들어 낸다.

컴파일 시간(Compile Time)과 실행 시간(Run Time)이라는 용어가 있다. 컴파일 시간이란 소스 코드를 컴파일하고 있는 그 순간을 의미하여 실행 시간이란 컴파일 한 프로그램이 실제로 컴퓨터 상에서 실행되고 있는 순간을 의미한다. 그렇기 때문에 "템플릿은 컴파일 시간에 코드를 만들어낸다" 라는 말은 "템플릿은 프로그램이 실행되는 도중이 아니라, 소스 코드를 컴파일하는 도중에 클래스나 함수를 만들어 낸다" 라는 뜻이 된다.

템플릿이 새로운 클래스나 함수를 만드는 동작을 컴파일 시간에 수행한다는 것은 중요한 의미가 있다. 만약에 프로그램이 실행되는 도중에 클래스나 함수를 만들게 되면 프로그램의 실행 속도가 느려지기 때문이다. 하지만 프로그램이 실행되기 전, 미리 만들어져 있기 때문에 템플릿을 많이 사용하더라도 프로그램이 느려지는 일은 없다.

대신에 컴파일하는 도중 클래스나 함수를 생성해야 하므로 컴파일이 오래 걸리는 단점이 생기기는 한다. 하지만 이는 충분히 감당할 수 있는 단점이다. 매번 실행할 때마다 느려지는 것보다 컴파일할 때 한 번만 느려지는 것이 훨씬 낫기 때문이다. 그리고 컴파일에 걸리는 시간이 심각할 정도로 오래 걸리는 것도 아니다.

템플릿 함수의 정의는 헤더 파일에 놓여야 한다.

소스 코드가 여러 파일로 이루어져 있는 경우 함수의 정의를 구현 파일에 놓는 것이 일반적이다. 만약에 함수의 정의를 헤더 파일에 놓게 되면 그 헤더 파일을 포함하는 다른 여러 구현 파일에서 중복 정의되는 결과를 낳기 때문이다.

하지만 템플릿 함수는 반드시 헤더 파일에 놓여야 된다. 템플릿 함수를 정의하는 것의 의미는 보통의 함수를 정의하는 것과는 다른 의미가 있기 때문이다. 템플릿 함수를 정의하는 것은 실제로 함수를 정의하는 의미라기보다는 컴파일러에게 함수를 만드는 방법을 가르쳐주는 의미를 갖기 때문에 이런 예외적인 규칙을 갖게 된다.

물론 여기서 말하는 템플릿 함수란 일반 함수와 멤버 함수 모두를 말하는 것이다. 중요한 것은 템플릿 클래스의 일반 멤버 함수도 여기에 포함된다는 것이다. 다시 말해 템플릿 클래스의 멤버 함수는 템플릿 함수가 아니더라도 헤더 파일에 위치해야 한다.

  • 일반적인 템플릿 함수
  • 템플릿 멤버 함수
  • 템플릿 클래스의 멤버 함수

템플릿 함수를 구현 파일에 놓을 수도 있는 방법

템플릿 함수의 정의를 헤더 파일에 놓게 되면 소스 코드가 지저분해지기 쉽다. 그래서 C++에서는 템플릿 함수의 정의를 구현 파일에 놓을 수 있게 export라는 키워드를 제공한다. 템플릿 함수의 정의를 구현 파일에 놓고 그 앞에 export라는 키워드를 붙여주면 되는 것이다.

Comments