티스토리 뷰

주의 사항!

  • 이 글은 제가 직접 공부하는 중에 작성되고 있습니다.
  • 따라서 제가 이해하는 그대로의 내용이 포함됩니다.
  • 따라서 이 글은 사실과는 다른 내용이 포함될 수 있습니다.

 

함수 템플릿

'함수 템플릿'은 다음과 같은 특징을 가지고 있습니다.

  • 함수 템플릿은 함수를 만들어 낸다. 함수의 기능은 결정되어 있지만, 자료형은 결정되어 있지 않아서 결정해야 한다.

모형자가 모형을 만드는 도구가 되는 것처럼 함수 템플릿은 함수를 만드는 도구가 됩니다. 하지만 함수 템플릿이 만들어 내는 함수의 자료형이 결정되어 있지 않습니다. 우리는 이 함수 템플릿을 이용해서 다양한 자료형의 함수를 만들어 낼 수 있습니다.

 

다음의 예를 통해서 함수 템플릿을 구체적으로 이해해 보겠습니다.

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

 

위 함수의 기능과 자료형은 쉽게 파악할 수 있을 것입니다. 위 함수는 전달받은 두 인자를 덧셈하는 기능을 가지며, int형 데이터를 다루고 있습니다. 위 함수를 만들어 낼 수 있는 템플릿이 있다면 다음과 같이 정의될 것입니다.

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

 

템플릿과 위 함수를 비교해 보면 int를 T로 바꾸었다는 것을 바로 알 수 있습니다. 이는 자료형을 지금 정하지 않고, 나중에 T를 이용해 실제 자료형을 결정하겠다는 의미입니다. 다만 이런 의미를 전달하기 위해서 문장 하나를 추가하여 다음과 같이 함수 템플릿을 완성해야 합니다.

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

 

위 템플릿에서 다음의 문장은,

template <typename T>

 

T라는 이름을 이용해서 함수를 템플릿으로 정의한다는 의미를 가지고 있습니다.

 

이제 앞서 정의한 함수 템플릿을 이용해서 함수를 만들어 보겠습니다. 만들 함수는 int형 Add함수와 double형 Add함수입니다. 다음의 예제를 보겠습니다.

//main.cpp 소스 파일로 저장
#include <iostream>
using namespace std;

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

int main(void)
{
	cout << Add<int>(15, 20) << endl;
	cout << Add<double>(2.9, 3.7) << endl;
	cout << Add<int>(3.6, 3.8) << endl;
	cout << Add<double>(3.14, 2.75) << endl;

	return 0;
}

/*
실행결과

35
6.6
6
5.89

*/

 

템플릿을 이용해 함수를 만드는 방법은 위 예제를 통해 쉽게 알 수 있습니다. 호출할 함수명 뒤에 <>를 이용해서 T에 적용할 자료형을 입력해줍니다.

 

위 예제에서는 int형 함수가 2번, double형 함수가 2번 만들어진 것으로 보입니다. 하지만 int형 함수와 double형 함수는 각각 1개씩만 생성됩니다. 처음 int형 함수를 생성하고, 다시 int형 함수를 생성하려고 할 때, 이미 만들어진 int형 함수가 있으면 해당 함수를 호출하기 때문입니다.

 

템플릿을 통해 함수를 만들게 되면 그 과정에서 속도가 조금 늦어질 수 있습니다. 하지만 걱정할 필요는 없습니다. 함수를 만드는 과정은 컴파일 단계에서 수행합니다. 때문에 프로그램 실행 단계에서 템플릿의 사용으로 인해 프로그램의 실행 속도가 늦어질 일은 절대 없습니다.

 

자료형을 명시하지 않고 템플릿 함수 호출

아마 다음과 같이 함수를 호출하는 일이 조금 번거로울 수 있습니다.

Add<double>(3.14, 2.97);

 

하지만 위와 같이 T의 자료형을 명시하지 않고도 다음과 같이 함수를 호출할 수도 있습니다.

Add(3.14, 2.97);

 

위와 같이 T의 자료형을 명시하지 않으면 컴파일러는 매개 변수에 전달된 인자의 자료형을 바탕으로 T의 자료형을 판단합니다. 위 함수는 매개 변수에 double형 인자가 주어졌으므로 컴파일러는 T를 double형으로 판단합니다.

따라서 앞서 소개한 예제는 다음과 같이 바꿀 수도 있습니다.

//main.cpp 소스 파일로 저장
#include <iostream>
using namespace std;

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

int main(void)
{
	cout << Add(15, 20) << endl;
	cout << Add(2.9, 3.7) << endl;
	cout << Add(3.6, 3.8) << endl;
	cout << Add(3.14, 2.75) << endl;

	return 0;
}

/*
실행결과

35
6.6
7.4
5.89

*/

 

앞서 보았던 예제와 위 예제를 비교해보면 실행 결과의 세 번째 값이 바뀌어 있습니다. 앞선 예제에서는 3.6과 3.8의 합을 구하는 함수를 호출하는데 T의 자료형으로 int를 명시해주었습니다. 따라서 3.6과 3.8을 int형 매개변수에 전달받으면서 각각 3으로 데이터 손실이 발생했으며 3 + 3이 연산되어 6이 결과로 반환되었습니다.

 

하지만 위 예제에서는 T의 자료형을 명시해주지 않았습니다. 따라서 컴파일러가 매개 변수에 전달된 값을 바탕으로 T를 double 형으로 판단했고 정상적으로 3.6 + 3.8이 연산되어 7.4가 결과로 반환되었습니다.

 

일반 함수와 템플릿 함수의 우선순위 비교

함수 템플릿을 바탕으로 컴파일러가 만든 '템플릿 함수'는 일반 함수와 구분이 됩니다. 즉, 함수명과 매개 변수의 개수, 자료형이 모두 같아도 일반 함수이냐 템플릿 함수이냐로 컴파일러는 구분할 수 있다는 뜻입니다. 다음의 예제를 통해 확인해 보겠습니다.

//main.cpp 소스 파일로 저장
#include <iostream>
using namespace std;

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

int Add(int num1, int num2)
{
	cout << "Add(int num1, int num2)" << endl;
	return num1 + num2;
}

double Add(double num1, double num2)
{
	cout << "Add(double num1, double num2)" << endl;
	return num1 + num2;
}

int main(void)
{
	cout << Add(15, 20) << endl;
	cout << Add(2.9, 3.7) << endl;
	cout << Add<int>(4, 5) << endl;
	cout << Add<double>(3.14, 2.75) << endl;

	return 0;
}

/*
실행결과

Add(int num1, int num2)
35
Add(double num1, double num2)
6.6
T Add(T num1, T num2)
9
T Add(T num1, T num2)
5.89

*/

 

main 함수의 Add(15, 20) 함수를 호출하는 부분을 예로 설명하겠습니다. 해당 문장을 컴파일러가 읽으면 컴파일러가 어떤 함수를 호출해야 하는지 생각해 보겠습니다. 우선 템플릿 함수를 호출할 수도 있습니다. T 자료형을 명시해주지 않았으나 매개 변수로 주어진 데이터를 바탕으로 T형을 int형으로 판단할 수 있습니다. 또, int형으로 정의된 일반 함수를 호출할 수도 있습니다. 따라서 컴파일러가 호출할 수 있는 함수는 템플릿 함수와 일반 함수 두 개입니다.

 

실행 결과를 보면 컴파일러는 일반 함수를 선택해서 호출했습니다. 이로써 일반 함수와 템플릿 함수 중 일반 함수의 우선순위가 더 높다는 것을 알 수 있습니다. 하지만 위 예제처럼 템플릿 함수와 일반 함수를 같이 정의하는 것은 옳지 못합니다. 

 

둘 이상의 자료형을 갖는 함수 템플릿 선언

함수 템플릿을 정의할 때에는 기본 자료형 선언을 못 하는 것으로 오해하는 경우가 있는데, 템플릿의 정의에도 다양한 자료형의 선언이 가능할 뿐만 아니라, 둘 이상의 형(type)에 대해서 템플릿을 선언할 수도 있습니다. 다음 예제를 보겠습니다.

//main.cpp 소스 파일로 저장
#include <iostream>
using namespace std;

template <class T1, class T2>
void ShowData(double num)
{
	cout << (T1)num << ", " << (T2)num << endl;
}

int main(void)
{
	ShowData<char, int>(65);
	ShowData<char, int>(65);
	ShowData<char, double>(68.9);
	ShowData<short, double>(69.2);
	ShowData<short, double>(70.4);

	return 0;
}

/*
실행결과

A, 65
A, 65
D, 68.9
69, 69.2
70, 70.4

*/

 

위 예제를 보면 함수 템플릿의 매개 변수에 double과 같은 기본 자료형을 사용했고, 함수의 내용을 보면 num를 각각 T1, T2의 형태로 형 변환하는 모습을 볼 수 있습니다.

 

또 T1, T2를 typename이 아닌 class를 사용해서 선언했습니다. 이때 사용된 class는 클래스를 선언할 때 사용하는 것과는 전혀 다릅니다. 여기서 사용된 class는 typename과 같은 역할을 합니다. 따라서 위 예제에서 class 대신 typename을 사용해도 정상적으로 컴파일되고 실행됩니다.

 

C++의 초창기에는 템플릿의 선언에 키워드 class만 사용했었습니다. 그런데 이는 클래스의 선언에 사용되는 키워드와 동일하다는 지적에 의해 이후에 typename이라는 키워드도 함께 사용할 수 있도록 정정되었습니다. 그러나 최근에 만들어진 국내외 자료들을 보면 여전히 키워드 class가 많이 사용되는 것을 알 수 있습니다. 이유는 typename보다 class가 입력하기 더 편하기 때문입니다.

 

(저는 이렇게 생각합니다. class는 확실히 클래스의 선언에 사용되는 키워드와 동일하다는 지적에 동일합니다. 그리고 typename이라는 키워드가 class와 완전히 동일하게 동작하도록 추가되었으므로, 앞으로는 typename을 사용하는 습관을 들이는 것이 좋다고 생각합니다. 다만, 다른 개발자의 코드나 라이브러리를 읽을 때 class 키워드가 사용되어 있어도 혼동되지 않도록 class라는 키워드도 있다는 것 정도만 알고 가면 좋을 것 같습니다.) 

 

함수 템플릿의 특수화

먼저 다음 예제를 보겠습니다.

//main.cpp 소스 파일로 저장
#include <iostream>
using namespace std;

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

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

	return 0;
}

/*
실행결과

15
T
7.5
Simple

*/

 

위 예제의 함수 템플릿 Max는 인자로 전달받은 두 값 중에 더 큰 값을 반환합니다. 그렇다면 Best와 Simple의 크기를 비교할 때 무엇을 기준으로 비교를 해야 할까요? 문자열의 길이를 기준으로 비교할 수도 있고, 사전 순서에 따라서 비교할 수도 있고, 아니면 문자열의 주소 값을 기준으로 비교할 수도 있습니다. 

 

확실한 건 위 코드에서 문자열을 무엇을 기준으로 비교하는지 개발자가 알 수 없다는 것입니다. 만약에 문자열의 길이 비교가 목적이라면, 다음의 형태로 템플릿 함수가 구성되어야 합니다.

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;
}

 

이렇듯 상황에 따라서 템플릿 함수의 구성 방법에 예외를 둘 필요가 있는데, 이때 사용되는 것이 '함수 템플릿의 특수화'입니다. 다음 예제를 통해 문자열의 길이와 사전 순서 비교에 대해서 함수 템플릿의 특수화를 진행해 보겠습니다.

//main.cpp 소스 파일로 저장
#include <iostream>
#include <cstring>
using namespace std;

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

template <>
char* Max(char* a, char* b)
{
	cout << "문자열 길이 비교" << endl;
	return (strlen(a) > strlen(b)) ? a : b;
}

template <>
const char* Max(const char* a, const char* b)
{
	cout << "사전순서 비교" << endl;
	return (strcmp(a, b) > 0) ? a : b;
}

int main(void)
{
	cout << Max(11, 15) << endl;
	cout << Max('T', 'Q') << endl;
	cout << Max(3.5, 7.5) << endl;
	cout << Max("Beast", "Son") << endl;
	char str1[] = "Beast";
	char str2[] = "Son";
	cout << Max(str1, str2) << endl;

	return 0;
}

/*
실행결과

15
T
7.5
사전순서 비교
Son
문자열 길이 비교
Beast

*/

 

위 예제에서 다음 문장이 가지는 의미는,

template <>
char* Max(char* a, char* b)
{
	cout << "문자열 길이 비교" << endl;
	return (strlen(a) > strlen(b)) ? a : b;
}

 

"char* 형 함수는 내가 이렇게 제시를 하니, char* 형 템플릿 함수가 필요한 경우에는 별도로 만들지 말고 이것을 써라."입니다. 마찬가지로 다음의 문장은,

template <>
const char* Max(const char* a, const char* b)
{
	cout << "사전순서 비교" << endl;
	return (strcmp(a, b) > 0) ? a : b;
}

"const char* 형 함수는 내가 이렇게 제시를 하니, const char* 형 템플릿 함수가 필요한 경우에는 별도로 만들지 말고 이것을 써라."와 같은 의미를 가집니다.

 

하지만 위에서 보인 특수화는 만족스럽지 않습니다. const char*형을 매개 변수로 주었을 때에도 문자열의 길이를 비교하고 싶을 수도 있지 않을까요? 이 문제를 어떻게 해결하면 좋을까요? 저는 아래와 같이 비교 기준을 제시하는 함수들을 정의하고, 이를 이용해 값이 큰 변수를 반환하는 함수를 정의해서 구현했지만 이 역시 만족스럽지는 않습니다. 자바나 코틀린과 같이 람다식을 이용해서 좀 더 편하고 범용성 있게 구현할 수는 없을까요?

#include <iostream>

using namespace std;

bool LengthComparison(const char* str1, const char* str2)
{
	return (strlen(str1) >= strlen(str2)) ? true : false;
}

bool DictionaryOrderComparison(const char* str1, const char* str2)
{
	return (strcmp(str1, str2) >= 0) ? true : false;
}

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

const char* Max(const char* a, const char* b, bool (*func)(const char*, const char*))
{
	return (func(a, b)) ? a : b;
}

int main()
{
	cout << Max("hello", "koey", LengthComparison) << endl;
	cout << Max("hello", "koey", DictionaryOrderComparison) << endl;
}

/*
실행 결과

hello
koey

*/

 

클래스 템플릿

앞서 함수를 템플릿으로 정의했듯이 클래스도 템플릿으로 정의가 가능합니다. 그리고 이렇게 정의된 템플릿을 가리켜 '클래스 템플릿'이라고 하며, 이를 기반으로 컴파일러가 만들어 내는 클래스를 가리켜 '템플릿 클래스'라고 합니다.

 

앞서 C++를 공부하면서 다음과 같은 배열 클래스를 정의한 적이 있습니다. 

class BoundCheckIntArray {......};         //int 형 변수를 저장할 배열
class BoundCheckPointArray {......};       //Point 객체를 저장할 배열
class BoundCheckPointPtrArray {......};    //Point 객체 포인터를 저장할 배열

 

배열 클래스를 위와 같이 세 개씩이나 정의해야 했던 이유는 바로 이들이 저장하는 데이터의 자료형이 달랐기 때문입니다. 기능과 내부의 행동은 모두 같은데 다루는 데이터의 자료형이 다르다는 이유만으로 유사한 클래스를 세 개씩이나 정의한다는 것은 불합리하게 느껴집니다.

 

눈치챘겠지만 이런 문제는 클래스 템플릿을 활용하여 해결할 수 있습니다. 먼저 다음의 클래스를 보겠습니다.

class Point
{
private:
	int xpos, ypos;
public:
	Point(int x = 0, int y = 0) : xpos(x), ypos(y) {}
	void ShowPosition() const
	{
		cout << '[' << xpos << ", " << ypos << ']' << endl;
	}
};

 

위의 클래스는 좌표 정보를 정수로 표현하도록 정의되어 있습니다. 따라서 실수의 형태로 좌표를 표현해야 하거나, 문자의 형태로 좌표를 표현 및 출력해야 하는 경우에는 별도의 클래스를 정의해야만 합니다. 그러나 위의 클래스를 다음과 같이 템플릿화하면 별도의 클래스를 정의할 필요가 없습니다.

template <typename T>
class Point
{
private:
	T xpos, ypos;
public:
	Point(T x = 0, T y = 0) : xpos(x), ypos(y) {}
	void ShowPosition() const
	{
		cout << '[' << xpos << ", " << ypos << ']' << endl;
	}
};

 

클래스 템플릿의 정의 방법은 함수 템플릿의 정의 방법과 동일하기 때문에 아마도 쉽게 이해가 되었을 것입니다. 이제 다음 예제를 통해서 위의 클래스 템플릿을 기반으로 객체를 생성해 보겠습니다.

//main.cpp 소스 파일로 저장
#include <iostream>
#include <cstring>
using namespace std;

template <typename T>
class Point
{
private:
	T xpos, ypos;
public:
	Point(T x = 0, T y = 0) : xpos(x), ypos(y) {}
	void ShowPosition() const
	{
		cout << '[' << xpos << ", " << ypos << ']' << endl;
	}
};

int main(void)
{
	Point<int> pos1(3, 4);
	pos1.ShowPosition();

	Point<double> pos2(2.4, 3.6);
	pos2.ShowPosition();

	Point<char> pos3('P', 'F');
	pos3.ShowPosition();

	return 0;
}

/*
실행결과

[3, 4]
[2.4, 3.6]
[P, F]

*/

 

(위 예제를 직접 실행해보면서, 템플릿 함수처럼 T의 자료형을 명시해주지 않아도 초기화 값을 바탕으로 컴파일러가 자동으로 T의 자료형을 결정할 수 있는지 확인하기 위해 <int>나 <double>이라고 명시한 것을 지워 보았습니다. 그런데 그렇게 하면 오류가 발생했습니다. '클래스 템플릿 "Point"에 대한 인수 목록이 없습니다.'는 메시지를 보내왔습니다.) 

 

템플릿 함수를 만드는 것과 템플릿 클래스의 객체를 생성하는 것은 크게 다르지 않습니다. 다만, 템플릿 클래스의 객체를 생성할 때는 <int>, <double>과 같은 자료형 정보를 생략할 수 없습니다.

 

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

클래스 템플릿도 멤버 함수를 클래스 외부에 정의하는 것이 가능합니다. 예를 들어 다음과 같이 정의된 클래스 템플릿이 있다면,

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

 

이 템플릿의 멤버 함수 SimpleFunc를 클래스의 외부에 정의할 때 다음과 같이 정의할 수 있습니다.

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

 

이때 몇 가지 주의해야 할 사항이 있습니다.

  • 멤버 함수를 외부에 정의할 때에도 template <typename T> 문구가 필요합니다.
  • 보통의 클래스의 경우, 외부에 멤버 함수를 정의할 때 함수명 앞에 '클래스명::'이 왔습니다. 그러나 클래스 템플릿의 경우에는 '클래스명 <T>::'와 같이 <> 안에 typaname으로 사용되는 문자(?)를 넣어주어야 합니다.

그럼 이제 앞서 보았던 예제를 대상으로 멤버 함수의 선언과 정의를 분리해보겠습니다.

//main.cpp 소스 파일로 저장
#include <iostream>
#include <cstring>
using namespace std;

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

template <typename T>
void Point<T>::ShowPosition() const
{
	cout << '[' << xpos << ", " << ypos << ']' << endl;
}

int main(void)
{
	Point<int> pos1(3, 4);
	pos1.ShowPosition();

	Point<double> pos2(2.4, 3.6);
	pos2.ShowPosition();

	Point<char> pos3('P', 'F');
	pos3.ShowPosition();

	return 0;
}

/*
실행결과

[3, 4]
[2.4, 3.6]
[P, F]

*/

 

위 예제를 보고 문득 typename T를 선언하지 않고 자료형을 명시해주어 특수화가 가능할지 궁금해졌습니다. 그래서 Point <int> 객체를 대상으로 생성자를 호출할 때 주어진 매개 변수에 10을 곱해서 멤버를 초기화하도록 유도해봤습니다. 결과는 아래와 같습니다.

//main.cpp 소스 파일로 저장
#include <iostream>

using namespace std;

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

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

template<>
Point<int>::Point(int x, int y) : xpos(x * 10), ypos(y * 10) {}    //생성자 특수화

template <typename T>
void Point<T>::ShowPosition() const
{
	cout << '[' << xpos << ", " << ypos << ']' << endl;
}

int main(void)
{
	Point<int> pos1(3, 4);
	pos1.ShowPosition();

	Point<double> pos2(2.4, 3.6);
	pos2.ShowPosition();

	Point<char> pos3('P', 'F');
	pos3.ShowPosition();

	return 0;
}

/*
실행결과

[30, 40]
[2.4, 3.6]
[P, F]

*/

 

유도한 대로 실행 결과를 보면 3과 4가 아닌, 30과 40으로 초기화된 것을 확인할 수 있습니다.

 

클래스 템플릿의 선언과 정의를 파일 단위로 분리 시 주의사항

클래스 템플릿의 멤버 함수의 선언과 정의를 분리하는 것까지 제대로 이해가 되었다면, 이를 이용해 파일을 분할하는 것도 어렵지 않아 보입니다. 한 번 파일을 분리해 보겠습니다.

//point.h 헤더 파일로 저장
#ifndef POINT_H
#define POINT_H

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

#endif
//point.cpp 소스 파일로 저장
#include "point.h"
#include <iostream>
using namespace std;

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

template <typename T>
void Point<T>::ShowPosition() const
{
	cout << '[' << xpos << ", " << ypos << ']' << endl;
}
//main.cpp 소스 파일로 저장
#include "point.h"

int main(void)
{
	Point<int> pos1(3, 4);
	pos1.ShowPosition();

	Point<double> pos2(2.4, 3.6);
	pos2.ShowPosition();

	Point<char> pos3('P', 'F');
	pos3.ShowPosition();

	return 0;
}

 

일반적인 클래스를 대상으로 해오던 것과 똑같이 파일을 분할했습니다. 그런데 컴파일을 해보면 문제가 발생합니다. 왜 그런지 알아보겠습니다.

 

컴파일은 파일 단위로 이뤄집니다. main 함수가 있는 main.cpp 파일이 어떻게 컴파일될지 생각해 보겠습니다. 컴파일러가 해당 파일을 컴파일하면 클래스 템플릿 Point를 바탕으로 int형, double형, char형 세 개의 템플릿 클래스를 생성해야 합니다. (함수 템플릿을 바탕으로 템플릿 함수를 만드는 것이 컴파일 단계에서 이뤄졌던 것과 같다고 생각합니다.) 

 

클래스 템플릿을 바탕으로 템플릿 클래스를 만들려면 우선 클래스 템플릿의 모든 정보를 컴파일러가 알 수 있어야 합니다. 그런데 main.cpp 파일에서는 Point 클래스 템플릿의 모든 정보를 알 수가 없습니다. 물론 point.h 헤더 파일을 인클루드 했지만 해당 헤더 파일에는 클래스 템플릿의 선언만 되어 있을 뿐, 정의는 point.cpp 파일에 있기 때문에 역시 모든 정보를 알 수는 없습니다.

 

point.cpp 파일도 컴파일하는 과정에서 같이 컴파일되는 것 아니냐 생각할 수 있습니다. 물론 point.cpp파일도 main.cpp 파일과 같이 병렬 컴파일됩니다. 그러나 컴파일만 같이 될 뿐, main.cpp 파일을 컴파일하면서 point.cpp 파일을 참조하거나 point.cpp 파일을 컴파일하면서 main.cpp 파일을 참조하지는 않습니다.

 

그래서 이런 문제를 해결하려면 클래스 템플릿의 멤버 함수를 point.h 파일에 정의하든지, 아니면 다음과 같이 point.cpp 파일을 인클루드 해야 합니다.

#include "point.cpp"

 

소스 파일을 인클루드 하는 것이 다소 이상하게 보이지만, 템플릿의 경우에는 이러한 방법을 사용해서라도 템플릿의 모든 정보를 소스 파일에 전달해야 합니다.

 

클래스 템플릿의 특수화

함수 템플릿의 특수화처럼 클래스 템플릿도 특수화를 할 수 있습니다. 또 특수화 방법과 개념도 함수 템플릿과 유사합니다. 클래스 템플릿을 특수화하는 이유는 특정 자료형을 기반으로 생성된 객체에 대해, 구분이 되는 다른 행동 양식을 적용하기 위함입니다. 클래스 템플릿을 특수화하는 방법은 다음과 같습니다. 먼저 다음과 같이 정의된 클래스 템플릿이 존재할 때,

template <typename T>
class Simple
{
public:
	T SimpleFunc(T num) {......}
};

 

이를 기반으로 자료형 int에 대해 특수화한 템플릿 클래스는 다음과 같이 정의합니다.

template <>
class Simple<int>
{
public:
	int SimpleFunc(int num) {......}
};

 

이제 다음 예제를 보겠습니다.

//main.cpp 소스 파일로 저장
#include <iostream>
#include <cstring>
using namespace std;

template <typename T>
class Point
{
private:
	T xpos, ypos;
public:
	Point(T x = 0, T y = 0) : xpos(x), ypos(y) {}
	void ShowPosition() const
	{
		cout << '[' << xpos << ", " << ypos << ']' << endl;
	}
};

template <typename T>
class SimpleDataWrapper
{
private:
	T mdata;
public:
	SimpleDataWrapper(T data) : mdata(data) {}
	void ShowDataInfo(void)
	{
		cout << "Data: " << mdata << endl;
	}
};

template <>
class SimpleDataWrapper <char*>
{
private:
	char* mdata;
public:
	SimpleDataWrapper(const char* data)
	{
		mdata = new char[strlen(data) + 1];
		strcpy(mdata, data);
	}
	void ShowDataInfo(void)
	{
		cout << "String : " << mdata << endl;
		cout << "Length : " << strlen(mdata) << endl;
	}
	~SimpleDataWrapper() { delete[] mdata; }
};

template <>
class SimpleDataWrapper <Point<int>>
{
private:
	Point<int> mdata;
public:
	SimpleDataWrapper(int x = 0, int y = 0) : mdata(x, y) {}
	void ShowDataInfo(void)
	{
		mdata.ShowPosition();
	}
};

int main(void)
{
	SimpleDataWrapper<int> iwrap(170);
	iwrap.ShowDataInfo();
	SimpleDataWrapper<char*> swrap("Class Template Specialization");
	swrap.ShowDataInfo();
	SimpleDataWrapper<Point<int>> poswrap(3, 7);
	poswrap.ShowDataInfo();

	return 0;
}

/*
실행결과

Data: 170
String : Class Template Specialization
Length : 29
[3, 7]

*/

 

위 예제에서는 char* 자료형과 Point<int> 자료형에 대해서 특수화를 진행하고 있습니다. 

 

클래스 템플릿의 부분 특수화

클래스 템플릿의 부분 특수화라는 것이 있습니다. 이는 간단한 개념입니다. 만약 아래와 같은 클래스가 있다고 가정해 보겠습니다.

template <typename T1, typename T2>
class MySimple {......};

 

이를 앞서 배운 것처럼 특수화 한다면 다음과 같이 특수화할 수 있을 것입니다.

template <>
class MySimple<int, double> {......};

 

위 특수화는 T1과 T2 둘 모두에 대한 특수화가 동시에 이뤄졌습니다. 반면 다음과 같이 T1이나 T2 중 하나만 특수화를 진행하는 것을 부분 특수화라고 합니다.

template <template T1>
class MySimple<T1, double> {......};

 

이를 활용한 예제를 보겠습니다.

//main.cpp 소스 파일로 저장
#include <iostream>
#include <cstring>
using namespace std;

template <typename T1, typename T2>
class Simple
{
public:
	void WhoAreYou()
	{
		cout << "size of T1 : " << sizeof(T1) << endl;
		cout << "size of T2 : " << sizeof(T2) << endl;
		cout << "<typename T1, typename T2>" << endl;
	}
};

template <>
class Simple<int, double>
{
public:
	void WhoAreYou()
	{
		cout << "size of T1 : " << sizeof(int) << endl;
		cout << "size of T2 : " << sizeof(double) << endl;
		cout << "<int, double>" << endl;
	}
};

template <typename T1>
class Simple<T1, double>
{
public:
	void WhoAreYou()
	{
		cout << "size of T1 : " << sizeof(T1) << endl;
		cout << "size of T2 : " << sizeof(double) << endl;
		cout << "<typename T1, double>" << endl;
	}
};

int main(void)
{
	Simple<char, int> obj1;
	obj1.WhoAreYou();
	Simple<short, double> obj2;
	obj2.WhoAreYou();
	Simple<int, double> obj3;
	obj3.WhoAreYou();

	return 0;
}

/*
실행결과

size of T1 : 1
size of T2 : 4
<typename T1, typename T2>
size of T1 : 2
size of T2 : 8
<typename T1, double>
size of T1 : 4
size of T2 : 8
<int, double>

*/

 

위 예제를 보면 Simple 클래스 템플릿이 <int, double>에 대해 특수화되어 있고, <T1, double>에 대해 부분 특수화되어 있습니다. 그래서 Simpe<char, int> 템플릿 클래스 객체를 생성하면 아무런 특수화되지 않은 클래스 템플릿이 사용되고, Simple<short, double> 템플릿 클래스 객체를 생성하면 부분 특수화된 클래스 템플릿을 사용합니다. 이를 이해하기는 어렵지 않습니다.

 

표준 라이브러리에서 템플릿의 활용

C++ 표준 라이브러리는 템플릿을 기반으로 디자인됩니다. 따라서 템플릿을 잘 알면, 그만큼 라이브러리에 대한 이해도와 활용 능력이 향상됩니다.

 

앞서 Point 클래스를 다음의 형태로 템플릿화했습니다.

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

 

그리고 다음의 형태로 Array 클래스 템플릿을 정의했습니다.

template <typename T>
class Array
{
private:
	T* arr;
	int arrlen;
	Array(const Array& arr) {}
	Array& operator=(const Array& arr) {}
public:
	Array(int len);
	T& operator[] (int index);
	T& operator[] (int index) const;
	int GetArrLen() const;
	~Array();
);

 

그렇다면, 위 클래스 템플릿을 기반으로 Point <int> 템플릿 클래스의 객체를 저장할 수 있는 객체는 어떻게 생성해야 할까요? 이는 사실 어려운 개념은 아닙니다. 만약 Array 클래스 템플릿을 기반으로 int형 데이터를 저장하는 배열 클래스의 객체를 생성한다면 다음과 같이 생성할 수 있습니다.

Array<int> arr(3);

 

만약 Point<int> 템플릿 클래스의 객체를 저장할 수 있는 배열 클래스의 객체를 생성한다면 다음과 같이 생성할 수 있습니다.

Array<Point<int>> arr(3);

 

만약 Point<int> 클래스의 객체를 가리키는 포인터를 저장하는 배열 클래스의 객체를 생성한다면 다음과 같이 생성할 수 있습니다.

Array<Point<int>*> arr(3);

 

위 예를 보면 쉽게 이해가 갈 것입니다. 다음 예제를 보면서 이러한 객체 생성의 규칙을 확인해보겠습니다.

//pointTemplate.h 헤더 파일로 저장
#ifndef POINTTEMPLATE_H
#define POINTTEMPLATE_H
#include <iostream>
using namespace std;

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

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

template <typename T>
void Point<T>::ShowPosition() const
{
	cout << '[' << xpos << ", " << ypos << ']' << endl;
}

#endif
//arrayTemplate.h 헤더 파일로 저장
#ifndef ARRAYTEMPLATE_H
#define ARRAYTEMPLATE_H

template <typename T>
class Array
{
private:
	int len;
	T* arr;
	Array(Array& arr) {}
	Array& operator=(Array& arr) {}
public:
	Array(int lne);
	T& operator[] (int index);
	T& operator[] (int index) const;
	int GetArrLen() const;
	~Array();
};

template <typename T>
Array<T>::Array(int len) : len(len)
{
	arr = new T[len];
}

template <typename T>
T& Array<T>::operator[] (int index)
{
	return arr[index];
}

template <typename T>
T& Array<T>::operator[] (int index) const
{
	return arr[index];
}

template <typename T>
int Array<T>::GetArrLen() const
{
	return len;
}
template <typename T>
Array<T>::~Array()
{
	delete[] arr;
}

#endif
//main.cpp 소스 파일로 저장
#include "pointTemplate.h"
#include "arrayTemplate.h"

int main(void)
{
	Array<Point<int>> oarr1(3);
	oarr1[0] = Point<int>(3, 4);
	oarr1[1] = Point<int>(5, 6);
	oarr1[2] = Point<int>(7, 8);

	for (int i = 0; i < oarr1.GetArrLen(); i++)
	{
		oarr1[i].ShowPosition();
	}

	Array<Point<double>> oarr2(3);
	oarr2[0] = Point<double>(3.14, 4.31);
	oarr2[1] = Point<double>(5.09, 6.07);
	oarr2[2] = Point<double>(7.82, 8.54);

	for (int i = 0; i < oarr2.GetArrLen(); i++)
	{
		oarr2[i].ShowPosition();
	}

	typedef Point<int>* POINT_PTR;    //자료형 재정의
    
	Array<POINT_PTR> oparr(3);
	oparr[0] = new Point<int>(11, 12);
	oparr[1] = new Point<int>(13, 14);
	oparr[2] = new Point<int>(15, 16);

	for (int i = 0; i < oparr.GetArrLen(); i++)
	{
		oparr[i]->ShowPosition();
	}

	delete oparr[0];
	delete oparr[1];
	delete oparr[2];

	return 0;
}

/*
실행결과

[3, 4]
[5, 6]
[7, 8]
[3.14, 4.31]
[5.09, 6.07]
[7.82, 8.54]
[11, 12]
[13, 14]
[15, 16]

*/

 

위 예제에서 보인 것과 같이 Point<int>* 포인터 형을 POINT_PTR로 재정의하여, POINT_PTR을 자료형처럼 사용할 수도 있습니다.

 

Point<int>, Point<double>와 같은 템플릿 클래스의 자료형을 대상으로도 템플릿이 아닌 일반 함수의 정의가 가능하고, 클래스 템플릿 내에서 이러한 함수를 대상으로 friend 선언도 가능합니다. 다음 예제를 통해 확인해보겠습니다.

//pointTemplate.h 헤더 파일로 저장
#ifndef POINTTEMPLATE_H
#define POINTTEMPLATE_H
#include <iostream>
using namespace std;

template <typename T>
class Point
{
private:
	T xpos, ypos;
public:
	Point(T x = 0, T y = 0);
	void ShowPosition() const;
	friend Point<int> operator+(const Point<int>& pos1, const Point<int>& pos2);
	friend ostream& operator<<(ostream& os, const Point<int>& pos);
};

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

template <typename T>
void Point<T>::ShowPosition() const
{
	cout << '[' << xpos << ", " << ypos << ']' << endl;
}

Point<int> operator+(const Point<int>& pos1, const Point<int>& pos2)
{
	return Point<int>(pos1.xpos + pos2.xpos, pos1.ypos + pos2.ypos);
}
ostream& operator<<(ostream& os, const Point<int>& pos)
{
	os << '[' << pos.xpos << ", " << pos.ypos << ']';
	return os;
}

#endif
//main.cpp 소스 파일로 저장
#include "pointTemplate.h"

int main(void)
{
	Point<int> pos1(2, 4);
	Point<int> pos2(4, 8);
	Point<int> pos3 = pos1 + pos2;

	cout << pos1 << pos2 << pos3 << endl;

	return 0;
}

/*
실행결과

[2, 4][4, 8][6, 12]

*/

 

템플릿 인자

템플릿을 정의할 때 결정되지 않은 자료형을 의미하는 용도로 사용되는 T 또는 T1, T2와 같은 문자를 가리켜 '템플릿 매개 변수'라고 합니다. 그리고 템플릿 매개 변수에 전달되는 자료형 정보를 가리켜 '템플릿 인자'라고 합니다.

 

템플릿 매개 변수 자리에 변수 선언

템플릿 매개 변수에는 변수의 선언이 올 수 있습니다. 다음의 클래스 템플릿 정의를 보겠습니다.

template <typename T, int len>
class SimpleArray
{
private:
	T arr[len];
public:
	T& operator[] (int index)
	{
		return arr[index];
	}
};

 

위 클래스 템플릿을 보면 템플릿 매개변수 자리에 int형 변수 len이 선언되어 있습니다. 마치 함수를 연상케 합니다. 이 템플릿을 이용해 템플릿 클래스를 생성할 때는 다음과 같이 생성할 수 있습니다.

SimpleArray<int, 10> arr;

 

템플릿 클래스 객체를 위와 같이 생성하면 arr객체는 int형 데이터를 저장하고, 길이가 10인 배열 클래스가 됩니다. 그런데 배열 클래스 객체를 생성하면서 배열의 길이 정보를 입력할 때는 아래와 같이 클래스 생성자를 이용할 수도 있습니다.

template <typename T>
class Array
{
private:
	int len;
	T* arr;
public:
	Array (int len) : len(len)
	{
		arr = new T[len];
	}
};

int main(void)
{
	Array<int> arr(10);
	
	return 0;
}

 

물론 생생자를 이용하는 방법으로도 충분히 구현할 수 있습니다. 그렇다면 이러한 문법이 생성자와는 다르게 가지는 의미가 무엇이 있을까요? 즉, 생성자를 이용해서도 충분히 구현할 수 있는데, 굳이 이러한 문법을 사용해서 구현해야 할 이유는 어디서 찾을 수 있을까요? 다음 예제를 보겠습니다.

//main.cpp 소스 파일로 저장
#include <iostream>
#include <cstring>
using namespace std;

template <typename T, int len>
class SimpleArray
{
private:
	T arr[len];
public:
	T& operator[] (int index) { return arr[index]; }
	SimpleArray<T, len>& operator=(const SimpleArray<T, len>& ref)
	{
		for (int i = 0; i < len; i++)
		{
			arr[i] = ref.arr[i];
		}
		return *this;
	}
};

int main(void)
{
	SimpleArray<int, 5> i5arr;
	for (int i = 0; i < 5; i++)
	{
		i5arr[i] = i * 10;
	}

	SimpleArray<int, 5> i5arr2;
	i5arr2 = i5arr;
	for (int i = 0; i < 5; i++)
	{
		cout << i5arr2[i] << ", ";
	}
	cout << endl;

	SimpleArray<int, 7> i7arr;
	for (int i = 0; i < 7; i++)
	{
		i7arr[i] = i * 10;
	}

	SimpleArray<int, 7> i7arr2;
	i7arr2 = i7arr;
	for (int i = 0; i < 7; i++)
	{
		cout << i7arr2[i] << ", ";
	}
	cout << endl;

	return 0;
}

/*
실행결과

0, 10, 20, 30, 40,
0, 10, 20, 30, 40, 50, 60,

*/

 

위 예제를 보면 SimpleArray<int, 5>나 SimpleArray<int, 7>는 하나의 자료형이 됩니다. 그리고 해당 객체 간 대입 연산은 자료형이 같아야 수행됩니다. 즉, SimpleArray<int, 5> 템플릿 클래스 객체는 SimpleArray<int, 5> 객체를 대상으로만 대입 연산을 허용합니다. SimpleArray<int, 7> 객체에 대해서는 대입 연산이 불가능합니다.

 

즉, 이러한 문법을 배열 클래스에 사용하게 되면, 자동으로 배열의 길이를 검사하게 되므로 유용합니다. 만약 생성자를 이용해서 배열 클래스 템플릿을 만들었다면, 배열 길이를 검사하는 별도의 코드를 추가로 작성해야 합니다.

 

템플릿 매개 변수의 디폴트 값 지정

템플릿 매개 변수에는 디폴트 값 지정도 가능합니다. 다음 예제를 보겠습니다.

//main.cpp 소스 파일로 저장
#include <iostream>
#include <cstring>
using namespace std;

template <typename T = int, int len = 7>    //디폴트 값 지정
class SimpleArray
{
private:
	T arr[len];
public:
	T& operator[] (int index) { return arr[index]; }
	SimpleArray<T, len>& operator=(const SimpleArray<T, len>& ref)
	{
		for (int i = 0; i < len; i++)
		{
			arr[i] = ref.arr[i];
		}
		return *this;
	}
};

int main(void)
{
	SimpleArray<> arr;
	for (int i = 0; i < 7; i++)
	{
		arr[i] = i + 1;
	}
	for (int i = 0; i < 7; i++)
	{
		cout << arr[i] << " ";
	}
	cout << endl;

	return 0;
}

/*
실행결과

1 2 3 4 5 6 7

*/

 

템플릿 매개 변수에 디폴트 값을 지정했더라도 템플릿 클래스를 생성할 때는 <>를 꼭 사용해야 합니다. 

 

클래스 템플릿과 static

static 멤버 변수는 변수가 선언된 클래스의 객체 간 공유가 가능한 변수입니다. 따라서 다음과 같이 클래스 템플릿이 정의되면,

template <typename T>
class SimpleStaticMem
{
private:
	static T mem;
public:
	void AddMem(T num) {mem += num;}
	void ShowMem() {cout << mem << endl;}
};

template <typename T>
T SimpleStaticMem<T>::mem = 0;

 

컴파일러에 의해서 다음과 같이 템플릿 클래스들이 생성되어,

class SimpleStaticMem<int>
{
private:
	static int mem;
public:
	void AddMem(int num) {mem += num;}
	void ShowMem() {cout << mem << endl;}
};

int SimpleStaticMem<int>::mem = 0;
class SimpleStaticMem<double>
{
private:
	static double mem;
public:
	void AddMem(double num) {mem += num;}
	void ShowMem() {cout << mem << endl;}
};

double SimpleStaticMem<double>::mem = 0;

 

템플릿 클래스 별로 static 멤버 변수를 유지하게 됩니다. 다음의 예제를 통해 이를 확인해 보겠습니다.

//main.cpp 소스 파일로 저장
#include <iostream>
using namespace std;

template <typename T>    //디폴트 값 지정
class SimpleStaticMem
{
private:
	static T mem;
public:
	void AddMem(T num) { mem += num; }
	void ShowMem() { cout << mem << endl; }
};

template <typename T>
T SimpleStaticMem<T>::mem = 0;

int main(void)
{
	SimpleStaticMem<int> obj1;
	SimpleStaticMem<int> obj2;
	obj1.AddMem(2);
	obj2.AddMem(3);
	obj1.ShowMem();

	SimpleStaticMem<long> obj3;
	SimpleStaticMem<long> obj4;
	obj3.AddMem(100);
	obj4.ShowMem();

	return 0;
}

/*
실행결과

5
100

*/

 

위 예제를 보면 SimpleStaticMem<int> 클래스 템플릿의 static 변수 mem과 SimpleStaticMem<long> 클래스 템플릿의 static 변수 mem이 따로 저장되어 있고, 같은 클래스 템플릿을 기반으로 하는 객체들은 같은 변수를 공유함을 확인할 수 있습니다.

 

템플릿 static 멤버 변수 특수화

템플릿 static 멤버 변수의 초기화도 특수화를 할 수 있습니다. 앞서 보인 예제에서는 다음과 같이 모든 mem가 0으로 초기화됩니다.

template <typename T>
T SimpleStaticMem<T>::mem = 0;

 

그래서 SimpleStaticMem<int>의 mem도 0으로 초기화되고, SimpleStaticMem<long>의 mem도 0으로 초기화되었습니다. 만약 SimpleStaticMem<long>에 한해서 mem을 5로 초기화하고 싶다면 다음과 같이 특수화할 수 있습니다.

template <>
long SimpleStaticMem<long>::mem = 5;

 

앞선 예제를 위와 같이 수정해서 확인해 보면 다음과 같습니다.

//main.cpp 소스 파일로 저장
#include <iostream>
using namespace std;

template <typename T>    //디폴트 값 지정
class SimpleStaticMem
{
private:
	static T mem;
public:
	void AddMem(T num) { mem += num; }
	void ShowMem() { cout << mem << endl; }
};

template <typename T>
T SimpleStaticMem<T>::mem = 0;

template <>
long SimpleStaticMem<long>::mem = 5;

int main(void)
{
	SimpleStaticMem<int> obj1;
	SimpleStaticMem<int> obj2;
	obj1.AddMem(2);
	obj2.AddMem(3);
	obj1.ShowMem();

	SimpleStaticMem<long> obj3;
	SimpleStaticMem<long> obj4;
	obj3.AddMem(100);
	obj4.ShowMem();

	return 0;
}

/*
실행결과

5
105

*/

 

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   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
글 보관함