2016년 3월 15일 화요일

[c++] 템플릿 ( Template )

템플릿 ( Template ) 
 Template 이라는 단어는 모형자라는 의미를 가진단어로, C++에서 템플릿은 어떤 제품을 만들어내는 틀, 예를 들어 붕어빵에 비교해 보자면, 붕어빵을 만들어 내는 틀을 템플릿이라 말 할 수 있다. 템플릿의 특징은 기능은 이미 결정되어 있지만, 데이터 타입은 결정되어 있지 않는다는 특징을 가지고 있다. 아래에는 Sub라는 함수가 있다.
  1. int Sub(int a, int b)  
  2. {  
  3.     return a-b;  
  4. }  
 Sub라는 함수는 두개의 int 형 데이터를 서로 빼주는 그런 함수이다. 이런 함수를 한번 템플릿화 해보겠는데, 템플릿의 특징은 무엇이라 했는가? 바로 기능은 결정되어 있고 데이터 타입은 결정되지 않는 그런것이라고 언급을 했었다. 여기서 기능은 Sub(바로 빼주는) 거라고 할 수 있고, 데이터 타입은 int 이다. 이것은 유념해 두고 템플릿화 해보면 다음과 같이 템플릿화 할 수 잇다. 
  1. template <typename T>  
  2. T Sub(T a, T b)   
  3. {  
  4.     return a-b;  
  5. }  
 여기서 template <typename T>는 T라는 타입이름에 대해서 그 아래 존재하는 함수를 템플릿화 하겠다는 의미이다. 여기서 T라는 자료형은 Sub라는 이 함수를 사용할 때, 결정된다. 그럼 이 템플릿화 한 함수를 한번 직접 써보자.
  1. #include <iostream>  
  2. using namespace std;  
  3.   
  4. template <typename T1, typename T2> // 함수 템플릿 정의  
  5. void ShowData(T1 a, T2 b)   
  6. {  
  7.     cout<<a << " ";  
  8.     cout<<b<<endl;    
  9. }  
  10.   
  11. int main(void)  
  12. {  
  13.     ShowData(1, 2);  
  14.     ShowData(3, 2.5);  
  15.   
  16.     return 0;  
  17. }  

이와 같이 템플릿(Template)은 자료형에 독립적으로 쓸 수 있다는 장점을 가지고 있다.

< 함수 템플릿 > 
 템플릿의 종류에는 함수 템플릿, 클래스 템플릿 이 두가지로 나뉠 수 있다. 책마다 함수 템플릿 또는  템플릿 함수로 이 두가지로 표기가 되어 있는데, 이런 명사 두개가 오는 단어들은 뒤에가 진짜다. 우선 함수 템플릿, 템플릿 함수의 의미를 정리 해보자.
 - 함수 템플릿 : 함수를 기반으로 구현이 된 템플릿 (함수가 아니라는 뜻이다)
 - 템플릿 함수 : 템플릿을 기반으로 한 함수라는 뜻
 우리가 앞서 본 템플릿 예제는 함수 템플릿이다. 이 함수 템플릿에 대해 좀 더 자세히 알아 보자.

< 둘이상 타입에 대한 템플릿 > 
  1. #include <iostream>  
  2. using namespace std;  
  3.   
  4. template <typename T> // 함수 템플릿 정의  
  5. void ShowData(T a, T b)   
  6. {  
  7.     cout<<a<<endl;  
  8.     cout<<b<<endl;    
  9. }  
  10.   
  11. int main(void)  
  12. {  
  13.     ShowData(1, 2);  
  14.     ShowData(3, 2.5); //error  
  15.   
  16.     return 0;  
  17. }  
 위와 같이 showdata의 인자값의 데이터 자료형을 각각 달리 해주면, 에러가 나오는 것을 알 수 있다. 이 경우 아래와 같이 사용하면 된다.
  1. #include <iostream>  
  2. using namespace std;  
  3.   
  4. template <typename T1,typename T2> // 함수 템플릿 정의  
  5. void ShowData(T1 a, T2 b)   
  6. {  
  7.     cout<<a<< " ";  
  8.     cout<<b<<endl;    
  9. }  
  10.   
  11. int main(void)  
  12. {  
  13.     ShowData(1, 2);  
  14.     ShowData(3, 2.5);  
  15.   
  16.     return 0;  
  17. }  


< 템플릿 특수화 > 
 특수화란 범위를 좁히는 것을 말한다. 다음과 같은 경우가 있다고 한번 가정해 보자. 
  1. #include <iostream>  
  2. using std::endl;  
  3. using std::cout;  
  4.   
  5. template <typename T> // 함수 템플릿 정의  
  6. int SizeOf(T a)   
  7. {  
  8.     return sizeof(a);  
  9. }  
  10.   
  11. int main(void)  
  12. {  
  13.     int i=10;  
  14.     double e=7.7;  
  15.     char* str="Good morning!";  
  16.   
  17.     cout<<SizeOf(i)<<endl;  
  18.     cout<<SizeOf(e)<<endl;  
  19.     cout<<SizeOf(str)<<endl;  
  20.   
  21.     return 0;  
  22. }  
 원래 있던 sizeof 함수를 템플릿화를 시켰다. 뭐 이상없이 찍힌다. 캐릭터형 포인터는 4가 찍히는 것이 맞겠지만, 사용자는 그 데이터형의 사이즈는 관심없고, 문자열이 들어 있다면 그 문자열의 크기가 얼마 인지 알고 싶다고 해보자. 하지만 위와 같이 템플릿화한 경우에는 변수 사이즈만 찍힐 것이다. 그래서 캐릭터형 포인터의 데이터 타입을 받으면 그 경우에는 문자열 크기를 알아 보는 strlen 함수를 써서 문자열 길이를 반환하는 것. 즉, 어떠한 경우에서만 특별히 다른 행동을 했으면 좋겠다. 이것이 바로 특수화다. 문법 사용은 다음과 같이 쓴다.
  1. #include <iostream>  
  2. using std::endl;  
  3. using std::cout;  
  4.   
  5. template <typename T> // 함수 템플릿 정의  
  6. int SizeOf(T a)   
  7. {  
  8.     return sizeof(a);  
  9. }  
  10.   
  11. template<> // 특수화  
  12. int SizeOf(char* a)   
  13. {  
  14.     return strlen(a);  
  15. }  
  16.   
  17. int main(void)  
  18. {  
  19.     int i=10;  
  20.     double e=7.7;  
  21.     char* str="Good morning!";  
  22.   
  23.     cout<<SizeOf(i)<<endl;  
  24.     cout<<SizeOf(e)<<endl;  
  25.     cout<<SizeOf(str)<<endl;  
  26.   
  27.     return 0;  
  28. }  



< 클래스 템플릿 > 
클래스를 템플릿 하고자 하면, 원하는 자료형만 T로 바꾸면 된다. 아래의 클래스를 예로 들어 보자. 
  1. class Data  
  2. {  
  3.     int data;  
  4. public:  
  5.     Data(int d){  
  6.         data=d;  
  7.     }  
  8.     void SetData(int d){  
  9.         data=d;  
  10.     }  
  11.     int GetData(){  
  12.         return data;  
  13.     }  
  14. };  
 템플릿화~
  1. template <typename T>  
  2. class Data  
  3. {  
  4.     T data;  
  5. public:  
  6.     Data(T d){  
  7.         data=d;  
  8.     }  
  9.     void SetData(T d){  
  10.         data=d;  
  11.     }  
  12.     T GetData(){  
  13.         return data;  
  14.     }  
  15. };  
 그럼 이렇게 선언한 클래스 템플릿을 main에서는 어떻게 사용할까? 이전처럼 사용하면 되는 것일까? Data d1(10); 이렇게 사용하면 문제가 된다. 무엇이 문제가 되는지 한번 자세히 알아 보자.
 우선 객체 생성 순서에 대해 생각해 보자. 메모리 할당 -> 생성자 호출.... 이런식이다. 그러면 Data d1(10); 이 문장에서  d1 이라는 이름으로 메모리 공간을 할당을 해야 할 것이다. 하지만 우리는 클래스 템플릿을 사용하고 있기 때문에,T가 어떤 데이터형을 사용해야 할건지가 결정이 나야 메모리 할당이 이루어는 구조를 가지고 있다. T가 결정나는 시점은 생성자가 호출되어야만 (괄호 안의 10이 호출 되어야만..) T가 int형 데이터 인지 알 수 있으므로 이전과 같은 문법을 사용한다면 메모리 할당을 전혀 하지 못하게 되는 것이다. 
 그래서 템플릿 클래스에서 Data 객체를 만들기 위해서는 생성자를 통해서 전달되는 인자의 정보를 참조하는 시기가 늦기 때문에 구체적으로 어떤 타입으로 템플릿을 구체화 시킬지 명시적으로 선언을 해줘야 한다. 아래의 실사용 예제를 보자.
  1. #include <iostream>  
  2. using namespace std;  
  3.   
  4. template <typename T> //Data 라는 템플릿 Data<T>가 이것의 이름이 된다.  
  5. class Data  
  6. {  
  7.     T data;  
  8. public:  
  9.     Data(T d);  
  10.     void SetData(T d);  
  11.     T GetData();  
  12. };  
  13.   
  14. template <typename T>  
  15. Data<T>::Data(T d){  
  16.     data=d;  
  17. }  
  18.   
  19. template <typename T>  
  20. void Data<T>::SetData(T d){  
  21.     data=d;  
  22. }  
  23.   
  24. template <typename T>  
  25. T Data<T>::GetData(){  
  26.     return data;  
  27. }  
  28.   
  29.   
  30. int main(void)  
  31. {  
  32.     Data<int> d1(0);  
  33.     d1.SetData(10);  
  34.   
  35.     Data<char> d2('a');  
  36.   
  37.     cout<<d1.GetData()<<endl;  
  38.     cout<<d2.GetData()<<endl;  
  39.   
  40.     return 0;  
  41. }  



템플릿의 동작원리는 함수 오버로딩(Function Overloading)과 유사한 형태로 구성이 된다. 다음 템플릿화된 소스코드를 보자.
  1. #include <iostream>  
  2. using namespace std;  
  3.   
  4. template <typename T>  
  5. T Add(T a, T b)  
  6. {  
  7.     return a+b;  
  8. }  
  9.   
  10. void main()  
  11. {  
  12.     cout << Add(10,20);  
  13. }  
 실제 위의 소스 코드는 어떻게 동작을 할까? 실제 main 함수에서 Add(10,20); 이 문장에서는 실제 아래와 같은 함수가 만들어진다.
  1. int Add(int a, int b)  
  2. {  
  3.     return a+b;  
  4. }  
 그래서 우리가 컴파일 하고 실행할 때 실제로 호출 하는 함수는 템플릿 기반으로 만들어진 int Add 함수를 컴파일러가 자동적으로 생성하고 호출 하는 것이다. 우리가 지금까지 컴파일러가 코드를 자동적으로 생성해 주는 경우는 없었지만, 컴파일 타임에 이런 인스턴스화가 일어난다. 이렇게 함수 템플릿이 인스턴스화 되어 나온것이 바로 템플릿 함수이다. 함수 템플릿과 템플릿 함수 이런 차이점이 있다.
 클래스 템플릿도 마찬가지이다. 하지만 클래스 템플릿을 만들 때, 주의할 점이 하나있다. 일반적으로 우리는 클래스를 만들때, (지금까지 예제는 main 함수가 있는 cpp 한곳에 만들었지만..) 헤더(header) 파일과, cpp 파일로 분리 해서, 헤더에는 선언만 cpp에는 정의만 이렇게 나눠서 작성을 할 것이다. 
 앞서 말했듯이, 함수 템플릿이 인스턴스화 되어 템플릿 함수가 실제로 호출이 일어나 프로그램을 실행하는 것이다. 템플릿 함수는 컴파일러가 만드는데 이렇게 파일을 나눠서 구현을 해버리고, main 함수에서 실제 호출 하려고 하면, 에러가 나는 것을 알 수 있다.

 cpp에서 정의 부분을 넣는데, cpp의 정의 부분은 링커(Linker)가 호출해주므로 이런 문제가 발생하는것이다. 그래서 만약 우리가 클래스 템플릿을 만들어서 쓰고 싶다면 헤더에 선언과 정의를 한번에 다 하는 것이 좋다.

넘겨진 파라미터를 이용해서 구체화 하는 과정에서 명시적으로 정의 및 구현 부가 보여야 한다. 


< 클래스 템플릿을 헤더에 선언과 정의를 해서 써야 하는 이유 > 
나중에는 컴파일된 Object File에 WriteCommand라는 Symbol이 있기는 한지 의문을 품고 Object File들을 직접 뜯어보기에까지 이르렀습니다. [...]
당연히 있을거라고 생각했는데 웬걸, 그런 심볼은 Object파일 안에 없었습니다. Object File에 Symbol이 존재하지 않는다는 것은, 해당 Method의 구현이 아예 컴파일되지 않았다는 것입니다.
여기에서 실마리를 얻고, 다시 몇시간 구글링과 삽질을 한 끝에 원인을 알아내고 오류를 고칠 수 있었습니다.

결론을 먼저 말하자면, 문제는 Template Class의 상속이 아닌 Template Class 자체에 있었습니다. 핵심은 'Template Class의 정의와 구현은 한 파일 안에 있어야 한다.' 라는 점입니다. 
보통 C++에서는 여러 가지 장점 때문에 Class의 정의가 있는 헤더 파일과 구현이 있는 소스 파일을 분리해서 작성합니다. 이렇게 분리하더라도 컴파일 이후 링커가 컴파일된 소스파일들을 하나로 연결해 주기 때문에 문제가 없는 것이지요.
여기서 짚고 가야 할 중요한 사실이 하나 있는데, 바로 'Template Class는 Class가 아니다.' 라는 사실입니다. 이게 뭔소린가 하면, Template Class는 Class를 찍어내기 위한 '틀'일 뿐, Class 자체는 아니라는 것입니다.
컴파일러는 파일 단위로 컴파일을 진행하기 때문에 어떤 헤더파일이 어떤 소스파일과 짝인지 여부는 고려하지 않고 컴파일을 합니다. 그것도 헤더파일은 따로 컴파일하지 않으며, 컴파일 대상은 오직 소스파일들(*.c, *.cpp)뿐입니다.
소스파일에 헤더파일이 Include 되어 있다면, 헤더파일을 그대로 복붙하고 컴파일을 진행할 뿐입니다. 어떠한 소스파일에도 포함되지 않은 헤더파일이 존재한다면, 그 헤더파일은 컴파일되지 않습니다.

위의 소스코드를 보면, LCDController.cpp에서 FrameBuffer.hpp를 Include하였습니다. FrameBuffer.hpp에는 FrameBuffer Template Class의 원형이 정의되어 있고, LCDController에서 상속할 때 Parameter를 명시해 줬으므로 컴파일 과정에서DataBusWidth = uint16_t인 FrameBuffer Class가 생성됩니다.
단, 이 때 FrameBuffer Class의 Method들은 정의되지 않았으므로 이후 과정은 링커에게 넘기게 되며, LCDController의 컴파일 과정은 오류 없이 종료됩니다.
다음 과정으로 FrameBuffer.cpp의 컴파일을 진행하는데, 여기에도 역시 FrameBuffer.hpp가 포함되어 있습니다. 이 컴파일과정에서 FrameBuffer Template Class는 정의되지만, 어디에도 Template Class Parameter인 DataBusWidth를 명시해서 FrameBuffer Class를 찍어내는 구문을 찾을 수 없습니다.
이 컴파일 과정에서 FrameBuffer를 찍어내는 틀(Template)은 한 번도 사용되지 않고 그냥 버려지게 됩니다. 즉, 컴파일 결과물에 FrameBuffer Class는 존재하지 않으며, Class Method들도 하나도 정의되지 않습니다. (DataBusWidth를 알 길이 없으니, 정의할 수 없는것입니다.)
결과적으로 LCDController.cpp를 컴파일할 때 다른 파일에 FrameBuffer Class Method들이 정의가 되어 있을 것으로 예상하고 컴파일을 마쳤으나, 정작 FrameBuffer.cpp를 컴파일할때는 FrameBuffer Class가 생성조차 되지 않았습니다. 따라서 링크 과정에서 당연히 Symbol을 찾을 수 없다는 오류가 발생하게 되는것이지요.

댓글 없음:

댓글 쓰기

[Effective C++] 항목 30 : 인라인 함수는 미주알고주알 따져서 이해해 두자.

인라인 함수를 사용하면 컴파일러가 함수 본문에 대해 문맥별 최적화를 걸기가 용이해집니다. 인라인 함수의 아이디어는  함수 호출문을 그 함수의 본문으로 바꿔치기하자는 것  남발했다가는 코드의 크기가 커질 게 뻔하다. 인라인 함수로 부풀려진 ...