LISTORY

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

IT/C++

[C++] 템플릿(Template)

LiStoryTeller 2018. 11. 26. 13:01


이번에 정리할 내용은 C++ 템플릿이다.


이번에도 앞서 강의들과 마찬가지로 youtube 강의를 참고하여 정리하였다.


12장-1 템플릿

12장-2 함수 템플릿

12장-3 클래스 템플릿

12장 -4 템플릿의 원리 



템플릿(Template)



템플릿에 대한 이해


템플릿은 간단하게 비유하자면 도형자로 볼 수 있다.


한마디로 제품을 만들어내는 틀이다. 우리가 가장 많이 들은 붕어빵을 찍어내는 기계라고도 할 수 있다,


이번 내용에서는 템플릿의 기본적인 사항과 실질적인 원리에 대해 다룰텐데,


일단 지금은 도형자적인 측면으로 설명해보겠다. 


도형자


템플릿은 기본적으로 기능은 결정되어 있다.


위의 도형자를 보면, 그릴 수 있는 도형이 정해져있다. 하지만 이 도형을 무슨 색으로 그릴 지는 결정되어 있지 않다.


1. 그릴 수 있는 도형이 결정되어 있음

2. 무슨 색으로 그릴지는 결정되어 있지 않음


이 두가지 조건이 충족된다면 템플릿이라 할 수 있다.


도형의 색은 펜을 가져다 그리기 시작하는 순간에 결정이 된다.


즉, 자를 사용하는 순간에 결정된다. 이제 예제를 보자


* IntroTemplate1.cpp

int Add(int a, int b)

{

return a+b;

}

함수를 보면, 이미 기능은 결정되어 있다. 기능은 덧셈이다. 템플릿의 첫번째 조건을 만족한다.


두번째 조건은 어떤 색으로 그릴지 결정되어 있지 않다는 건데, 여기서 색은 자료형, 데이터 타입을 뜻한다.


근데 이 함수는 이미 int로 결정되어 있다. 그러므로 이 함수는 템플릿이 아니다.


이제 이 함수를 템플릿화 해보자. 템플릿화 하기 위해선 타입을 결정하지 말아야 한다.



보통 우리는 방정식을 쓸 때 결정되어지지 않은 것을 가리켜  문자로 표현한다.


ex. a + b = 3


우리도 결정되어있지 않다는 의미로 문자를 넣어주자. 근데 이 문자는 관례상 Template의 약자를 대신하여 T를 사용한다.


그럼 이렇게 된다.

T Add(T a, T b)

{

return a + b;

}

보면 덧셈 기능은 여전히 결정되어 있지만 타입은 결정되어 있지 않다. 이게 바로 템플릿이다.


하지만 문제가 있다. 당연한 말이지만 컴파일러가 T가 무엇인지 이해하지 못한다.


우리가 클래스 이름을 T로 할수도 있고 열거형에서도 T를 쓸수 있고 T를 임의로 쓸 경우는 다양하기 때문이다.


그래서 우리는 컴파일러에게 이 T는 Template에서 사용한 T라 설명을 해 줄 필요가 있다. 이를 코드로 이렇게 바꾸었다.

template <typename T>

T Add (T a, T b)

{

return a+b;

}


이 코드를 해석하면 다음과 같다.


T라는 typename에 대해서 아래 존재하는 함수를 템플릿화 하겠다.


이게 바로 템플릿 선언이다. T자료형은 Add라는 함수를 쓰는 순간 결정된다.



이제 예제를 보자.

/*

IntroTemplate1.cpp

*/


#include <iostream>

using std::endl;

using std::cout;


template <typename T>

T Add(T a, T b)

{

return a+b;

}


int main(void)

{

cout<<Add(10, 20)<<endl;

cout<<Add(1.1, 2.2)<<endl;


return 0;

}


위 코드의 실행 결과는 

30

3.3 

이 나올 것이다.


예제를 분석해보겠다.


Add라는 템플릿(정확한 명칭은 함수 템플릿.. 이는 나중에 다시 정리하겠다)이 있고, main에서 Add함수를 호출한다. 


첫번째는 int로 호출했고 두번째는 double 자료형으로 호출하였다.


이렇듯 템플릿을 사용하면 자료형에 독립적으로 함수 호출이 가능하다.


이에 대한 내부적인 매커니즘은 뒤에서 다루도록 하겠다.



템플릿과 연산자 오버로딩의 연관성


11장에서는 STL에 대하여 다뤘는데, 블로그에 따로 정리한 사항은 없다.


* STL : Standard Template Library, 우리가 알고있는 알고리즘 구현되어 있다.


여기서는 템플릿과 연산자 오버로딩의 연관성에 대해 조금만 설명하도록 하겠다.


위의 예제에서 우리가 String이라는 클래스를 하나 더 추가했다고 가정해보자.


class String{

};


그럼 이 String 클래스의 객체를 두개 만들어서 Add 함수에 전달할 수 있을까?


인자로 받을 수 있다. 인자로 받는 것 자체에는 문제가 없는데 다른 곳에 문제가 있다.


문제는 뭐냐면 객체들끼리 덧셈 가능한가? 즉, 주어진 조건에서 연산이 가능한가가 문제이다.


예를 들어 Add가 STL로 존재한다고 가정해보자.


우리가 Add 함수의 인자로 무언가를 전달하기 위해서는 덧셈이 가능해야 한다.


사용자가 정의한 클래스의 객체도 STL을 기반으로 사용할 수 있다.


하지만 STL이 요구하는 게 있다. 함수를 호출하며 인자 전달 시, 조건을 만족해야한다.



다른 예제를 만들어보자.

/*

IntroTemplate1.cpp

*/


#include <iostream>

#include <string>

using std::endl;

using std::cout;


using std::string;


template <typename T>

T Add(T a, T b)

{

return a+b;

}


int main(void)

{

string str1 = "abc";

string str2 = "def";


cout<<Add(str2, str1)<<endl;


return 0;

}


이걸 컴파일하여 결과를 보면

defabc 

가 나온다.


즉, 템플릿의 특성은 이렇다.


템플릿은 기본 자료형 뿐만이 아니라 요구사항만 만족이 되면 사용자가 정의한 클래스의 객체도 인자로 받을 수 있다.




함수 템플릿


함수템플릿 vs 템플릿 함수


위의 Add 함수의 정확한 명칭은 함수 템플릿일까 템플릿 함수일까


템플릿의 원리를 이해하기 전에는 잘 이해하기 힘들다.


함수 템플릿은 함수를 기반으로 된 템플릿이고, 템플릿 함수는 템플릿을 기반으로 하는 함수이다.


그럼 Add 함수와 밑의 예제는 함수 템플릿이 맞다.


이유는 밑에서 다시 다루도록 하겠다.



둘 이상의 타입에 대해서 템플릿화


둘 이상의 타입에 대해서 템플릿화할 수 있을까? 예제를 보고 확인하겠다.


-IntroTemplate3.cpp

/*

IntroTemplate3.cpp

*/


#include <iostream>

using std::endl;

using std::coutl;


template <typemane T1, typename T2> // 함수 템플릿 정의

void ShowData(T1 a, T2 b)

{

cout<<a<<endl;

cout<<b<<endl;

}


int main(void)

{

ShowData(1, 2);

ShowData(3, 2.5);


return 0;

}

다시 말하자면 ShowData() 역시 함수가 아니라 함수 템플릿이다. 


이렇게 위의 코드처럼 인자의 타입을 달리하고 싶을 경우가 있다.


위 함수 템플릿을 해석하면 다음과 같다.


자료형 T1과 T2에 대하 아래에 함수를 템플릿화 하겠다.


이 경우에는 main 함수에서 자료형을 다르게 전달할 수 있다.


※ Template 디자인을 깊게 설명하지 않는 이유?


템플릿 통해서 프로그래밍하는 거를 Generic Programing 이라고 한다.


즉, 템플릿을 보면 자료형이 일반화 된다. (여기서 일반화된다는 것은 범위를 넓힌다는 뜻이다) 자료형의 제한을 안받도록 일반화시킨다.


단순히 템플릿을 사용한다고 제네릭하게 된다는게 아니라 일반화시킬 수 있는 기법들이 존재한다.


이렇게 일반화 시키는건 사실 쉽지 않다. 여러가지 상황을 고려하여 디자인을 하면 굉장히 오랜 시간이 걸린다.


템플릿을 기반으로 모든 클래스나 함수를 일반화시키면 더 오랜 시간이 걸린다.


그래서 보통 라이브러리를 구현하면 템플릿을 기반으로 generic Programing을 도전하겠지만 대부분의 경우 그렇게 하지 않는다.



함수 템플릿의 특수화


- SepciFuncTemplate2.cpp

#include <iostream>

using std::endl;

using std::cout;


template <typename T> // 함수 템플릿 정의

int SizeOf(T a)

{

return Sizeof(a);

}


int main(void)

{

int i = 10;

double e = 7.7;

char* str="Good morning!";


cout<<SizeOf(i)<<endl;

cout<<SizeOf(e)<<endl;

cout<<SizeOf(str)<<endl;


return 0;

}

SizeOf는 전달된 인자의 사이즈를 반환시켜 주는 함수 템플릿이다.


결과는 

4

8

4

가 나온다.


마지막은 포인터이므로 4가 출력되는건 지극히 정상적이다. 하지만 우리가 알고싶은건 문자열의 길이이다..


이럴 경우 제한을 지닌다. 문자열의 길이를 계산할 수 없다. 그럼 이 sizeof가 strlen 함수의 호출로 바뀌어야 한다.


지금은 그게 불가능하다.


그럼 int나 double이 왔을 땐 위와같이 처리하고, char*가 왔을 때만 특별히 다른 연산을 할 수 없을까?


그런 요구를 반영한 것이 함수 템플릿의 특수화이다.


아래와 같이 템플릿을 정의하고, 추가적으로 하나의 템플릿을 하나 더 정의하자.

template <typename T>

int SizeOf(T a)

{

return Sizeof(a);

}

특수화하면 다음과 같다.

template<> // 특수화하겠다.

int SizeOf(char *a)

{

return strlen(a);

}

이 함수 템플릿을 해석해보면 아래 존재하는 대상에 대해 특수화 하겠다라는 뜻이다.


SizeOf 함수는 SizeOf 템플릿에 대해 특수화되어있다.


이는 T가 char*로 전달이 되면 아래의 함수를 대신 호출하겠다는 의미가 된다.


다시 예제를 보겠다.

#include <iostream>

using std::endl;

using std::cout;


template <typename T> // 함수 템플릿 정의

int SizeOf(T a)

{

return Sizeof(a);

}


template <> // 함수 템플릿 특수화

int SizeOf(char* a)

{

return strlen(a);

}


int main(void)

{

int i = 10;

double e = 7.7;

char* str="Good morning!";


cout<<SizeOf(i)<<endl;

cout<<SizeOf(e)<<endl;

cout<<SizeOf(str)<<endl;


return 0;

}

결과를 보면 다음과 같다.

4

8

13




클래스 템플릿


객체를 생성하는 방법에 대해 이야기해보자.


클래스가 있다. 이를 템플릿으로 선언하는 방법은 함수를 템플릿으로 선언하는것과 다를게 없다.


예제를 통해 살펴보겠다.

class Data

{

int data;

public:

Data(int d) { data = d; }

void SetData(int d) {

data = d;

}

int GetData() {

return data;

}

};

이제 위의 예제를 템플릿화 시키겠다. 템플릿화를 원하는 자료형을 T로 바꾸어 주자. 


여기선 int 형을 T로 바꾸어주었다.

template<typename T>

class Data

{

T data;

public:

Data(T d) { data=d; }

void SetData(T d) {

data = d;

}

T GetData() {

return data;

}

};

함수와 마찬가지로, T로 바꾼 다음엔 T가 어떠한 의미를 지니는지 설명해주어야 한다.


그래서 클래스 위에 T는 아래에 존재하는 클래스를 템플릿화하기 필요한 자료형이라는 의미로 template <typename T>를 선언해주었다. 


이제 템플릿으로 구현된 데이터 클래스 객체를 만들어 보자


Data d1(10);


여기서 10은 생성자에서 봤을 때 int형 자료이다.


그럼 보통 우리는 int형으로 구체화된 데이터 객체(Data)가 생성될 것이라 생각한다.


하지만 여기에는 문제가 있다. 우리가 객체를 생성할 때, 생성되는 순서가 있다.


객체 생성 순서?

1. 메모리공간 할당 

2. 생성자 호출


d1 객체를 생성하기 위해서, 일단 해당 이름으로 메모리 공간을 할당해야 한다.


근데 문제는 메모리 공간을 할당하기 위해선 자료형을 알아야 한다는 점이다. int일 경우에는 4, char일 경우에는 1이 할당된다.


즉, 메모리 공간을 할당하기 위해선 T가 결정이 나야하고, 이 결정이 나지 않은 상태에서는 메모리 공간을 할당할 수 없다.


근데 T가 결정되기 위해선 생성자가 호출이 되어야한다.  보면 순서가 맞지 않는다.


T가 먼저 결정이 나야 메모리 공간을 할당 할 수 있는데 생성자가 호출되야지만 T를 결정지을 수 있다.


이렇듯 생성자를 통해 전달되는 인자의 정보는 참조하는 시기가 늦기 때문에


템플릿을 기반으로 한 데이터 객체를 만들 때에는 반드시 어떠한 자료형으로 구체화시킬지 미리 선언해주어야 한다.


밑에 main을 보자

int main(void)

{

Data<int> d1(0);

d1.SetData(10);

Data<char> d2('a);

...

}

Data<int> d1(0);


이 코드를 해석해보면 다음과 같다.


데이터 클래스의 객체를 생성하는 과정에서 템플릿으로 선언이 되어있는 T를 int로 인식시켜라


즉, 생성자 호출시 인자를 가지고 결정을 지으면 메모리공간을 할당하지 못하므로, 어떠한 타입으로 템플릿을 구체화시킬지 정해두는 것이다.


다시 예제를 보겠다.


아래의 코드를 보면 Data라는 템플릿이 있다. 


이는 Data<T>가 이름이 된고, 엄밀히 말하면 클래스는 아니다.

template <typename T>

class Data

{

T data;

public:

Data(T d) { data=d; }

void SetData(T d) {

data = d;

}

T GetData() {

return data;

}

};

이제 이 코드의 선언/정의를 분리하겠다.

templage <typename T>

class Data

{

T data;

public:

Data(T d);

void SetData(T d);

T GetData();

};


template <typename T>

Data<T>::Data(T d) {

data = d;

}

template <typename T>

void Data<T>::GetData() {

return data;

}

이 클래스 템플릿의 이름은 Data<T>이므로 해당 템플릿의 함수 정의 앞에는 Data<T>라는 이름을 붙여주어야 한다.


그 다음에 T가 뭔지 또 다시 설명을 붙여놓았는데, 이 이유는 template <typename T>는 바로 아래에 있는 선언에만 범위의 유효성을 지니기 때문이다.


기억할것

1. Data라는 템플릿은 Data<T>이름을 가진다.

2. 선언을 앞에 항상 붙여주어야 한다.





템플릿의 원리 이해


앞선 정리에서 함수 템플릿, 클래스 템플릿 모두 함수, 클래스가 아닌 템플릿임을 강조하였다.


이 정리에선 그 이유를 설명하겠다.




T Add(T a, T b)  는 함수 템플릿이다(함수가 아니다).


이번 시간에는 템플릿이 어떻게 구성되는지 원리를 말할건데, 결론부터 이야기하자면 템플릿은 함수 오버로딩과 유사한 형태로 구성된다.


템플릿은 함수 오버로딩과 유사한 형태로 구성된다.


위에 그림을 간단히 설명해보겠다.


Add 템플릿이 있고, main이 템플릿에 값을 전달한다.


그럼 인자의 데이터 형에 따라 오른쪽의 함수들이 만들어진다.


우리가 main 함수에서 호출하는 Add 함수는 왼쪽이 아니라 오른쪽의 함수들이다.


즉, 템플릿을 기반으로해서 코드가 만들어진다. 이는 컴파일러가 컴파일 시 만들어진다.


그러므로 함수 템플릿은 함수를 만들 수 있는 템플릿에 지나지 않는다. 함수가 아니므로, 절대 우리가 호출할 일은 없다.


대신 컴파일러가 요구(여기선 인자형)에 따라 함수를 만들어 내는데, 그렇게 만들어진 함수가 템플릿 함수이다.


즉, 함수 템플릿은 함수를 만들어 내기 위한 틀이고, 템플릿 함수는 실제로 호출이 가능한 함수이이자 실제로 호출되는 함수이다.


이는 클래스에도 똑같이 적용된다.


예를 들어보자.  A라는 클래스를 템플릿으로 만든다고 가정해보겠다.


그럼 A.h에는 클래스에 대한 선언을, A.cpp에는 A 클래스에 대한 정의를 적기 마련이다.


이 클래스를 템플릿화 시키기 위해서 선언과 정의를 모두 템플릿화 시켜주었다. 하지만 이렇게 하고 컴파일을 해도 에러가 뜬다.


main.cpp에 

A<int> a(10); 

코드를 사용하여 객체를 생성해주었다.


main.cpp는 이때 A.h 헤더파일을 include한다. 문제는 헤더파일만 include 한다는 것이다.


템플릿은 앞서 말했듯이, 그 자체가 호출되는 주체가 아니다. 이는 클래스도 마찬가지이다.


클래스 코드를 만들어 줄 뿐, 그 자체가 객체가 되지 않는다.


즉, 위에 코드를 보면 int 형으로 구체화된 A 클래스가 새로 만들어져야한다.


A 클래스를 새로 만들기 위해, 헤더파일에서 클래스의 선언은 참조할 수 있을 지 모르지만 정의는 참조할 수 없다.


다른 파일과 연결해주는 것은 링커의 역할이다. 컴파일러는 할 수 없다.


이 문제를 해결하기 위해선 클래스 템플릿의 선언과 정의를 분리하지 말고 하나의 파일에 넣어야 한다.


즉, A.h에 모든 코드를 넣으면 된다.





'IT > C++' 카테고리의 다른 글

[C++] 복사 생성자  (0) 2018.11.22
[C++] 레퍼런스 함수 정리  (0) 2018.11.22
Comments